| 1 | # Operator registry and first team provisioning |
| 2 | |
| 3 | Status: design approved by configured Claude Fable (`claude-fable-5`, verified |
| 4 | provider metadata), 2026-09-08, reviewed commit898fe219f4da5092b2cc28210b344ede6a478e50, |
| 5 | Home discussion37. Implementation and live-use gates below remain. This makes |
| 6 | `home-operator-seed.md`'s invocation, baseline and activation adapters concrete. |
| 7 | It adds no live records, keys, configuration or command implementation. |
| 8 | |
| 9 | ## Operator and authority |
| 10 | |
| 11 | Implement a separate local Go command, `oficina provision`, callable directly by |
| 12 | a human. It uses supported CARLOS APIs/CLI and the fixed Home operator command. |
| 13 | No LLM, remote shell, SQL console or manual database edit is required. Home keeps |
| 14 | no platform provisioning credential. The command never serves a website. |
| 15 | |
| 16 | The operator authenticates to the existing CARLOS console. Every maintenance |
| 17 | mutation still requires its actual browser session, CSRF, current account-owner |
| 18 | lookup and `requireFreshAuth`, in that order. The reviewed maintenance request |
| 19 | binds its immutable encrypted input and selected artifact/config/instance. The |
| 20 | local signer cannot bypass that gate. A bearer has no invented auth_time. Source: |
| 21 | platform dcf0cad `internal/console/stepup.go`, and platform117 revised maintenance |
| 22 | design (currently 0c59ea2; wire API remains under review). |
| 23 | |
| 24 | A separately generated provisioner signing key attests the operator's selected |
| 25 | installation baseline and collected evidence. Both Home roles pin this public key |
| 26 | outside the signed request. It is distinct from credential owner-seed signing, |
| 27 | directory provisioning signing and roster response keys. Compromise of this key |
| 28 | is operator compromise; its public signature alone cannot execute anything without |
| 29 | the maintenance fence and fresh console approval. The credential owner receipt |
| 30 | still resolves the requested owner's identity. The operator cannot submit a |
| 31 | person_ref in place of that receipt. |
| 32 | |
| 33 | The initial baseline is an explicit trusted owner attestation: this installation |
| 34 | has never issued a roster generation or consumer record. Deployment history, |
| 35 | current configured versions, and fenced schema inspection inform that decision. |
| 36 | They cannot mathematically prove historical absence after a possible restore. |
| 37 | The review screen states this distinction and shows the exact installation and |
| 38 | baseline digest. If the owner cannot attest it, enrollment refuses and recovery |
| 39 | requires its separately reviewed procedure. This is the only initial-install |
| 40 | exception; retries and ordinary startup cannot repeat it. |
| 41 | |
| 42 | ## Registry custody and persistence |
| 43 | |
| 44 | The first supported workflow runs natively on this Linux operator workspace. |
| 45 | `oficina provision init --state <directory> --floor-dir <independent-directory>` |
| 46 | requires empty regular directories on persistent local ext4/XFS filesystems, |
| 47 | mode0700, no symlinks or shared/network filesystem. The floor directory must be |
| 48 | outside the state directory and its backup scope. Init creates random device and |
| 49 | registry IDs plus signing/encryption keys, and stores the keys only in an age |
| 50 | scrypt-encrypted credential file. Unlock through a private TTY prompt or explicit |
| 51 | private password FD, never an argv/env value. Keys remain in process memory only |
| 52 | while the command runs. No secret is printed or committed. Initialization writes a signed initializing |
| 53 | genesis and encrypted key file before creating the registry; an interrupted init |
| 54 | can resume only that exact genesis after unlocking its keys. It never silently |
| 55 | replaces an existing key, registry ID or partially created floor. |
| 56 | |
| 57 | The independent floor backend writes signed immutable sequence/head files with |
| 58 | exclusive creation, file fsync and parent-directory fsync; a durable head marker |
| 59 | is updated by rename under the process lock only after the immutable file is |
| 60 | complete. It stores no secret and never appears inside a registry backup. Bind |
| 61 | registry/device IDs and signer public-key fingerprint in the credential file, |
| 62 | registry and floor genesis. Missing or mismatched identity refuses. On open, validate all bounded immutable |
| 63 | floor records and their contiguous signed chain, not only the replaceable head |
| 64 | marker; an old marker cannot hide a newer completed floor file. Linux filesystem |
| 65 | semantics are tested directly with process/crash injection on a supported local |
| 66 | filesystem; an in-memory backend is insufficient. Unsupported filesystem types |
| 67 | refuse. Copying only state or rolling back its encrypted keys cannot roll back the |
| 68 | independent floor. A disk failure losing both is an unrecoverable condition in this |
| 69 | slice, not permission to initialize again. |
| 70 | |
| 71 | An optional macOS backend can use pinned |
| 72 | [github.com/keybase/go-keychain v0.0.1](https://github.com/keybase/go-keychain/tree/v0.0.1) |
| 73 | through native Security-framework calls. It must satisfy the identical floor |
| 74 | contract before being supported; this is not a prerequisite for the Linux pilot. |
| 75 | Home itself remains CGO_ENABLED=0. No secret-bearing security-command argv or |
| 76 | plaintext-key fallback exists. Keys and floor identity are bound to one operator |
| 77 | device. Copying them to a second device is not a supported concurrent writer. |
| 78 | |
| 79 | Use a dedicated local SQLite registry outside Home's database, deployment directory |
| 80 | and backup schedule. Acquire an exclusive process lock for the entire operation. |
| 81 | SQLite uses one writer connection, foreign_keys ON, secure_delete ON and synchronous |
| 82 | FULL. Schema includes registry identity/version; installations keyed by exact |
| 83 | console/account/app/primary host; immutable operation records; encrypted private |
| 84 | inputs/results; and evidence records. Every installation has one current sequence, |
| 85 | head digest, enrolled directory identity, baseline operation and current generation. |
| 86 | Only the provisioner command writes these rows. |
| 87 | |
| 88 | Every record carries registry/device ID, installation ID, monotonically increasing |
| 89 | sequence, previous record digest, random operation ID, fixed action, expected state, |
| 90 | created time and digest of randomized sealed private payload. Sign the canonical |
| 91 | closed record with Rastrillo crypto.Sign context `oficina/provisioner/record/v1`. |
| 92 | Write the immutable signed record and ciphertext with the installation's new head |
| 93 | in one transaction using sequence+head CAS. No update/delete API exists for records. |
| 94 | A reused operation ID with identical typed request returns its stored record/result; |
| 95 | changed input refuses. Retain 10,000 operations per registry and refuse new work at |
| 96 | capacity until a reviewed archive policy exists. Advance state before dispatch, |
| 97 | then append outcome evidence after receiving the exact private result. An ambiguous |
| 98 | remote result leaves an in-flight journal record; it never causes an automatic |
| 99 | second Home operation. |
| 100 | |
| 101 | The independent floor backend holds the latest registry sequence and signed head |
| 102 | digest outside the registry backup scope. A new head follows a two-phase local protocol: commit a signed |
| 103 | prepared record with old/new floor to SQLite and fsync; CAS the independent floor from |
| 104 | old to new; commit finalized state and checkpoint/fsync the parent directory. Before |
| 105 | any remote dispatch, require finalized matching floor. Crash between steps resumes |
| 106 | only the single matching prepared record. Floor ahead without the exact prepared |
| 107 | record, DB ahead without a valid adjacent preparation, competing device or any |
| 108 | older/mismatched head refuses. No sequence reuse follows a failed write. The floor |
| 109 | CAS primitive may be serialized by the exclusive device lock; re-read before write |
| 110 | and refuse unexpected values. A floor backend without durable update semantics |
| 111 | is unsupported. Tests inject crashes at every boundary. |
| 112 | |
| 113 | `oficina provision backup --output <exclusive-file>` exports encrypted registry |
| 114 | records, private payloads and wrapped recovery keys through a password-protected |
| 115 | standard age container using pinned |
| 116 | [filippo.io/age v1.3.2](https://github.com/FiloSottile/age/tree/v1.3.2) |
| 117 | and its scrypt recipient/identity APIs. No custom password encryption is introduced. |
| 118 | Use mode0600 and atomic fsync/rename. Print only the archive digest and sequence. |
| 119 | Require independent backup storage selected by the operator and a verified restore |
| 120 | drill before enrolling a non-synthetic installation. Never include a plaintext key |
| 121 | or decrypted owner address. A lost local registry does not make init available for |
| 122 | the same surviving floor identity. A restored archive must meet the surviving independent |
| 123 | floor and verify every signature/hash-chain entry; an older archive refuses. If both |
| 124 | registry and independent floor are lost, this slice cannot recover or re-enroll; |
| 125 | explicit recovery design and fresh owner confirmation are required. Export/import |
| 126 | must be human-usable commands and carry no secrets through argv, terminal or shell |
| 127 | history. Passphrases use a private terminal prompt or file descriptor. |
| 128 | |
| 129 | The separate durable floor protects against registry-backup rollback, not an |
| 130 | attacker controlling the operator device and unlocked signing keys. Copying the |
| 131 | registry and floor together and rolling both back is excluded from safe restore; document this and test |
| 132 | the refusal achievable with a surviving floor. Production recovery readiness still |
| 133 | requires the independent backup/restore rehearsal, not merely a second database. |
| 134 | |
| 135 | ## Signed private operation contract |
| 136 | |
| 137 | The Home command receives one closed private JSON body of at most64KiB: |
| 138 | `{version:1, signed_authorization, operation_input}`. signed_authorization is a |
| 139 | closed signature envelope capped at16KiB; operation_input is bounded canonical |
| 140 | JSON capped at48KiB and also subject to the64KiB total. Its exact typed encoding |
| 141 | is reused for digest and dispatch; alternate encodings, duplicate/unknown fields |
| 142 | and trailing JSON refuse. The existing operator operation fields live inside |
| 143 | operation_input. The signed authorization binds SHA256 of those exact canonical |
| 144 | bytes plus role, exact configured origin, installation ID, registry/device ID, |
| 145 | per-role sequence, previous authorization head, operation ID, issued/expires times |
| 146 | and purpose. The local journal's global sequence is bound as a reference as well. |
| 147 | The platform encrypts this body for its worker through its approved private input |
| 148 | protocol, decrypts there and supplies it on private stdin. Home-specific code does |
| 149 | not parse the platform envelope or assume it can decrypt platform-held material. |
| 150 | The public journal stores only a digest of randomized ciphertext; the signature |
| 151 | with a potentially guessable plaintext digest stays inside encrypted private input. |
| 152 | The Home command receives no registry decryption key. |
| 153 | |
| 154 | Sign authorization using context `oficina/home/operator-input/v1`, with duration |
| 155 | at most300 seconds and exact fixed operation allowlist. Each Home role validates |
| 156 | the pinned public key, envelope bounds, exact origin/role, receipt clock and typed |
| 157 | request digest before constructing its unexported operator context. Persist the |
| 158 | accepted registry identity, device identity, per-role sequence and head in its own |
| 159 | operator schema with the operation result. Require sequence exactly previous+1, |
| 160 | correct previous head, or exact already-committed replay. Each role has its own |
| 161 | ordered authorization stream because credential and directory actions interleave; |
| 162 | the outer journal's global sequence remains independent. Do not advance the |
| 163 | per-role accepted stream when merely preparing a request. A refused/expired request |
| 164 | gets an immutable outcome in the global journal, while the next per-role slot stays |
| 165 | unaccepted. A replacement authorization for that slot requires proof the old |
| 166 | attempt is not running and its authorization has expired; otherwise the provisioner |
| 167 | waits/refuses. An ambiguous attempt must be reconciled, never replaced. Successful |
| 168 | Home acceptance updates its stream and result in the same transaction. A lost accepted operation |
| 169 | result is recovered by replay of the same signed authorization, which may return an |
| 170 | already-committed result after expiry but cannot perform a new mutation. |
| 171 | |
| 172 | Read-only inspect has no accepted-stream mutation: it validates a separately |
| 173 | signed inspect challenge bound to role/origin and expiry, returns challenge plus |
| 174 | observation time with the state, and never advances authority or mints a seed. |
| 175 | The private platform result channel binds that observation to the exact attempt. |
| 176 | It cannot initialize an absent authority stream or be submitted as owner proof. |
| 177 | |
| 178 | Initial trust enrollment pins registry/device identity and operator public key |
| 179 | through the reviewed per-instance protected configuration, preserving all existing |
| 180 | keys. A changed pinned identity/key does not reset stored sequence/head. The first |
| 181 | operation requires a create-only expected-empty operator authority table and a |
| 182 | signed baseline, approved through the same fresh console gate. Thereafter absent |
| 183 | operator state alongside an existing baseline/generation is a refusal. Provisioner |
| 184 | and Home pin both key ID and exact encoded public key; no trust-on-first-request. |
| 185 | |
| 186 | The platform supplies only its accepted maintenance execution boundary. Home |
| 187 | validates the private signed authorization itself; it needs no invented signed |
| 188 | platform receipt. The fixed operator entry point is enabled only for maintenance |
| 189 | execution of the immutable configured release. Normal HTTP startup does not expose |
| 190 | it. Tests must prove direct app requests cannot invoke it. |
| 191 | |
| 192 | ## Installation and evidence sequence |
| 193 | |
| 194 | `oficina provision enroll` prepares a human-readable baseline plan and immutable |
| 195 | private request. It identifies existing credential/directory primary hosts, |
| 196 | canonical origins, account, existing data-key fingerprints, operator public pins, |
| 197 | exact release/artifact and checked configuration bindings plus registry identity. Use supported |
| 198 | `carlos instances list`, route/version checks and maintenance preflight; |
| 199 | restart's asleep output is not exec eligibility proof. The operator's fresh approval |
| 200 | is required on the console's exact request. Existing Home databases and identities |
| 201 | are retained. No credential sign-in is fabricated for the owner. |
| 202 | |
| 203 | 1. Apply reviewed additive operator/roster schemas using a normal verified release, |
| 204 | preserving role boundaries. Load separately generated operator keys through |
| 205 | protected per-instance configuration. Verify actual host version in both body |
| 206 | and X-Carlos-Version before maintenance; record response digests and time. |
| 207 | 2. Fenced directory inspect confirms exact DB/schema/role/config and no generation |
| 208 | or consumer history. The external signed baseline binds this observation and the |
| 209 | owner's never-provisioned attestation. Generate a random32-byte generation in |
| 210 | the registry and include it in the signed initialize request. Directory persists |
| 211 | that exact authorized generation once; it does not substitute another random value. |
| 212 | 3. Obtain the proposed team integer/sqid through inspect. Collect an approved sealed |
| 213 | queue request, decrypt locally only in bounded private memory, and bind its approval |
| 214 | to the operation. The synthetic pilot instead uses an explicit operator-authored |
| 215 | synthetic plan containing already-authenticated synthetic addresses and labels, |
| 216 | signed in this same journal and approved through the same fresh console gate; |
| 217 | this creates no fake queue row or alternate Home identity authority. For synthetic previews require the already-authenticated synthetic |
| 218 | identity and existing_only credential resolution. Fresh console approval runs |
| 219 | credential.owner-seed; its private signed receipt is encrypted into the journal. |
| 220 | 4. Submit directory.seed-team with that receipt, exact proposed ID, reserved label, |
| 221 | sealed-input display name and generation. A raced proposal or expired receipt |
| 222 | requires a new explicit plan/approval. A receipt cannot seed a sibling team's owner. |
| 223 | 5. Create an isolated Docs platform instance through existing supported instance APIs, |
| 224 | with a distinct DB namespace, instance/content/DEK keys, per-instance environment |
| 225 | and consumer ID. Never clone existing Home/Docs keys or documents. Attach the exact |
| 226 | canonical team alias using `carlos domains attach --app ... --route ...`; verify |
| 227 | the returned current domain binding, primary route and environment. |
| 228 | 6. Register the exact consumer with credential Home by updating its bounded immutable |
| 229 | HOME_CONSUMERS manifest through protected environment configuration. Preserve every |
| 230 | existing registration and all current assertion keys. Read back the public manifest and protected configuration delivery result, |
| 231 | then use the fixed credential compatibility checker under the maintenance |
| 232 | environment binding to verify the active manifest digest and public key |
| 233 | fingerprints. Verify the actual restarted credential host version. Its normal |
| 234 | GET /authorize validation can confirm that this consumer/audience pair reaches the |
| 235 | expected sign-in flow without sending mail; it cannot alone prove key custody. |
| 236 | Current source allows16 entries/8192bytes and requires directory registration; |
| 237 | stop at the bound. Scalable registration is separate work. |
| 238 | 7. Provision the pending directory consumer. Verify its returned signed record using |
| 239 | the independent pinned directory operator public key. Install the exact record and |
| 240 | private bearer into the isolated Docs config using supported protected delivery. |
| 241 | The local journal records ciphertext digests and the checked maintenance |
| 242 | environment binding, never secret values in public receipts. |
| 243 | 8. Collect activation evidence and publish only through the approved activation rule |
| 244 | below. Verify completed two-team browser acceptance using real synthetic identities |
| 245 | and actual Home credential authentication; no pre-minted session is live evidence. |
| 246 | |
| 247 | Evidence is collected by typed adapters, not CLI sentence parsing: use the supported |
| 248 | console/domain/instance/version response structs or add a reviewed JSON CLI output |
| 249 | for a currently text-only command. Source platform dcf0cad `instances_cli.go`, |
| 250 | `domains_cli.go`, `cli.go:routeVersionCheck`, `internal/console/apicli.go`; Home171 |
| 251 | `internal/home/runtime.go` HOME_CONSUMERS validation and `exchange.go` authorization |
| 252 | validation. Current domain responses distinguish derived State from legacy Status: require |
| 253 | State=live (not merely Status=live), then verify actual TLS/version. Domain claim |
| 254 | alone does not prove materialized TLS. Current configGet has no revision field and |
| 255 | never returns secret values; absence is not a successful secret/config comparison. |
| 256 | Use maintenance's immutable protected environment binding and a fixed non-serving |
| 257 | application compatibility checker against that snapshot. The Home checker reports |
| 258 | only exact origins, active public operator/assertion/roster key fingerprints and |
| 259 | consumer-manifest digest. The Docs checker reports exact TeamMode bindings, verified |
| 260 | provisioning record digest, generation and SHA256 of the canonical bearer already |
| 261 | present in protected config. These are bounded private results, never public logs. |
| 262 | Checkers make no network calls, mint no sessions and change no DB/key/authority. |
| 263 | They require separate small implementation/review slices; no current API is claimed |
| 264 | to supply these application-specific facts. Existing secret fingerprints are compared |
| 265 | privately to enrolled fingerprints, without copying existing private keys out. Dial only configured |
| 266 | canonical HTTPS hosts with normal certificate validation, no redirects unless the |
| 267 | specific sign-in probe validates every expected hop. Compare full expected version |
| 268 | in body/header and current platform route/alias mapping. Store bounded response |
| 269 | digests, typed binding facts and timestamps in the encrypted signed journal; token, |
| 270 | email, assertion and unrelated response content are never retained in evidence. |
| 271 | |
| 272 | ## Activation gate and pending roster integration |
| 273 | |
| 274 | An activation authorization signs exact consumer/team/app/audience, generation, |
| 275 | record sequence/digest, deployed release/artifact digest, checked maintenance environment binding, |
| 276 | credential registration digest, materialized alias binding, verification |
| 277 | start/end time and the versioned acceptance policy. The local provisioner only emits |
| 278 | it after its typed collectors pass. It is bounded to16KiB, expires within300 seconds, |
| 279 | and is approved in the fresh console maintenance request. The Home command verifies |
| 280 | this signed authorization and current stored bindings again before switching live; |
| 281 | it never trusts a request's boolean or opaque reference as evidence. |
| 282 | |
| 283 | There is an ordering conflict to resolve explicitly: current roster producer32fb |
| 284 | serves only live team_instances, but a real TeamMode startup/read needs a roster |
| 285 | before its instance can be accepted for live entry. Proposed narrow amendment for |
| 286 | review: add a separate directory-only consumer verification-window table with |
| 287 | consumer primary key/FK, expiry and audited opening operation. Verification and |
| 288 | activation audit/history rows use new additive tables; preserve the existing |
| 289 | roster_operator_receipts CHECK enum unchanged. A signed operator |
| 290 | `verify-consumer` operation may grant one pending binding at most600 seconds of |
| 291 | roster reads, using an absolute issued_at/expires_at signed into its authorization. |
| 292 | This grants actual app standing to that team's membership holders through its |
| 293 | exact consumer; it is not merely a read-only probe. It grants no item permissions. The producer still validates its exact enabled bearer/registry/route |
| 294 | and generation; it additionally accepts pending only while this audited window is |
| 295 | current. The chooser remains live-only. No browser assertion or consumer request |
| 296 | can open/extend the window. Only the exact enabled consumer's current/approved |
| 297 | overlap bearer and exact team/app/origin receive these signed pages. Other pending |
| 298 | bindings, siblings and anonymous requests remain refused. The stored expiry is |
| 299 | immutable; failure, read, retry and exact replay cannot extend it. Consumer disable |
| 300 | or expiry stops subsequent pending roster reads; existing client leases retain |
| 301 | their already-approved60-second ceiling from the last successful fetch start. |
| 302 | Tests measure that ceiling for silent SSE and protected commits as well as requests. The activation operation clears the |
| 303 | window atomically with live state. A failed verification leaves the instance pending; |
| 304 | a distinct deliberately approved verification attempt needs a new signed/fresh- |
| 305 | approved operation and audit record, and cannot overwrite the old window/history. |
| 306 | Opening while an earlier attempt is active refuses. Completed activation is |
| 307 | idempotent by operation ID/digest; later verification attempts for a live binding |
| 308 | refuse. Retain opening, expiry and activation outcome records after clearing the |
| 309 | active window. Activation compares the accepted verification attempt, exact epoch, |
| 310 | current owner membership and evidence digest under its transaction lock; a changed |
| 311 | epoch/owner requires new evidence and approval. |
| 312 | |
| 313 | This amendment changes the approved producer contract and requires explicit Fable |
| 314 | approval before implementation. No pending read exception is implied by approval of |
| 315 | the core operator package alone. It enables actual two-team authentication/standing |
| 316 | and isolation checks without listing unaccepted instances in the chooser. Initial |
| 317 | pilot teams contain only the specifically authenticated synthetic owners. Their |
| 318 | canary browser evidence does not claim general production or restore readiness. |
| 319 | |
| 320 | ## Failure and acceptance |
| 321 | |
| 322 | Every step records its intent before dispatch and its exact result before preparing |
| 323 | the next step. Resume inspects the journal and supported maintenance status; it never |
| 324 | infers success from a timeout or deletes an ambiguous record. Cross-role operations |
| 325 | are not one database transaction: a credential stub or pending team can remain after |
| 326 | a later failure. The journal identifies it, and only the exact reviewed retry can |
| 327 | continue. Automatic compensating deletion is prohibited. |
| 328 | |
| 329 | Tests cover fresh-owner refusal/removal; wrong signer/role/origin/digest; cross-role |
| 330 | stream confusion; replay after expiry versus new action after expiry; floor/DB |
| 331 | crashes; stale backups; lost floor/registry; concurrent processes/devices; |
| 332 | create-only baseline and initial generation; exact protected config/alias/version |
| 333 | mismatch; pending verification expiry; wrong team/token; activation replay and |
| 334 | membership/owner changes between observation and commit. Run an actual human-usable |
| 335 | synthetic CLI rehearsal, registry export/restore at the current floor and stale-copy |
| 336 | refusal, then actual supported platform maintenance canary. The eventual operator runbook must bound each deliberate Home maintenance outage; |
| 337 | credential Home downtime prevents suite sign-in. All source changes need |
| 338 | normal full gates, Fable whole-code review and verified canary before merge/deploy. |
| 339 | |