ablative docs
↑↓ move↵ openesc close
Stack Notebook ablative.com.au

Sign-in and permissions

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

Open .md

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.

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.

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.

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.

{
  "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.

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.

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.

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.

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.

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.