| 1 | # Local Home operator seed and provisioning |
| 2 | |
| 3 | Status: design approved by configured Claude Fable (`claude-fable-5`, provider |
| 4 | event metadata verified), 2026-09-08, exact reviewed commit |
| 5 | 898fe219f4da5092b2cc28210b344ede6a478e50. Home discussion 37 retains the verdict |
| 6 | and earlier quota/permission refusals. This branch starts at directory runtime171. |
| 7 | It defines an application package and local command protocol; the companion |
| 8 | `home-provisioner.md` defines the authority adapters and staging proposal. No |
| 9 | implementation, platform117 or live provisioning approval is implied. |
| 10 | |
| 11 | ## Contracts and scope |
| 12 | |
| 13 | The governing sources are Home spec §7, §10, §14 and rulings 8, 22 and 23 in |
| 14 | `docs/superpowers/specs/2026-08-30-home-design.md`; approved directory design |
| 15 | fa64e286990762aecc6913bb9076a3cb75b088c3, `implementation/team-directory.md` |
| 16 | (Home discussion 24); approved roster contract 4e7b3ff, |
| 17 | `implementation/roster-contract.md` (discussion 28); and approved producer |
| 18 | 32fb49c1d5a4f0317b7291f441f78e21b7f0a11c, |
| 19 | `implementation/roster-producer-storage.md` and |
| 20 | `internal/home/directorydata/roster_migrations/migrations/0001_roster.sql` |
| 21 | (discussion 33; use the migration embedded by RosterSchema if its filename |
| 22 | changes). Shared roster 667dc8218dd5 supplies Provisioning, SignProvisioning and |
| 23 | VerifyProvisioning. HTTP runtime design 5fbfdf0 is a separate integration review, |
| 24 | not authority for this writer. Home discussion 35 records the source audit. |
| 25 | |
| 26 | The credential role alone resolves an address with its pepper. The directory |
| 27 | alone creates teams and memberships. An audited operator selects an approved |
| 28 | sealed request and its intended owner; a browser assertion, a visitor, an |
| 29 | instance bearer and the platform account owner alone supply no team-owner |
| 30 | identity proof. The operator transport authenticates the operator's authority to |
| 31 | run an approved action on this exact deployment. The credential receipt below |
| 32 | binds the intended owner across roles. This preserves the operator seed exception |
| 33 | in §14; it does not assert that provisioning is a browser fresh-owner ceremony. |
| 34 | |
| 35 | This slice supports generation initialization when no generation or consumer |
| 36 | exists, a new team with its first owner, initial consumer provisioning and |
| 37 | activation. It provides no roster editing, token rotation, restore, owner |
| 38 | confirmation, key rotation, sibling owner seeding or ownership transfer command. |
| 39 | Those require their own reviewed operation and fresh-owner rules. In particular, |
| 40 | missing generation alongside existing consumers is a refusal, never a repair. |
| 41 | |
| 42 | ## Transport and command boundary |
| 43 | |
| 44 | Platform discussion 117 owns stopping traffic and all background work, holding |
| 45 | the database lease, selecting the immutable executable and exact database, and |
| 46 | resuming safely. Its private-input/result protocol is generic. The proposed |
| 47 | entry point is `home operator-v1 --db <platform-selected-existing-db>`, with |
| 48 | standard output captured as the private result by the transport. The fixed command and |
| 49 | role permissions belong to reviewed deployment configuration. No request chooses |
| 50 | a binary, path, SQL, environment variable, shell fragment or network destination. |
| 51 | The command never starts an HTTP server, sends mail or makes network calls. |
| 52 | |
| 53 | Read one UTF-8 JSON object from private stdin with a 64 KiB hard limit including |
| 54 | whitespace, then require EOF. Write one JSON result of at most 64 KiB to the |
| 55 | private standard-output channel only, after transaction commit. The transport |
| 56 | never tees or logs stdout and discards stderr. Input and result |
| 57 | are encrypted at rest by the transport, available only through its authenticated |
| 58 | private result channel, and never included in public status, receipts or logs. |
| 59 | The application does not implement its own remote encryption protocol. |
| 60 | |
| 61 | Within the signed private wrapper defined in home-provisioner.md, operation-input |
| 62 | common fields are `version:1`, `operation`, and `operation_id` (random |
| 63 | canonical 32-byte base64url). Operation-specific closed objects are specified |
| 64 | below. Reject unknown or duplicate fields, trailing JSON, alternate numeric |
| 65 | forms, invalid UTF-8, malformed canonical base64 and oversized decoded values |
| 66 | before mutation. Strings are bounded before allocating their decoded contents. |
| 67 | No error includes supplied values. Exit 0 means a complete committed result is |
| 68 | available; exit 2 is fixed refusal and exit 3 unavailable/ambiguous. Neither exit |
| 69 | code permits automatic mutation retry. The platform queries the original result |
| 70 | or explicitly reruns the exact input under a fresh fence; Home idempotency then |
| 71 | returns the committed result without performing the action again. |
| 72 | |
| 73 | The invocation must be authorized outside the request. The local adapter accepts |
| 74 | an immutable operation allowlist and expected role/origin/database identity from |
| 75 | reviewed launch configuration. A request's `operation` only selects within that |
| 76 | allowlist. Package constructors are private to the command adapter or receive an |
| 77 | opaque validated operator context; exported structs cannot fabricate that context. |
| 78 | The transport's operation ID is distinct from Home operation_id. Both must be |
| 79 | bound by the adapter's verified invocation context. Current platform source has |
| 80 | no signed Home invocation receipt, and this design does not assert otherwise. No body field can claim an |
| 81 | operator identity or maintenance state. The platform must provide an authenticated |
| 82 | invocation context or equivalent private launch configuration before command |
| 83 | wiring is approved; an environment boolean supplied by the caller is insufficient. |
| 84 | |
| 85 | ## Keys and durable private results |
| 86 | |
| 87 | Use the existing Rastrillo family key encoding and crypto.Sign/Verify operations, |
| 88 | with strict canonical key decoding and derived key IDs. Reuse shared seal for |
| 89 | stored private envelopes. Do not copy signing arithmetic or browser assertion |
| 90 | validation to create operator authority. |
| 91 | |
| 92 | Credential operator signing uses a separately provisioned private key and context |
| 93 | `oficina/home/owner-seed/v1`; the directory pins its public key outside every |
| 94 | receipt. Directory provisioning signing uses its separately provisioned operator |
| 95 | key and shared context `oficina/roster/provisioning/v1`. Roster response keys are |
| 96 | also distinct. Keys arrive through the existing protected per-instance config |
| 97 | mechanism; runtime browser assertion keys and existing data keys remain unchanged. |
| 98 | Ordinary startup neither creates nor rotates any of these keys. Reject identical |
| 99 | public keys across the roles within the command's configured material. Public |
| 100 | key pin replacement is an explicit future operator migration, not an input field. |
| 101 | |
| 102 | Add role-specific operator result tables through additive migrations. Credential |
| 103 | and directory databases never receive each other's tables. Each result row has |
| 104 | operation_id primary key, fixed action, opaque operator reference from verified |
| 105 | invocation context, creation time, a keyed request digest, and a sealed result. |
| 106 | The request digest is HMAC-SHA256 using an independently provisioned 32-byte |
| 107 | idempotency key. The actual shared sealer exposes Seal/Open, not a key derivation |
| 108 | API. No custom KDF is introduced. Digest canonical typed operation input, not raw JSON whitespace. |
| 109 | This prevents a stored bare address/name digest becoming an offline dictionary. |
| 110 | |
| 111 | Seal result bytes with the existing role sealer and purpose |
| 112 | `operator.result/v1/<exact-origin>/<operation-id>/<action>`. A result can contain |
| 113 | an authority-bearing receipt or bearer and therefore never appears plaintext in |
| 114 | SQLite/WAL, logs or public receipts. Never persist the original address/input. |
| 115 | Repeated exact input under the same operation ID returns the stored result; |
| 116 | changed semantic input or different action refuses. A lost post-commit response |
| 117 | therefore does not mint a new bearer, person or receipt. Keep successful result |
| 118 | rows until an explicit reviewed retention policy exists; no rolling cleanup may |
| 119 | make an old operation reusable. This initial low-volume administrative table has |
| 120 | a hard 10,000-operation limit, refusing further new operations at capacity. |
| 121 | |
| 122 | The existing roster_operator_receipts table retains its approved public metadata |
| 123 | shape and SHA256 digest of the signed provisioning record; no bearer, display |
| 124 | name, input digest containing address material or private result belongs there. |
| 125 | The new directory result row and that receipt commit in the same transaction. |
| 126 | Private results are bounded to 48 KiB before sealing, leaving transport space. |
| 127 | |
| 128 | ## Operations |
| 129 | |
| 130 | All origins come from validated configuration or must equal it exactly. Production |
| 131 | team destinations reuse the compiled registry and canonical label policy from |
| 132 | `directoryFeatures` and `directorydata.Config.ValidateRoute`; for Docs the origin |
| 133 | is `https://<label>.docs.oficina.works`. Reject alternate ports, paths, aliases, |
| 134 | userinfo, fragments, queries and unapproved app slugs. This design adds no pilot |
| 135 | manifest or per-team config exception. |
| 136 | |
| 137 | `directory.inspect` accepts no payload and returns only current generation (or |
| 138 | explicit absent), the next proposed team integer, and derived team sqid. It writes |
| 139 | no authority and is not a reservation. Derive next ID from the maximum existing |
| 140 | team ID and sqlite_sequence, then add one, refusing signed-64-bit overflow. No |
| 141 | shared Home/suite codec exists today. Pin official github.com/sqids/sqids-go |
| 142 | v0.4.1 (upstream commit 2ba95c7920dcf08340e099c7b22beb1178da6106), using |
| 143 | sqids.New() and that version's fixed default alphabet, blocklist and zero minimum |
| 144 | length. Encode exactly one positive integer. Require Decode to return exactly |
| 145 | one integer in 1..MaxInt64 and Encode(decoded) to equal the original string. |
| 146 | This preserves the integer-encoding sqid required by §10; Docs' random document |
| 147 | IDs are unrelated. Golden vectors are 1=Uk, 2=gb, 3=Ef, 1000=pnd and |
| 148 | MaxInt64=AqkYhRmbHpEX. Refuse zero, negative/multiple numbers, alternate encodings |
| 149 | and reserved DNS labels independently. Dependency/options changes require explicit |
| 150 | compatibility review and must preserve existing team sqids. The upstream |
| 151 | [canonical encoding guidance](https://github.com/sqids/sqids-go/tree/v0.4.1) |
| 152 | requires re-encoding; a disposable local pinned-library probe verified these vectors. |
| 153 | The seed checks this proposal again under the write lock. A concurrent seed makes |
| 154 | the old proposal fail; the operator obtains a new proposal and new owner receipt. |
| 155 | |
| 156 | `credential.owner-seed` accepts `directory`, `team_id`, `team_sqid`, and a private |
| 157 | `address` bounded to 320 UTF-8 bytes from the operator-approved decrypted queue |
| 158 | request. The address is transported privately into credential Home; queue |
| 159 | unsealing and request approval remain the external drain's duty. The command |
| 160 | normalizes and blind-indexes with the same credential helper as sign-in, resolves |
| 161 | or creates the normal opaque person stub, and never mints a session. Synthetic |
| 162 | live previews require already authenticated synthetic identities and an |
| 163 | `existing_only:true` mode; that mode refuses a missing person and sends no mail. |
| 164 | The directory cannot request address resolution over HTTP. Refactor the existing |
| 165 | findOrMintPerson SQL into a transaction-capable internal helper: its current |
| 166 | a.db.Writer() call cannot participate in the required operator transaction. |
| 167 | Keep sign-in semantics, blindIndex and day-coarsened created_at unchanged. |
| 168 | |
| 169 | The result is `{version:1, operation_id, outcome:"committed", receipt}`. receipt |
| 170 | uses the shared closed `{v,kid,payload,signature}` envelope pattern with canonical |
| 171 | base64url bytes and raw 64-byte signature, at most 8 KiB total. Its signed payload |
| 172 | contains version, purpose="owner-seed", operation_id, credential issuer, intended |
| 173 | directory, proposed integer team ID and sqid, resolved person_ref, role="owner", |
| 174 | issued_at and expires_at. Duration is at most 300 seconds. The credential operation |
| 175 | and result are atomic with person-stub creation. Retry returns identical signed |
| 176 | bytes, even after expiry; it never renews authority. An expired receipt requires |
| 177 | a newly approved operation ID. The payload contains no address or team name. |
| 178 | |
| 179 | `directory.initialize` accepts `expected_generation:null`. Inside its transaction |
| 180 | require no generation, no roster consumers, no roster tokens and no previous |
| 181 | bootstrap receipt; existing teams/memberships may be retained. Accept the exact random32-byte canonical base64url generation from the verified |
| 182 | external baseline record, persist it once and a bootstrap receipt |
| 183 | plus sealed result atomically. Require explicit deployment precondition that no |
| 184 | consumer has ever been provisioned; a database which has lost generation/receipt |
| 185 | history cannot satisfy this precondition based on its own empty tables. An independently |
| 186 | signed external installation record and its monotonic registry floor must attest |
| 187 | that this exact directory database has never had a roster generation provisioned. |
| 188 | The record issuer, signature verification and durable external registry are not |
| 189 | implemented by current Home or platform. The proposed companion home-provisioner.md |
| 190 | defines them and the explicit trusted fresh-owner baseline attestation; that |
| 191 | amendment requires approval before constructing initial-install authority. Platform |
| 192 | maintenance provides exclusion, not this Home-specific historical evidence. If |
| 193 | that evidence is unavailable, initialization is unavailable. Restore cannot use |
| 194 | this verb. Ordinary boot only applies empty schema and never invokes it. |
| 195 | |
| 196 | `directory.seed-team` accepts `expected_generation`, proposed `team_id` and |
| 197 | `team_sqid`, canonical `label`, `display_name` (1..255 UTF-8 bytes), and the owner |
| 198 | `receipt` (at most 8 KiB). Check receipt signature, configured issuer/directory, |
| 199 | exact team proposal, fixed owner role, canonical person ref, valid issue time and |
| 200 | five-minute expiry. Verify time again under the write lock. Persist the consumed |
| 201 | credential receipt operation ID under a unique constraint in this same transaction. |
| 202 | Expired or malformed input makes no change. A committed exact replay returns its |
| 203 | stored result despite receipt expiry; changed input under either consumed owner |
| 204 | receipt or directory operation refuses. Distinct directory operation IDs cannot |
| 205 | reuse the same owner receipt. |
| 206 | |
| 207 | Require generation equality, absent team/label and unchanged next ID. Allocate |
| 208 | using AUTOINCREMENT, verify the returned integer and corresponding sqid, seal name |
| 209 | under the existing ID-bound helper, insert exactly the receipt's person as first |
| 210 | owner, and persist result plus consumed-receipt metadata atomically. No unsealed |
| 211 | placeholder is written; an encrypted placeholder of the existing test/store shape |
| 212 | may only exist within the transaction before the final seal. Membership triggers |
| 213 | advance epoch; do not add a second increment. Return opaque team/sqid, generation |
| 214 | and resulting epoch. No team_instances row is live at this point. |
| 215 | |
| 216 | `directory.provision-consumer` accepts expected generation, team sqid, app slug, |
| 217 | consumer ID (1..64 ASCII letters/digits/_/-), exact expected audience and expected |
| 218 | sequence 0 for this initial-only operation. Require an existing team with an owner, |
| 219 | no existing binding for team/app, no consumer ID/origin collision and exact compiled |
| 220 | route. Insert pending team_instance, enabled consumer, current token hash and |
| 221 | successful provisioning receipt in one transaction. Generate a random 32-byte |
| 222 | canonical bearer; store only SHA256 of its exact 43-character string in token rows. |
| 223 | The signed shared Provisioning has sequence 1, all configured bindings, current |
| 224 | generation, actual epoch and configured roster public keys (1..8); owner_confirmation |
| 225 | is empty for this initial generation. Verify the produced record against the |
| 226 | independently pinned directory operator public key before commit. Seal the private |
| 227 | result containing `{record, bearer}` for exact retries. Pending bindings do not |
| 228 | serve roster pages or appear in the chooser. No content key is copied or generated |
| 229 | by Home. The external provisioner creates the isolated Docs DB and its own keys. |
| 230 | |
| 231 | `directory.activate-consumer` accepts expected generation, consumer ID, expected |
| 232 | record sequence/digest, and an opaque deployment verification operation reference. |
| 233 | The verified operator context must attest successful exact-host/body/header build |
| 234 | verification, assertion registration, isolated instance database/config, and the |
| 235 | reviewed TeamMode acceptance gates. A string supplied by the request is not that |
| 236 | attestation. Requery pending binding, consumer enabled state, token, generation and |
| 237 | record metadata, then switch only that existing pending binding to live and commit |
| 238 | its audit/result atomically. This package verifies an opaque approved activation |
| 239 | context; it does not fetch a URL or trust arbitrary deployment evidence JSON. |
| 240 | Current platform receipts do not attest all these Home-specific facts. The |
| 241 | external provisioner/verification adapter and its pinned evidence verifier require |
| 242 | the companion home-provisioner.md authority design and approval; no constructor may accept caller-controlled |
| 243 | booleans as a substitute. Until that adapter exists, activation remains unwired. A compatibility account-link preview is not sufficient for activation. |
| 244 | |
| 245 | ## Transaction and existing-database checks |
| 246 | |
| 247 | Open an existing regular database with creation disabled, through the pinned |
| 248 | platform path. Check application identity, expected role/origin, migration ledger |
| 249 | and required additive schema versions before any action. Never call the general |
| 250 | web startup migrator to repair an unexpected DB from this command. The deployment |
| 251 | applies reviewed role-specific additive migrations before operator execution. |
| 252 | |
| 253 | Acquire one dedicated database/sql connection. Set foreign_keys ON before beginning |
| 254 | and read it back as 1 on that exact connection; pooled connections cannot substitute |
| 255 | for this check. Also require secure_delete ON, expected journal configuration, |
| 256 | no foreign_key_check violations, and the verified transport fence/lease. Take an |
| 257 | immediate write transaction with a bounded lock timeout; all idempotency, generation, |
| 258 | sequence, owner receipt and registry preconditions are rechecked inside it. Do not |
| 259 | perform network work while holding the lock. Refuse wrong clocks, time rollback |
| 260 | relative to persisted operator completion time, exhausted counters or corrupt rows. |
| 261 | Write all related authority and audit rows together. Close/checkpoint through the |
| 262 | supported transport protocol before resume; an uncertain commit remains fenced |
| 263 | until querying the same operation establishes its outcome. |
| 264 | |
| 265 | No success path changes existing sessions, assertion keys, content encryption, |
| 266 | existing team/member rows or unrelated consumers. Rollback leaves no orphaned team, |
| 267 | owner, token, receipt or result. Read-only inspect has no minting side effect. |
| 268 | |
| 269 | ## Review and acceptance |
| 270 | |
| 271 | Implement narrow package tests before a command adapter. Exercise actual additive |
| 272 | schemas and sealer, signed owner receipts, actual shared provisioning verification, |
| 273 | and real credential normalization/blind indexing. Require meaningful failures for |
| 274 | FK off on the actual connection, wrong DB/role/origin, missing fence context, forged |
| 275 | operator context, unknown key/purpose, browser assertion substitution, stale and |
| 276 | future receipts, receipt reuse, raced integer proposal, changed-payload retry, |
| 277 | commit/output loss, name-seal failure, generation mismatch, false initial install, |
| 278 | consumer collision, roster epoch overflow and transaction rollback. |
| 279 | |
| 280 | Extend gate 2's expensive privacy half when this seed verb lands, as required |
| 281 | by the Home spec. Scan SQLite, WAL, captured stdout/stderr and public receipt serialization for known |
| 282 | synthetic addresses, names, bearer strings, receipt payloads and private results. |
| 283 | Prove current Home session acceptance/signout and all existing team ciphertexts |
| 284 | survive applying the additive tables. Mutation tests must fail when signature, |
| 285 | consumed-receipt uniqueness, FK verification or idempotency checks are removed. |
| 286 | Demonstrate the private-stdout command with a disposable local fence fixture only; that |
| 287 | fixture is not evidence of platform maintenance correctness. Full repo gates, exact |
| 288 | CI and Fable whole-code review precede integration. Live use additionally requires |
| 289 | approved/shipped platform 117, operator authority and invocation/activation context |
| 290 | adapters, actual two-team TeamMode acceptance and canary verification. |
| 291 | |