oficina / home Public

Design local operator owner receipts and atomic directory seed

Paul Campbell pushed by paul@keymail.dev 6dc1e7ea9fe4ffcd7fd1bde8c2d7117103864ec6 parent 171f49a
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.