Design local operator owner receipts and atomic directory seed
1 file changed,
+265
−0
implementation/home-operator-seed.md+265 −0
CI passed — run details
diff --git a/implementation/home-operator-seed.md b/implementation/home-operator-seed.md| new file mode 100644 |
| index 0000000..2e71e85 |
| --- /dev/null |
| +++ b/implementation/home-operator-seed.md |
| @@ -0,0 +1,265 @@ |
| +# Local Home operator seed and provisioning |
| + |
| +Status: proposed internal design, 2026-09-08. Fable approval is required before |
| +implementation. This branch starts at directory runtime 171f49a. It defines an |
| +application package and a local command protocol; it neither mounts a public |
| +operator endpoint nor authorizes live provisioning. |
| + |
| +## Contracts and scope |
| + |
| +The governing sources are Home spec §7, §10, §14 and rulings 8, 22 and 23 in |
| +`docs/superpowers/specs/2026-08-30-home-design.md`; approved directory design |
| +fa64e286990762aecc6913bb9076a3cb75b088c3, `implementation/team-directory.md` |
| +(Home discussion 24); approved roster contract 4e7b3ff, |
| +`implementation/roster-contract.md` (discussion 28); and approved producer |
| +32fb49c1d5a4f0317b7291f441f78e21b7f0a11c, |
| +`implementation/roster-producer-storage.md` and |
| +`internal/home/directorydata/roster_migrations/migrations/0001_roster.sql` |
| +(discussion 33; use the migration embedded by RosterSchema if its filename |
| +changes). Shared roster 667dc8218dd5 supplies Provisioning, SignProvisioning and |
| +VerifyProvisioning. HTTP runtime design 5fbfdf0 is a separate integration review, |
| +not authority for this writer. Home discussion 35 records the source audit. |
| + |
| +The credential role alone resolves an address with its pepper. The directory |
| +alone creates teams and memberships. An audited operator selects an approved |
| +sealed request and its intended owner; a browser assertion, a visitor, an |
| +instance bearer and the platform account owner alone supply no team-owner |
| +identity proof. The operator transport authenticates the operator's authority to |
| +run an approved action on this exact deployment. The credential receipt below |
| +binds the intended owner across roles. This preserves the operator seed exception |
| +in §14; it does not assert that provisioning is a browser fresh-owner ceremony. |
| + |
| +This slice supports generation initialization when no generation or consumer |
| +exists, a new team with its first owner, initial consumer provisioning and |
| +activation. It provides no roster editing, token rotation, restore, owner |
| +confirmation, key rotation, sibling owner seeding or ownership transfer command. |
| +Those require their own reviewed operation and fresh-owner rules. In particular, |
| +missing generation alongside existing consumers is a refusal, never a repair. |
| + |
| +## Transport and command boundary |
| + |
| +Platform discussion 117 owns stopping traffic and all background work, holding |
| +the database lease, selecting the immutable executable and exact database, and |
| +resuming safely. Its private-input/result protocol is generic. The proposed |
| +entry point is `home operator-v1 --db <platform-selected-existing-db>` with a |
| +private result file descriptor supplied by the transport. The fixed command and |
| +role permissions belong to reviewed deployment configuration. No request chooses |
| +a binary, path, SQL, environment variable, shell fragment or network destination. |
| +The command never starts an HTTP server, sends mail or makes network calls. |
| + |
| +Read one UTF-8 JSON object from private stdin with a 64 KiB hard limit including |
| +whitespace, then require EOF. Write one JSON result of at most 64 KiB to the |
| +private result descriptor only, after transaction commit. Standard output and |
| +standard error carry no data and the transport discards them. Input and result |
| +are encrypted at rest by the transport, available only through its authenticated |
| +private result channel, and never included in public status, receipts or logs. |
| +The application does not implement its own remote encryption protocol. |
| + |
| +Request common fields are `version:1`, `operation`, and `operation_id` (random |
| +canonical 32-byte base64url). Operation-specific closed objects are specified |
| +below. Reject unknown or duplicate fields, trailing JSON, alternate numeric |
| +forms, invalid UTF-8, malformed canonical base64 and oversized decoded values |
| +before mutation. Strings are bounded before allocating their decoded contents. |
| +No error includes supplied values. Exit 0 means a complete committed result is |
| +available; exit 2 is fixed refusal and exit 3 unavailable/ambiguous. Neither exit |
| +code permits automatic mutation retry. The platform queries the original result |
| +or explicitly reruns the exact input under a fresh fence; Home idempotency then |
| +returns the committed result without performing the action again. |
| + |
| +The invocation must be authorized outside the request. The local adapter accepts |
| +an immutable operation allowlist and expected role/origin/database identity from |
| +reviewed launch configuration. A request's `operation` only selects within that |
| +allowlist. Package constructors are private to the command adapter or receive an |
| +opaque validated operator context; exported structs cannot fabricate that context. |
| +The transport's signed operation ID is distinct from Home operation_id. Both are |
| +bound by the adapter's verified invocation context. No body field can claim an |
| +operator identity or maintenance state. The platform must provide an authenticated |
| +invocation context or equivalent private launch configuration before command |
| +wiring is approved; an environment boolean supplied by the caller is insufficient. |
| + |
| +## Keys and durable private results |
| + |
| +Use the existing Rastrillo family key encoding and crypto.Sign/Verify operations, |
| +with strict canonical key decoding and derived key IDs. Reuse shared seal for |
| +stored private envelopes. Do not copy signing arithmetic or browser assertion |
| +validation to create operator authority. |
| + |
| +Credential operator signing uses a separately provisioned private key and context |
| +`oficina/home/owner-seed/v1`; the directory pins its public key outside every |
| +receipt. Directory provisioning signing uses its separately provisioned operator |
| +key and shared context `oficina/roster/provisioning/v1`. Roster response keys are |
| +also distinct. Keys arrive through the existing protected per-instance config |
| +mechanism; runtime browser assertion keys and existing data keys remain unchanged. |
| +Ordinary startup neither creates nor rotates any of these keys. Reject identical |
| +public keys across the roles within the command's configured material. Public |
| +key pin replacement is an explicit future operator migration, not an input field. |
| + |
| +Add role-specific operator result tables through additive migrations. Credential |
| +and directory databases never receive each other's tables. Each result row has |
| +operation_id primary key, fixed action, opaque operator reference from verified |
| +invocation context, creation time, a keyed request digest, and a sealed result. |
| +The request digest is HMAC-SHA256 using a dedicated key derived through the shared |
| +sealer's supported key separation or, if no derivation API exists, an independently |
| +provisioned 32-byte idempotency key. No custom KDF is introduced. The chosen |
| +implementation must use the independent key when the shared API offers only |
| +Seal/Open. Digest canonical typed operation input, not raw JSON whitespace. |
| +This prevents a stored bare address/name digest becoming an offline dictionary. |
| + |
| +Seal result bytes with the existing role sealer and purpose |
| +`operator.result/v1/<exact-origin>/<operation-id>/<action>`. A result can contain |
| +an authority-bearing receipt or bearer and therefore never appears plaintext in |
| +SQLite/WAL, logs or public receipts. Never persist the original address/input. |
| +Repeated exact input under the same operation ID returns the stored result; |
| +changed semantic input or different action refuses. A lost post-commit response |
| +therefore does not mint a new bearer, person or receipt. Keep successful result |
| +rows until an explicit reviewed retention policy exists; no rolling cleanup may |
| +make an old operation reusable. This initial low-volume administrative table has |
| +a hard 10,000-operation limit, refusing further new operations at capacity. |
| + |
| +The existing roster_operator_receipts table retains its approved public metadata |
| +shape and SHA256 digest of the signed provisioning record; no bearer, display |
| +name, input digest containing address material or private result belongs there. |
| +The new directory result row and that receipt commit in the same transaction. |
| +Private results are bounded to 48 KiB before sealing, leaving transport space. |
| + |
| +## Operations |
| + |
| +All origins come from validated configuration or must equal it exactly. Production |
| +team destinations reuse the compiled registry and canonical label policy from |
| +`directoryFeatures` and `directorydata.Config.ValidateRoute`; for Docs the origin |
| +is `https://<label>.docs.oficina.works`. Reject alternate ports, paths, aliases, |
| +userinfo, fragments, queries and unapproved app slugs. This design adds no pilot |
| +manifest or per-team config exception. |
| + |
| +`directory.inspect` accepts no payload and returns only current generation (or |
| +explicit absent), the next proposed team integer, and derived team sqid. It writes |
| +no authority and is not a reservation. Derive next ID from the maximum existing |
| +team ID and sqlite_sequence, reject overflow, and encode with the suite's pinned |
| +sqid codec. Before implementation select and cite that actual shared codec; |
| +there is no random substitute for the integer-encoding sqid required by §10. |
| +The seed checks this proposal again under the write lock. A concurrent seed makes |
| +the old proposal fail; the operator obtains a new proposal and new owner receipt. |
| + |
| +`credential.owner-seed` accepts `directory`, `team_id`, `team_sqid`, and a private |
| +`address` bounded to 320 UTF-8 bytes from the operator-approved decrypted queue |
| +request. The address is transported privately into credential Home; queue |
| +unsealing and request approval remain the external drain's duty. The command |
| +normalizes and blind-indexes with the same credential helper as sign-in, resolves |
| +or creates the normal opaque person stub, and never mints a session. Synthetic |
| +live previews require already authenticated synthetic identities and an |
| +`existing_only:true` mode; that mode refuses a missing person and sends no mail. |
| +The directory cannot request address resolution over HTTP. |
| + |
| +The result is `{version:1, operation_id, outcome:"committed", receipt}`. receipt |
| +uses the shared closed `{v,kid,payload,signature}` envelope pattern with canonical |
| +base64url bytes and raw 64-byte signature, at most 8 KiB total. Its signed payload |
| +contains version, purpose="owner-seed", operation_id, credential issuer, intended |
| +directory, proposed integer team ID and sqid, resolved person_ref, role="owner", |
| +issued_at and expires_at. Duration is at most 300 seconds. The credential operation |
| +and result are atomic with person-stub creation. Retry returns identical signed |
| +bytes, even after expiry; it never renews authority. An expired receipt requires |
| +a newly approved operation ID. The payload contains no address or team name. |
| + |
| +`directory.initialize` accepts `expected_generation:null`. Inside its transaction |
| +require no generation, no roster consumers, no roster tokens and no previous |
| +bootstrap receipt; existing teams/memberships may be retained. Generate a random |
| +32-byte canonical base64url generation once, persist it and a bootstrap receipt |
| +plus sealed result atomically. Require explicit deployment precondition that no |
| +consumer has ever been provisioned; a database which has lost generation/receipt |
| +history cannot satisfy this precondition based on its own empty tables. Supported |
| +transport must bind the independently verified initial-install attestation. If |
| +that evidence is unavailable, initialization is unavailable. Restore cannot use |
| +this verb. Ordinary boot only applies empty schema and never invokes it. |
| + |
| +`directory.seed-team` accepts `expected_generation`, proposed `team_id` and |
| +`team_sqid`, canonical `label`, `display_name` (1..255 UTF-8 bytes), and the owner |
| +`receipt` (at most 8 KiB). Check receipt signature, configured issuer/directory, |
| +exact team proposal, fixed owner role, canonical person ref, valid issue time and |
| +five-minute expiry. Verify time again under the write lock. Persist the consumed |
| +credential receipt operation ID under a unique constraint in this same transaction. |
| +Expired or malformed input makes no change. A committed exact replay returns its |
| +stored result despite receipt expiry; changed input under either consumed owner |
| +receipt or directory operation refuses. Distinct directory operation IDs cannot |
| +reuse the same owner receipt. |
| + |
| +Require generation equality, absent team/label and unchanged next ID. Allocate |
| +using AUTOINCREMENT, verify the returned integer and corresponding sqid, seal name |
| +under the existing ID-bound helper, insert exactly the receipt's person as first |
| +owner, and persist result plus consumed-receipt metadata atomically. No unsealed |
| +placeholder is written; an encrypted placeholder of the existing test/store shape |
| +may only exist within the transaction before the final seal. Membership triggers |
| +advance epoch; do not add a second increment. Return opaque team/sqid, generation |
| +and resulting epoch. No team_instances row is live at this point. |
| + |
| +`directory.provision-consumer` accepts expected generation, team sqid, app slug, |
| +consumer ID (1..64 ASCII letters/digits/_/-), exact expected audience and expected |
| +sequence 0 for this initial-only operation. Require an existing team with an owner, |
| +no existing binding for team/app, no consumer ID/origin collision and exact compiled |
| +route. Insert pending team_instance, enabled consumer, current token hash and |
| +successful provisioning receipt in one transaction. Generate a random 32-byte |
| +canonical bearer; store only SHA256 of its exact 43-character string in token rows. |
| +The signed shared Provisioning has sequence 1, all configured bindings, current |
| +generation, actual epoch and configured roster public keys (1..8); owner_confirmation |
| +is empty for this initial generation. Verify the produced record against the |
| +independently pinned directory operator public key before commit. Seal the private |
| +result containing `{record, bearer}` for exact retries. Pending bindings do not |
| +serve roster pages or appear in the chooser. No content key is copied or generated |
| +by Home. The external provisioner creates the isolated Docs DB and its own keys. |
| + |
| +`directory.activate-consumer` accepts expected generation, consumer ID, expected |
| +record sequence/digest, and an opaque deployment verification operation reference. |
| +The verified operator context must attest successful exact-host/body/header build |
| +verification, assertion registration, isolated instance database/config, and the |
| +reviewed TeamMode acceptance gates. A string supplied by the request is not that |
| +attestation. Requery pending binding, consumer enabled state, token, generation and |
| +record metadata, then switch only that existing pending binding to live and commit |
| +its audit/result atomically. This package verifies an opaque approved activation |
| +context; it does not fetch a URL or trust arbitrary deployment evidence JSON. |
| +Until the transport/operator adapter can construct that context, activation remains |
| +unwired. A compatibility account-link preview is not sufficient for activation. |
| + |
| +## Transaction and existing-database checks |
| + |
| +Open an existing regular database with creation disabled, through the pinned |
| +platform path. Check application identity, expected role/origin, migration ledger |
| +and required additive schema versions before any action. Never call the general |
| +web startup migrator to repair an unexpected DB from this command. The deployment |
| +applies reviewed role-specific additive migrations before operator execution. |
| + |
| +Acquire one dedicated database/sql connection. Set foreign_keys ON before beginning |
| +and read it back as 1 on that exact connection; pooled connections cannot substitute |
| +for this check. Also require secure_delete ON, expected journal configuration, |
| +no foreign_key_check violations, and the verified transport fence/lease. Take an |
| +immediate write transaction with a bounded lock timeout; all idempotency, generation, |
| +sequence, owner receipt and registry preconditions are rechecked inside it. Do not |
| +perform network work while holding the lock. Refuse wrong clocks, time rollback |
| +relative to persisted operator completion time, exhausted counters or corrupt rows. |
| +Write all related authority and audit rows together. Close/checkpoint through the |
| +supported transport protocol before resume; an uncertain commit remains fenced |
| +until querying the same operation establishes its outcome. |
| + |
| +No success path changes existing sessions, assertion keys, content encryption, |
| +existing team/member rows or unrelated consumers. Rollback leaves no orphaned team, |
| +owner, token, receipt or result. Read-only inspect has no minting side effect. |
| + |
| +## Review and acceptance |
| + |
| +Implement narrow package tests before a command adapter. Exercise actual additive |
| +schemas and sealer, signed owner receipts, actual shared provisioning verification, |
| +and real credential normalization/blind indexing. Require meaningful failures for |
| +FK off on the actual connection, wrong DB/role/origin, missing fence context, forged |
| +operator context, unknown key/purpose, browser assertion substitution, stale and |
| +future receipts, receipt reuse, raced integer proposal, changed-payload retry, |
| +commit/output loss, name-seal failure, generation mismatch, false initial install, |
| +consumer collision, roster epoch overflow and transaction rollback. |
| + |
| +Scan SQLite, WAL, captured stdout/stderr and public receipt serialization for known |
| +synthetic addresses, names, bearer strings, receipt payloads and private results. |
| +Prove current Home session acceptance/signout and all existing team ciphertexts |
| +survive applying the additive tables. Mutation tests must fail when signature, |
| +consumed-receipt uniqueness, FK verification or idempotency checks are removed. |
| +Demonstrate the private-FD command with a disposable local fence fixture only; that |
| +fixture is not evidence of platform maintenance correctness. Full repo gates, exact |
| +CI and Fable whole-code review precede integration. Live use additionally requires |
| +approved/shipped platform 117, operator authority and invocation/activation context |
| +adapters, actual two-team TeamMode acceptance and canary verification. |