# Sign-in and permissions

Place a store behind Lys, grant access to its database and present each caller's own pass.

Lys issues passes and decides grants. Haematite verifies the pass and enforces each route's right on the named resource. The app credential places resources and asks live questions. It never stands in for the person or agent doing the work.

## Before you begin

Have a Lys issuer, a registered haematite app and a database resource with an id such as `wards`. The schema must agree with the binary. The service user needs access to its app credential file. Each caller needs its own sign-in or agent proof.

A service without `lys` is available to its owner through a private socket and makes no Lys decisions. Adding a door or an HTTP listener does not add store enforcement to that service. Configure the store itself before offering shared access.

## Check the permission schema

Run the check with the haematite app credential. The credential is read from its file and never printed.

**Terminal**

```bash
haematite lys-schema --issuer https://lys.example.au \
  --credential-file /etc/haematite/lys-app-credential --check
```


The check reads the schema Lys holds and compares it with the schema compiled into this binary from `docs/design/access/schema.json`. Lists of names are compared as sets. Empty optional members do not create drift.

If the result is `schema_drift`, register the intended schema, then check again.

**Terminal**

```bash
haematite lys-schema --issuer https://lys.example.au \
  --credential-file /etc/haematite/lys-app-credential --register
```


A registration carries the version it replaces and an operation id derived from the schema. A repeated registration finds the same change. A `proposed` result means an administrator must decide it on Lys's Apps screen. It is not evidence that the new schema is active. Only a successful check proves agreement.

- `haematite.database`: Actions read, query, write, branch, merge, restore, copy and serve. read, query and write are decided from the verified pass. The other actions require a live Lys answer. Observer gives read and query; writer adds write and branch; administrator includes every action. Agents may be granted read, query, write and branch.
- `haematite.collection`: Child of a database. Actions read, query and write, all available to agents and decided from the verified pass. Resource ids are database plus a dot plus the collection name.
- `haematite.branch`: Child of a database. Actions read, query, write and merge. read, query and write are available to agents and decided from the verified pass. merge requires a live decision. Ids are database plus a dot plus branch name.
- `haematite.backup`: Child of a database. Actions restore and copy, both deliberate. This schema does not create a backup or restore CLI command.


`--record` saves Lys's answer and its version. `--recorded` compares that file without a network or credential. A matching recorded answer proves what the file contains, not what Lys holds now.

**Terminal**

```bash
haematite lys-schema --issuer https://lys.example.au \
  --credential-file /etc/haematite/lys-app-credential --record schema-record.json
haematite lys-schema --recorded schema-record.json
```


## Put the store behind Lys

Add the Lys object to the service configuration, then start the service with that file.

**Add Lys to service.json**

```json
{
  "lys": {
    "issuer": "https://lys.example.au",
    "database": "wards",
    "placement_credential_file": "/etc/haematite/lys-app-credential"
  }
}
```


The issuer must be an origin, not an address ending in `/api`. The database id must agree with the resource in Lys. With a placement credential, the service places existing collections and branches at startup and places new ones after their writes commit. Their resource ids are `wards.patients` and `wards.main` for this example.

Placement under a database lets Lys expand a database grant onto its children. A resource placed as restricted does not acquire a right merely because its parent has one. Without a placement credential the service does not place resources automatically. Place them in Lys or grant the exact resources they use.

The service listens while it reads published keys in the background. Until they arrive, decided calls receive `lys_keys_pending`. It does not admit a request without verification because the issuer is unavailable. An unknown signing key triggers a published-key refresh before its final refusal.

## Grant the database rights

For a reader, grant `read` and `query` on `haematite.database` with id `wards`. For a writer, add `write`. A mutation also requires write access to its branch and the collection or other resource its change touches. Confirm that Lys has placed those children and included their rights in the pass.

An agent may receive `branch`, but the schema does not permit agents to receive database `merge`, `restore`, `copy` or `serve`. A person with the required right handles those deliberate acts. A database role is expanded by Lys; Haematite does not infer a local role from the caller's name.

Reads, queries and ordinary writes use the verified pass without a live grant request. Fork, discard and merge are deliberate routes and ask Lys live. An unavailable live decision is `lys_could_not_be_asked`, even if the pass contains a related right. A held change is `held_for_approval`; its draft is made from the exact requested change. Nothing applies merely because the draft exists.

## Present a person's pass

