Define operator registry authority and pending-consumer verification design
2 files changed,
+305
−7
implementation/home-operator-seed.md+10 −7implementation/home-provisioner.md+295 −0
CI passed — run details
diff --git a/implementation/home-operator-seed.md b/implementation/home-operator-seed.md| index 4e42141..5c5b26d 100644 |
| --- a/implementation/home-operator-seed.md |
| +++ b/implementation/home-operator-seed.md |
| @@ -3,7 +3,8 @@ |
| Status: revised internal design, 2026-09-08. Fable R1 ended at a provider quota |
| refusal without a verdict; Home discussion 37 records it. 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 |
| +application package and a local command protocol; the proposed companion |
| +`home-provisioner.md` makes the authority adapters and staging amendment concrete; it neither mounts a public |
| operator endpoint nor authorizes live provisioning. |
| ## Contracts and scope |
| @@ -56,7 +57,8 @@ 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 |
| +Within the signed private wrapper defined in home-provisioner.md, operation-input |
| +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 |
| @@ -175,16 +177,17 @@ 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 |
| +bootstrap receipt; existing teams/memberships may be retained. Accept the exact random32-byte canonical base64url generation from the verified |
| +external baseline record, persist it once 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. An independently |
| signed external installation record and its monotonic registry floor must attest |
| that this exact directory database has never had a roster generation provisioned. |
| The record issuer, signature verification and durable external registry are not |
| -implemented by current Home or platform. They require a separate concrete design |
| -and approval before the adapter can construct initial-install authority. Platform |
| +implemented by current Home or platform. The proposed companion home-provisioner.md |
| +defines them and the explicit trusted fresh-owner baseline attestation; that |
| +amendment requires approval before constructing initial-install authority. Platform |
| maintenance provides exclusion, not this Home-specific historical evidence. If |
| that evidence is unavailable, initialization is unavailable. Restore cannot use |
| this verb. Ordinary boot only applies empty schema and never invokes it. |
| @@ -235,7 +238,7 @@ its audit/result atomically. This package verifies an opaque approved activation |
| context; it does not fetch a URL or trust arbitrary deployment evidence JSON. |
| Current platform receipts do not attest all these Home-specific facts. The |
| external provisioner/verification adapter and its pinned evidence verifier require |
| -a separate concrete authority design; no constructor may accept caller-controlled |
| +the companion home-provisioner.md authority design and approval; no constructor may accept caller-controlled |
| booleans as a substitute. Until that adapter exists, activation remains unwired. A compatibility account-link preview is not sufficient for activation. |
| ## Transaction and existing-database checks |
diff --git a/implementation/home-provisioner.md b/implementation/home-provisioner.md| new file mode 100644 |
| index 0000000..b738128 |
| --- /dev/null |
| +++ b/implementation/home-provisioner.md |
| @@ -0,0 +1,295 @@ |
| +# Operator registry and first team provisioning |
| + |
| +Status: proposed design amendment, 2026-09-08; Fable approval required. This makes |
| +`home-operator-seed.md`'s invocation, baseline and activation adapters concrete. |
| +It adds no live records, keys, configuration or command implementation. |
| + |
| +## Operator and authority |
| + |
| +Implement a separate local Go command, `oficina provision`, callable directly by |
| +a human. It uses supported CARLOS APIs/CLI and the fixed Home operator command. |
| +No LLM, remote shell, SQL console or manual database edit is required. Home keeps |
| +no platform provisioning credential. The command never serves a website. |
| + |
| +The operator authenticates to the existing CARLOS console. Every maintenance |
| +mutation still requires its actual browser session, CSRF, current account-owner |
| +lookup and `requireFreshAuth`, in that order. The reviewed maintenance request |
| +binds its immutable encrypted input and selected artifact/config/instance. The |
| +local signer cannot bypass that gate. A bearer has no invented auth_time. Source: |
| +platform dcf0cad `internal/console/stepup.go`, and platform117 revised maintenance |
| +design (currently e899a47; wire API remains under review). |
| + |
| +A separately generated provisioner signing key attests the operator's selected |
| +installation baseline and collected evidence. Both Home roles pin this public key |
| +outside the signed request. It is distinct from credential owner-seed signing, |
| +directory provisioning signing and roster response keys. Compromise of this key |
| +is operator compromise; its public signature alone cannot execute anything without |
| +the maintenance fence and fresh console approval. The credential owner receipt |
| +still resolves the requested owner's identity. The operator cannot submit a |
| +person_ref in place of that receipt. |
| + |
| +The initial baseline is an explicit trusted owner attestation: this installation |
| +has never issued a roster generation or consumer record. Deployment history, |
| +current configured versions, and fenced schema inspection inform that decision. |
| +They cannot mathematically prove historical absence after a possible restore. |
| +The review screen states this distinction and shows the exact installation and |
| +baseline digest. If the owner cannot attest it, enrollment refuses and recovery |
| +requires its separately reviewed procedure. This is the only initial-install |
| +exception; retries and ordinary startup cannot repeat it. |
| + |
| +## Registry custody and persistence |
| + |
| +The operator runs `oficina provision init --state <directory>` once. Require an |
| +empty regular directory on a persistent local filesystem; create with mode0700, |
| +refuse symlinks, network filesystems and a preexisting registry. Init creates a |
| +random device ID, registry ID, signer key and encryption key in an explicitly |
| +selected OS keychain, then creates the registry. No secret is printed or committed. |
| +The first human-operated build supports macOS Keychain through pinned |
| +[github.com/keybase/go-keychain v0.0.1](https://github.com/keybase/go-keychain/tree/v0.0.1), |
| +using its native Security-framework calls, with no secret-bearing security-command |
| +argv. This separate local CLI may use cgo; Home itself remains CGO_ENABLED=0. |
| +Unsupported platforms refuse rather than saving plaintext key files. A Linux |
| +Secret Service adapter is separate work and must pass the same custody tests. Test |
| +backends exist only in disposable tests. Keys are separate entries bound to the |
| +registry/device ID. A DB copied without its corresponding keychain identity is |
| +not an initialized registry. |
| + |
| +Use a dedicated local SQLite registry outside Home's database, deployment directory |
| +and backup schedule. Acquire an exclusive process lock for the entire operation. |
| +SQLite uses one writer connection, foreign_keys ON, secure_delete ON and synchronous |
| +FULL. Schema includes registry identity/version; installations keyed by exact |
| +console/account/app/primary host; immutable operation records; encrypted private |
| +inputs/results; and evidence records. Every installation has one current sequence, |
| +head digest, enrolled directory identity, baseline operation and current generation. |
| +Only the provisioner command writes these rows. |
| + |
| +Every record carries registry/device ID, installation ID, monotonically increasing |
| +sequence, previous record digest, random operation ID, fixed action, expected state, |
| +created time and digest of randomized sealed private payload. Sign the canonical |
| +closed record with Rastrillo crypto.Sign context `oficina/provisioner/record/v1`. |
| +Write the immutable signed record and ciphertext with the installation's new head |
| +in one transaction using sequence+head CAS. No update/delete API exists for records. |
| +A reused operation ID with identical typed request returns its stored record/result; |
| +changed input refuses. Retain 10,000 operations per registry and refuse new work at |
| +capacity until a reviewed archive policy exists. Advance state before dispatch, |
| +then append outcome evidence after receiving the exact private result. An ambiguous |
| +remote result leaves an in-flight journal record; it never causes an automatic |
| +second Home operation. |
| + |
| +The keychain also holds the latest registry sequence and signed head digest as a |
| +rollback floor. A new head follows a two-phase local protocol: commit a signed |
| +prepared record with old/new floor to SQLite and fsync; CAS the keychain floor from |
| +old to new; commit finalized state and checkpoint/fsync the parent directory. Before |
| +any remote dispatch, require finalized matching floor. Crash between steps resumes |
| +only the single matching prepared record. Keychain ahead without the exact prepared |
| +record, DB ahead without a valid adjacent preparation, competing device or any |
| +older/mismatched head refuses. No sequence reuse follows a failed write. The keychain |
| +CAS primitive may be serialized by the exclusive device lock; re-read before write |
| +and refuse unexpected values. A keychain backend without durable update semantics |
| +is unsupported. Tests inject crashes at every boundary. |
| + |
| +`oficina provision backup --output <exclusive-file>` exports encrypted registry |
| +records, private payloads and wrapped recovery keys through a password-protected |
| +standard age container using pinned |
| +[filippo.io/age v1.3.2](https://github.com/FiloSottile/age/tree/v1.3.2) |
| +and its scrypt recipient/identity APIs. No custom password encryption is introduced. |
| +Use mode0600 and atomic fsync/rename. Print only the archive digest and sequence. |
| +Require independent backup storage selected by the operator and a verified restore |
| +drill before enrolling a non-synthetic installation. Never include a plaintext key |
| +or decrypted owner address. A lost local registry does not make init available for |
| +the same keychain identity. A restored archive must meet the surviving keychain |
| +floor and verify every signature/hash-chain entry; an older archive refuses. If both |
| +registry and independent floor are lost, this slice cannot recover or re-enroll; |
| +explicit recovery design and fresh owner confirmation are required. Export/import |
| +must be human-usable commands and carry no secrets through argv, terminal or shell |
| +history. Passphrases use a private terminal prompt or file descriptor. |
| + |
| +The keychain is an independent rollback floor, not protection against an attacker |
| +who controls the operator's device and signing keys. Backing up registry and keychain |
| +together and rolling both back is excluded from safe restore; document this and test |
| +the refusal achievable with a surviving floor. Production recovery readiness still |
| +requires the independent backup/restore rehearsal, not merely a second database. |
| + |
| +## Signed private operation contract |
| + |
| +The Home command receives one closed private JSON body of at most64KiB: |
| +`{version:1, signed_authorization, operation_input}`. signed_authorization is a |
| +closed signature envelope capped at16KiB; operation_input is bounded canonical |
| +JSON capped at48KiB and also subject to the64KiB total. Its exact typed encoding |
| +is reused for digest and dispatch; alternate encodings, duplicate/unknown fields |
| +and trailing JSON refuse. The existing operator operation fields live inside |
| +operation_input. The signed authorization binds SHA256 of those exact canonical |
| +bytes plus role, exact configured origin, installation ID, registry/device ID, |
| +per-role sequence, previous authorization head, operation ID, issued/expires times |
| +and purpose. The local journal's global sequence is bound as a reference as well. |
| +The platform encrypts this body for its worker through its approved private input |
| +protocol, decrypts there and supplies it on private stdin. Home-specific code does |
| +not parse the platform envelope or assume it can decrypt platform-held material. |
| +The public journal stores only a digest of randomized ciphertext; the signature |
| +with a potentially guessable plaintext digest stays inside encrypted private input. |
| +The Home command receives no registry decryption key. |
| + |
| +Sign authorization using context `oficina/home/operator-input/v1`, with duration |
| +at most300 seconds and exact fixed operation allowlist. Each Home role validates |
| +the pinned public key, envelope bounds, exact origin/role, receipt clock and typed |
| +request digest before constructing its unexported operator context. Persist the |
| +accepted registry identity, device identity, per-role sequence and head in its own |
| +operator schema with the operation result. Require sequence exactly previous+1, |
| +correct previous head, or exact already-committed replay. Each role has its own |
| +ordered authorization stream because credential and directory actions interleave; |
| +the outer journal's global sequence remains independent. Do not advance the |
| +per-role accepted stream when merely preparing a request. A refused/expired request |
| +gets an immutable outcome in the global journal, while the next per-role slot stays |
| +unaccepted. A replacement authorization for that slot requires proof the old |
| +attempt is not running and its authorization has expired; otherwise the provisioner |
| +waits/refuses. An ambiguous attempt must be reconciled, never replaced. Successful |
| +Home acceptance updates its stream and result in the same transaction. A lost accepted operation |
| +result is recovered by replay of the same signed authorization, which may return an |
| +already-committed result after expiry but cannot perform a new mutation. |
| + |
| +Read-only inspect has no accepted-stream mutation: it validates a separately |
| +signed inspect challenge bound to role/origin and expiry, returns challenge plus |
| +observation time with the state, and never advances authority or mints a seed. |
| +The private platform result channel binds that observation to the exact attempt. |
| +It cannot initialize an absent authority stream or be submitted as owner proof. |
| + |
| +Initial trust enrollment pins registry/device identity and operator public key |
| +through the reviewed per-instance protected configuration, preserving all existing |
| +keys. A changed pinned identity/key does not reset stored sequence/head. The first |
| +operation requires a create-only expected-empty operator authority table and a |
| +signed baseline, approved through the same fresh console gate. Thereafter absent |
| +operator state alongside an existing baseline/generation is a refusal. Provisioner |
| +and Home pin both key ID and exact encoded public key; no trust-on-first-request. |
| + |
| +The platform supplies only its accepted maintenance execution boundary. Home |
| +validates the private signed authorization itself; it needs no invented signed |
| +platform receipt. The fixed operator entry point is enabled only for maintenance |
| +execution of the immutable configured release. Normal HTTP startup does not expose |
| +it. Tests must prove direct app requests cannot invoke it. |
| + |
| +## Installation and evidence sequence |
| + |
| +`oficina provision enroll` prepares a human-readable baseline plan and immutable |
| +private request. It identifies existing credential/directory primary hosts, |
| +canonical origins, account, existing data-key fingerprints, operator public pins, |
| +exact release/artifact/config revisions and registry identity. Use supported |
| +`carlos instances list/show`, route/version checks and maintenance preflight; |
| +restart's asleep output is not exec eligibility proof. The operator's fresh approval |
| +is required on the console's exact request. Existing Home databases and identities |
| +are retained. No credential sign-in is fabricated for the owner. |
| + |
| +1. Apply reviewed additive operator/roster schemas using a normal verified release, |
| + preserving role boundaries. Load separately generated operator keys through |
| + protected per-instance configuration. Verify actual host version in both body |
| + and X-Carlos-Version before maintenance; record response digests and time. |
| +2. Fenced directory inspect confirms exact DB/schema/role/config and no generation |
| + or consumer history. The external signed baseline binds this observation and the |
| + owner's never-provisioned attestation. Generate a random32-byte generation in |
| + the registry and include it in the signed initialize request. Directory persists |
| + that exact authorized generation once; it does not substitute another random value. |
| +3. Obtain the proposed team integer/sqid through inspect. Collect an approved sealed |
| + queue request, decrypt locally only in bounded private memory, and bind its approval |
| + to the operation. For synthetic previews require the already-authenticated synthetic |
| + identity and existing_only credential resolution. Fresh console approval runs |
| + credential.owner-seed; its private signed receipt is encrypted into the journal. |
| +4. Submit directory.seed-team with that receipt, exact proposed ID, reserved label, |
| + sealed-input display name and generation. A raced proposal or expired receipt |
| + requires a new explicit plan/approval. A receipt cannot seed a sibling team's owner. |
| +5. Create an isolated Docs platform instance through existing supported instance APIs, |
| + with a distinct DB namespace, instance/content/DEK keys, per-instance environment |
| + and consumer ID. Never clone existing Home/Docs keys or documents. Attach the exact |
| + canonical team alias using `carlos domains attach --app ... --route ...`; verify |
| + the returned current domain binding, primary route and environment. |
| +6. Register the exact consumer with credential Home by updating its bounded immutable |
| + HOME_CONSUMERS manifest through protected environment configuration. Preserve every |
| + existing registration and all current assertion keys. Read back the configured |
| + revision/digest and verify the actual restarted credential host version. Its normal |
| + GET /authorize validation can confirm that this consumer/audience pair reaches the |
| + expected sign-in flow without sending mail; it cannot alone prove key custody. |
| + Current source allows16 entries/8192bytes and requires directory registration; |
| + stop at the bound. Scalable registration is separate work. |
| +7. Provision the pending directory consumer. Verify its returned signed record using |
| + the independent pinned directory operator public key. Install the exact record and |
| + private bearer into the isolated Docs config using supported protected delivery. |
| + The local journal records ciphertext digests and platform config revisions, never |
| + secret values in public receipts. |
| +8. Collect activation evidence and publish only through the approved activation rule |
| + below. Verify completed two-team browser acceptance using real synthetic identities |
| + and actual Home credential authentication; no pre-minted session is live evidence. |
| + |
| +Evidence is collected by typed adapters, not CLI sentence parsing: use the supported |
| +console/domain/instance/version response structs or add a reviewed JSON CLI output |
| +for a currently text-only command. Source platform dcf0cad `instances_cli.go`, |
| +`domains_cli.go`, `cli.go:routeVersionCheck`, `internal/console/apicli.go`; Home171 |
| +`internal/home/runtime.go` HOME_CONSUMERS validation and `exchange.go` authorization |
| +validation. Domain claim alone does not prove materialized TLS. Dial only configured |
| +canonical HTTPS hosts with normal certificate validation, no redirects unless the |
| +specific sign-in probe validates every expected hop. Compare full expected version |
| +in body/header and current platform route/alias mapping. Store bounded response |
| +digests, typed binding facts and timestamps in the encrypted signed journal; token, |
| +email, assertion and unrelated response content are never retained in evidence. |
| + |
| +## Activation gate and pending roster integration |
| + |
| +An activation authorization signs exact consumer/team/app/audience, generation, |
| +record sequence/digest, deployed release/artifact digest, protected config revision, |
| +credential registration revision/digest, materialized alias binding, verification |
| +start/end time and the versioned acceptance policy. The local provisioner only emits |
| +it after its typed collectors pass. It is bounded to16KiB, expires within300 seconds, |
| +and is approved in the fresh console maintenance request. The Home command verifies |
| +this signed authorization and current stored bindings again before switching live; |
| +it never trusts a request's boolean or opaque reference as evidence. |
| + |
| +There is an ordering conflict to resolve explicitly: current roster producer32fb |
| +serves only live team_instances, but a real TeamMode startup/read needs a roster |
| +before its instance can be accepted for live entry. Proposed narrow amendment for |
| +review: add a separate directory-only consumer verification-window table with |
| +consumer primary key/FK, expiry and audited opening operation. A signed operator |
| +`verify-consumer` operation may grant one pending binding at most600 seconds of |
| +roster reads, using an absolute issued_at/expires_at signed into its authorization. |
| +This grants actual app standing to that team's membership holders through its |
| +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 |
| +and generation; it additionally accepts pending only while this audited window is |
| +current. The chooser remains live-only. No browser assertion or consumer request |
| +can open/extend the window. Only the exact enabled consumer's current/approved |
| +overlap bearer and exact team/app/origin receive these signed pages. Other pending |
| +bindings, siblings and anonymous requests remain refused. The stored expiry is |
| +immutable; failure, read, retry and exact replay cannot extend it. Consumer disable |
| +or expiry stops subsequent pending roster reads; existing client leases retain |
| +their already-approved60-second ceiling from the last successful fetch start. |
| +Tests measure that ceiling for silent SSE and protected commits as well as requests. The activation operation clears the |
| +window atomically with live state. A failed verification leaves the instance pending; |
| +a distinct deliberately approved verification attempt needs a new signed/fresh- |
| +approved operation and audit record, and cannot overwrite the old window/history. |
| +Opening while an earlier attempt is active refuses. Completed activation is |
| +idempotent by operation ID/digest; later verification attempts for a live binding |
| +refuse. Retain opening, expiry and activation outcome records after clearing the |
| +active window. Activation compares the accepted verification attempt, exact epoch, |
| +current owner membership and evidence digest under its transaction lock; a changed |
| +epoch/owner requires new evidence and approval. |
| + |
| +This amendment changes the approved producer contract and requires explicit Fable |
| +approval before implementation. No pending read exception is implied by approval of |
| +the core operator package alone. It enables actual two-team authentication/standing |
| +and isolation checks without listing unaccepted instances in the chooser. Initial |
| +pilot teams contain only the specifically authenticated synthetic owners. Their |
| +canary browser evidence does not claim general production or restore readiness. |
| + |
| +## Failure and acceptance |
| + |
| +Every step records its intent before dispatch and its exact result before preparing |
| +the next step. Resume inspects the journal and supported maintenance status; it never |
| +infers success from a timeout or deletes an ambiguous record. Cross-role operations |
| +are not one database transaction: a credential stub or pending team can remain after |
| +a later failure. The journal identifies it, and only the exact reviewed retry can |
| +continue. Automatic compensating deletion is prohibited. |
| + |
| +Tests cover fresh-owner refusal/removal; wrong signer/role/origin/digest; cross-role |
| +stream confusion; replay after expiry versus new action after expiry; keychain/DB |
| +floor crashes; stale backups; lost keychain/registry; concurrent processes/devices; |
| +create-only baseline and initial generation; exact protected config/alias/version |
| +mismatch; pending verification expiry; wrong team/token; activation replay and |
| +membership/owner changes between observation and commit. Run an actual human-usable |
| +synthetic CLI rehearsal, registry export/restore at the current floor and stale-copy |
| +refusal, then actual supported platform maintenance canary. All source changes need |
| +normal full gates, Fable whole-code review and verified canary before merge/deploy. |