oficina / home Public

Define operator registry authority and pending-consumer verification design

Paul Campbell pushed by paul@keymail.dev 6c36fcbe8c2b531da566fa1e7ade886a65cd3c5f parent df93303
2 files changed, +305 −7
  • implementation/home-operator-seed.md +10 −7
  • implementation/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.