A person opens Studio at the door's HTTPS origin. `/signin` sends the browser to Lys. `/callback` exchanges the code with a browser-bound state and PKCE verifier. The door keeps the verified pass and any refresh token server-side. The browser holds an opaque, secure session cookie.

Each forwarded store connection presents that person's pass as `session.pass`. A signed-in person without the grant receives `permission_refused`, with the resource, action and Lys request address. Signing in again does not create the missing grant. See [Studio and the door](/haematite/getting-started/studio-and-the-door/).

## Present an agent's pass

An MCP stdio client can read an existing pass file. It rereads the file before each tool call, so another process can renew it without restarting MCP.

**Terminal**

```bash
haematite mcp --config /etc/haematite/service.json \
  --pass-file /etc/haematite/agent-pass
```


An agent with a private key asks Lys's agent pass route with a signed request. The key may be PEM or a seat file containing `privateKeyPem`.

**Terminal**

```bash
haematite mcp --config /etc/haematite/service.json \
  --lys-agent https://lys.example.au ward-agent --key-file /etc/haematite/agent-key.pem
```


An agent with a grant credential sends it in `lys-grant-token` to the same route.

**Terminal**

```bash
haematite mcp --config /etc/haematite/service.json \
  --lys-agent https://lys.example.au ward-agent --credential-file /etc/haematite/agent-credential
```


A seat started by Lys can exchange its run pass from the named environment variable. The run pass is sent in `lys-agent-pass`; it is not used directly as a store pass.

**Terminal**

```bash
haematite mcp --config /etc/haematite/service.json \
  --lys-agent https://lys.example.au ward-agent --run-pass-env LYS_AGENT_PASS
```


The route is `/api/agents/ward-agent/pass`, with audience `haematite`. Key, credential and run-pass proofs are alternatives. With `--lys-agent`, the stdio process asks for a fresh pass before a call when none is held or expiry is within 60 seconds. Only that source gets one renewal and one more presentation after an expired, unpublished-key, signature or encoding refusal. A missing grant does not trigger renewal. `--pass-file` rereads the file on each call and never renews it; another process must replace an expired pass.

HTTP MCP takes the caller's pass in the `Authorization` bearer header on each request. `mcp-pass` can supply a header object directly to an MCP host's header helper. Its standard output contains the pass, so send it to the host and never copy it into a log.

## What the store checks

The verifier checks the signed issuer, audience, published Ed25519 key, signature, expiry, issue time, claim contract and audience of every right. Issue or not-before times more than five seconds ahead are refused. The requested resource and action must have an outright right for an ordinary route, or an allowed live answer for a deliberate route. Multi-resource changes must satisfy every resource check.

- `pass_missing`: No pass was presented. Supply a pass for haematite before calling a protected store.
- `wrong_audience`: The pass names another product. Ask Lys for audience haematite.
- `wrong_issuer`: The signed issuer differs from the store configuration. Use the configured Lys issuer.
- `pass_expired`: The expiry has passed. Renew the pass.
- `pass_not_yet_valid`: The issue or not-before time is beyond the permitted five-second clock skew. Correct the clocks and obtain a fresh pass.
- `unpublished_key`: The signing key id is absent from the published set. Refresh the issuer keys and obtain a current pass.
- `signature_refused`: The signature does not verify. Obtain a fresh pass from the trusted issuer.
- `pass_encoding_refused`: A token segment is not canonical base64url. Read the original pass without alteration.
- `contract_refused`: A pass, key or claim does not meet the verification contract. Obtain a correctly issued pass.
- `rights_outside_audience`: The pass carries rights for another application. Reissue it for haematite.
- `rights_truncated`: Lys reports omitted rights. Obtain an untruncated pass for this audience.


These eleven names are the HTTP listener's `SIGN_IN_AGAIN` list. A refusal during pass presentation becomes HTTP 401 `mcp_pass_refused` with the store refusal name and a sign-in challenge. `lys_keys_pending` or another store failure is HTTP 503 `mcp_store_unavailable`. `permission_refused` and `held_for_approval` concern the act; follow their stated grant or approval step.

## Confirm access

Use the caller's MCP connection to list tools, describe the store and run a query. Then attempt an ordinary write only with a writer's pass and a known expected version. A reader's denied write must remain denied.

The examples here were exercised against a local issuer stand-in with scratch keys. Real Lys enrolment, administrator approval and proxy sign-in are separate deployment checks. The schema and route classifications come from main `84b9497a`; no live grant was changed to write these pages.
