From 85858938fca91b097c0cbc3f737e67fbd8d20e27 Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Tue, 10 Nov 2026 23:52:29 -0600 Subject: [PATCH] docs: design credential bindings for agent copies --- ...26-11-11_copy-credential-binding-design.md | 450 ++++++++++++++++++ 1 file changed, 450 insertions(+) create mode 100644 docs/plans/2026-11-11_copy-credential-binding-design.md diff --git a/docs/plans/2026-11-11_copy-credential-binding-design.md b/docs/plans/2026-11-11_copy-credential-binding-design.md new file mode 100644 index 000000000..063ba8b9c --- /dev/null +++ b/docs/plans/2026-11-11_copy-credential-binding-design.md @@ -0,0 +1,450 @@ +# Credential binding for agent copies + +Date: 2026-11-11 UTC. Lane `org-cred-bind-design`, issue #1873. +Status: proposed, for Jason's approval before implementation. Doc only. +Baseline: `6415e81ee75a6783b889b67b985f61825f9a074e` (`origin/refactor`). +All current-code citations below refer to that commit. References to other +designs describe proposed policy, not a claim that it is implemented. + +## 1. Outcome and boundaries + +An owner can approve a credential binding for one root or project agent copy +inside one organization. A launch uses only that copy's own secret slot, +with verified service permissions bounded by its frozen role and the +organization's narrower approval. No agent can bind credentials for itself +or another agent. Missing, stale or uncertain authority refuses launch and +credential use. Copies in other organizations remain independent. + +This proposes a writer, schema and enforcement changes for later slices. It +changes no code, right, role, contract, secret or running instance. Host and +model credentials remain on their existing grant and proxy paths. There is +no migration of named-agent credentials, no root-wide secret binding, no +shared token pool, and no new plugin service in the first build. + +Owner direction requires each non-assistant agent's credentials to stay +inside its organization even when its definition comes from root +(`docs/plans/2026-11-09_unified-workspace-owner-direction.md:124-131`). A3 +explicitly deferred binding and its two-org thread-copy test +(`/mnt/storage/scratch/mosaic-lead/reports/org-a3-build.md:52,216-217`). + +## 2. Current behavior, traced before the proposal + +| Step | Current behavior and evidence | +| --- | --- | +| Declare requirements | `checkCredentials` accepts each known service once and validates its scope shape. Gitea uses category levels; Vikunja uses route-group verbs; host has no scopes (`packages/organization/src/role.mjs:100-143`). `admin` and `all` are outside the grantable Gitea categories (`packages/organization/src/vocabulary.mjs:187-191`). | +| Declare a named instance | `roles..credentials` must reference exactly the role's services, except host grants and the chief's Gitea exception when no library exists. Vikunja also requires a unique bot identity (`packages/organization/src/organization-config.mjs:50-59,124-167`). | +| Locate the slot | `parseCredentialRef` accepts exactly one of `store`, `file` or `env`. `{store:true}` derives the file from config directory, business, agent and service; it does not accept another agent's store path. Named agents can still use external file and environment references (`packages/organization/src/credentials.mjs:23-59`). This lane proposes only the store form for copies. | +| Resolve launch inputs | `resolveInstance` takes the entry from the validated organization, intersects tools/network/actions with limits, then returns its non-host credential references and tracker metadata (`packages/organization/src/resolve.mjs:34-69,81-95`). These limits do not compute a credential scope intersection. | +| Load secrets | Runtime indexes references by `business/role` and sets the store root to the agents secret directory (`packages/bus/src/runtime.mjs:333-345,378-379`). `Credentials` loads those references into its private map (`packages/bus/src/credentials.mjs:130-165`). It derives its store filename from business, agent and service (`packages/bus/src/credentials.mjs:351-355`). File reads reject symlinks, wrong ownership/mode and repository/data-root placement (`packages/bus/src/credentials.mjs:357-415`). | +| Use secrets | `Credentials.useFor` derives business and role from the session, rather than a caller-supplied target. Missing tokens refuse `credential-unavailable`; Vikunja expiry refuses `credential-expired` (`packages/bus/src/credentials.mjs:597-624`). The task adapter uses that session-bound path (`packages/tasks/src/adapter.mjs:84-88`). | +| Issue and revoke | The broker requires a human, with an explicit tester-verb exception; it checks that the agent exists and prevents delete while ordinary or thread claims remain (`packages/bus/src/broker.mjs:6212-6225`). The credential writer reads only `document.roles[agent].credentials`; issue writes a store reference, revoke drops memory/file before writing or recording the receipt (`packages/bus/src/credential-verbs.mjs:16-51`). | +| Record writes | The organization writer takes the shared registry lock, then the organization's history `write.lock`, validates the revision and document, records a prepared change and publishes the file (`packages/organization/src/organization.mjs:143-159,175-220,278-340`; `packages/organization/src/org-registry.mjs:73-101`). Its `pathsOnly` option records changed field paths and presence without values (`organization.mjs:276-278,324-327`). | +| Materialize copies | Root and project entries explicitly refuse `credentials`, `tracker`, `identity`, `publish`, `holder` and `hosts` (`packages/organization/src/agent-levels.mjs:18-41,53-59,105-115`). Copy names preserve root names or append project names. Collisions refuse both claimants; org instances shadow copies. Every required non-host service makes a copy `unbound` (`packages/organization/src/agent-copies.mjs:36-53,73-104`). An unbound copy never joins the role map and launch refuses `credential-unbound`, naming missing services (`agent-copies.mjs:223-245,279-287`; `packages/cli/src/launcher.mjs:361-375`). | +| Guard launch races | Launch checks against a fresh organization revision, then final `request.admission` compares it again after async preparation (`packages/cli/src/launcher.mjs:357-364,802-813`; `packages/bus/src/runtime.mjs:636-661`). This revision covers organization bytes, not role file bytes. A3 records that limitation explicitly (`/mnt/storage/scratch/mosaic-lead/reports/org-a3-build.md:215`). | + +Important limit: validating a slot reference does not prove the remote +token's permissions. For non-host/non-model services, `Credentials.#format` +checks token syntax and length, not remote scopes +(`packages/bus/src/credentials.mjs:448-451`). Existing Gitea minting has a +fixed seat scope list, not a per-copy intersection +(`packages/bus/src/gitea-grant.mjs:27-30,262-269`). Vikunja startup has +specific negative permission probes, not a complete per-composition scope +proof (`packages/tasks/src/startup.mjs:30-55`). The new build must close +these gaps for copies; a JSON field that asserts scopes is insufficient. + +## 3. Choice and storage + +Considered three approaches: + +1. Add credentials to root/project definitions. Rejected: a root definition + would carry a per-org concern and invite cross-org reuse. +2. Write synthetic copies into `roles`. Rejected: this conflicts with org + shadowing and turns a boot copy into a persistent named instance. +3. Add an org-local binding overlay and reuse the existing per-agent store. + Recommended. It keeps copy definitions secret-free, preserves shadowing, + and gives bindings the same revision fence as other org settings. + +The authoritative metadata lives in the existing organization file, +`/businesses/.json`, under a proposed top-level +`copyBindings` key. It is not written into `agents.json` or `roles/`. +An absent key means no bindings. The writer stores one record per concrete +copy instance; the example below is illustrative schema, not current JSON: + +```json +"copyBindings": { + "writer.site": { + "copyId": "", + "orgIncarnation": "", + "source": { "level": "project", "name": "writer", "project": "site" }, + "definition": { "ref": "org:release-writer", "rev": 3, "sha256": "" }, + "generation": 1, + "state": "bound", + "services": { + "gitea": { + "slot": "self", + "scopes": ["read:repository"], + "resources": { "projects": ["site"] }, + "rotateBy": "2026-12-01", + "grantId": "", + "grantGeneration": 1 + } + }, + "decision": "" + } +} +``` + +`slot: "self"` means exactly `(org, concrete instance, service)`. It is a +selector for today's derived slot, not an arbitrary string slot name. The +record contains no path, environment name, token, token fragment or token +hash. Grant ids identify authorization records, never encode token bytes. +Secret values stay exclusively in the existing private credential store +and trusted service-call memory, never in metadata, evidence, bundles, +decision text or errors. The symbolic paths in this design describe source +architecture; runtime responses never emit resolved host paths. + +`source` includes the level and original name/project, so dotted-name +collisions cannot silently attach an old binding. `copyId` is created on +first approval and retired on fire, shadowing, source removal or replacement. +`generation` increases for every binding change or revocation and never +resets while that copyId exists. An unbound tombstone remains after unbind. +A new copyId never adopts an old slot file. Name reuse requires verified +old-slot cleanup and newly verified credentials before activation. + +For a root copy, source.project is null and its definition is the frozen +root definition identity. For a project copy it names the org definition +or template identity. A definition hash includes composition and contract; +the applicable instruction/personality revision is pinned too. Import +provenance (`from.sha256`) is not the customized definition's current hash. + +Vikunja metadata additionally contains the existing bot identity shape +(`tracker: {bot, botId}`) at the copy record level. It must pass the same +org-wide uniqueness check as named instances, including other copies. +No service binding implicitly grants identity, publication, host enrollment +or project access. Existing identity/publish requirements remain separate. + +### Writer and lock order + +Extend `organizationWriter` with dedicated trusted methods +`applyCopyBindingDecision` and `revokeCopyBinding`; names are proposed API. +Only the authenticated broker/Console owner path invokes these methods. +Generic document writes and root/project writers must reject changes to +`copyBindings`, even if a submitted document validates. An actor string, +decision id or hash supplied by an agent is never authorization. + +Reuse the shared registry lock `/org-runtime.lock`, then the +organization lock `/businesses/.history//write.lock`. +Do not introduce a second competing metadata file or lock. Reuse the +writer's revision conflict, prepared/committed receipt and paths-only +evidence machinery. Under the locks, establish fresh active org incarnation, +owner identity, expected document revision and decision digest. Perform +the final checks in the publication critical section, not only in HTTP. + +Do not hold filesystem locks across remote service calls or waits for runs +to stop. Before such work, persist a non-active pending operation with a +unique id and expected generations. Reacquire the locks, re-read and compare +all identities before publishing. A competing write invalidates preparation. +Use the same operation id for idempotent recovery; a prepared or uncertain +operation cannot produce a usable binding or resurrect the prior token. + +## 4. Authority, decisions and deployed ownership + +Use the existing grantable `org.hire` right as the authority-change entry +right. It already exists, defaults to owner yes/member no, and maps +`hire.resolve.console` to `org.hire` +(`packages/organization/src/org-rights.mjs:6-26,67-69`). Do not add an +`org.credentials` right that would imply delegable token-grant authority. + +The right is necessary but not sufficient: any decision containing a +credential line requires the current owner of the target org, even if a +member holds `org.hire` or `decide`. Map credential resolution to an +explicit owner check as well as the right check, rechecked at commit. +The owner-only credential read stays owner-only; `board.read` gets at most +the existing copy's missing-service status, never a binding record. + +Bind and unbind are **authority-change decisions**, represented by the +hire model's `op: "change"`, with a binding-specific payload. The +decision names source, copyId (or new-copy proposal), org incarnation, +definition revision/hash, before/after service scopes and resources, and +expected binding generation. The broker computes credential lines and +the whole-decision digest. Approval consumes the decision once. Dropping, +narrowing or replacing a line invalidates the old digest and requires +confirmation of the new digest; widening always raises a new decision. + +This reuses hire-as-a-decision instead of creating a second approval +model. It follows the roles-and-rails design's credential ruling, +owner restriction, digest, seven-day expiry, one-step human-originated +decision, and post-approval binding steps +(`docs/plans/2026-11-10_roles-and-rails-design.md:35,232-310`). The chief +may propose a binding only through that decision path, never by naming a +slot, supplying a token or calling the writer. Other agents cannot raise +this authority-change subtype. No agent resolves it, including the chief, +an arbiter, its subject, or a different copy of the same definition. + +This is an authority change under system rails R1, R5, R7 and R8 and the +role's credential rail. Default autonomy is **in the loop**. As required by +the existing design, credential bindings do not move to near or out of the +loop at all without a new owner ruling +(`docs/plans/2026-11-10_roles-and-rails-design.md:90-119,416-425,445-463`). +Unbind uses the same human decision record, including a one-step +owner-originated decision. Emergency **revoke** remains an owner-only +immediate verb: it only removes use, must not wait for a chief proposal, +and records the authority reduction. Revoke never issues a token or changes +an unbound binding to bound. + +A root-level definition change still needs the instance owner. Binding one +root copy inside org O needs O's owner and grants nothing in another org. +Being an instance operator or root-business owner alone does not make a +person O's owner. Accounts design separates these principals explicitly +(`docs/plans/2026-11-10_accounts-design.md:314-349`). No account-only session +falls back to a human/root transport. All Console mutations retain Origin, +session, fresh ownership and confirmation fences. + +The host remains the trust boundary. Host root and the instance uid can +already change the instance and its secrets; this proposal does not claim +protection from either, or from a sandbox escape to that uid. The deployed +instance uid must remain separate from human/development seats +(`docs/plans/2026-11-10_accounts-design.md:887-936`). + +## 5. Permission calculation and proof + +For every service V, compute: + +`effective(V) = role-declared(V) intersect org-approved-binding(V)`. + +The binding is the org's per-copy narrowing. Omitted services are unbound, +not permission to use another source. Unknown services/scopes refuse; they +are never ignored. The requested scopes must already be a subset of the +role. Do not silently trim a broader request and report that it succeeded. +Resources also intersect the org's current admitted project set and the +copy's union. A project copy may access its project only. A root copy may +access only the approved projects that still admit it. An empty intersection +allows no service operations and leaves a required service unbound. + +Scope comparison is semantic: Gitea read is narrower than write in the same +category, categories are independent, and neither admin nor all is allowed. +Vikunja compares each route-group/verb pair. Missing categories/groups mean +no permission; there is no wildcard. All adapters must apply their service's +registered semantics, never compare scope strings lexicographically. + +### At proposal, approval and bind + +1. Resolve the concrete copy from the target org's boot snapshot and current + project union. Refuse missing, shadowed, colliding, excluded or fired + copies. Never create `roles[copyName]` to make the operation succeed. +2. Validate the role through the core composition validator and current + enabled registry. Read T1's definition revision/hash, or the frozen root + role identity. Verify org active state and incarnation. Compute all + credential lines from these inputs, not from chief-supplied summaries. +3. Check every requested service/scope/resource against that definition and + org. Verify the resolver is the fresh org owner. The owner cannot override + a system rail or widen a binding past the approved role; changing the role + requires its own approved change first. +4. Obtain a dedicated service credential for this exact org/copy/service. + Trusted service adapters must establish both token scopes and remote + identity resource grants. Prove the allowed projects/repos at the service, + not just a local `resources` list. A root-wide or cross-org bot account is + unacceptable even if its token is stored in a per-org directory. +5. Accept a token only after trusted live introspection verifies its scopes, + service account, permitted resources and expiry against this operation, + or after a reviewed managed mint path produces and verifies that exact + bounded grant. An owner-entered assertion, copied grant id, token format, + or a few successful/negative probes are not proof of complete permissions. + If the service/version cannot prove the bound, refuse + `credential-scope-unverified`. Ship that refusal rather than an unsafe + compatibility mode. This is a required service-adapter build item. +6. Stage the secret privately in the existing store with the binding inactive. + Recheck decision digest, definition hash, org incarnation, document + revision and slot generation after remote work. Only then commit the + metadata/receipt. A grant record binds the opaque grant id, copyId, + incarnation and generation to the issued slot. It stores no secret or + token-derived digest. Ambiguous crash recovery keeps use closed and + requires a new verified issue if exact provenance cannot be established. + +Gitea's existing fixed `SEAT_SCOPES` must not be reused for a read-only role. +Vikunja needs a reviewed verifier for the complete route-group scope set +and bot/project access. Until each adapter exists, that service's copy +binding refuses. Do not claim the existing named-agent path provides this +proof. Extending remote minting beyond its current approved envelope is a +separate protected change, identified in slice CB2 below. + +### At launch, restart and every use + +The first build uses the existing restart model for **new references**. +Approval may record a binding, but it is not usable until the org bus +restarts and verifies it. Surface `restart-required`, not a successful +launch. Today reference/bot changes already require restart +(`packages/bus/src/runtime.mjs:547-583`). Do not add live reference reload +in the first slice. Revocations take effect immediately and never wait +for this restart. + +On boot and every launch, re-resolve the effective role and binding, +validate source/copyId/incarnation/generation, and obtain fresh trusted +service scope/resource proof for the actual slot contents. If verification +is unavailable, launch refuses. All required services must be bound and +available. Bindings may be approved incrementally, but partial success +cannot launch. A stale file alone never counts as a binding. + +Preserve A3's organization revision fence. Add a separate launch authority +digest over frozen definition identity, copyId, org incarnation, binding +generation and verified grant generation. Do not redefine the writer's +document revision. Carry both into final admission and `bindLaunch`, and +bind the admitted generations to the session. A pending revocation blocks +admission, including the interval between final admission and spawn/bind. +If a child was spawned before revocation won, deny its capability and stop +it; it must perform no credential call. Do not release a launch fence on an +uncertain outcome until that child is confirmed stopped. + +At every credential-bearing operation, check session org/instance, active +incarnation, effective scopes/resources, binding/grant generation and slot +liveness before any external request. Derive the slot from the session, +never from request arguments or thread text. Raw tokens and credential file +paths do not enter sandboxes. The trusted service adapter checks each +method/resource as well as token permissions. Constrain outbound destinations +to the approved service origin and reject redirects carrying authorization +to another origin. Operation completion rechecks the generation before +publishing its result. Unknown or stale state refuses closed. + +## 6. Lifecycle and running copies + +All transitions below close credential admission first, invalidate affected +session generations, then stop **all** affected ordinary and thread runs. +Serialize close versus new admission so a concurrent launch cannot escape +the stop inventory. Abort cancellable service calls and wait for all held +uses to settle before slot deletion or replacement. A completed external +write cannot be undone; report that limit, never call local revoke a remote +rollback. Existing non-host `Credentials.use` does not provide the host-key +abort behavior, so generalizing the use fence is required, not assumed +(`packages/bus/src/credentials.mjs:556-569,618-653`). + +| Event | Durable outcome and effect on a running copy | Next launch/use | +| --- | --- | --- | +| Unbind service | Owner-approved change bumps generation, records unbound tombstone, closes use, stops every run of that copy, revokes/removes its slot. Never retain a reusable token behind an inactive binding. | `credential-unbound`: `credentials.gitea needs an owner-approved binding.` | +| Emergency revoke | Owner verb immediately closes use, bumps generation, stops all copy runs and performs local revoke. A managed remote grant is also revoked through its approved adapter. Keep tombstone; reissue needs a fresh decision and verification. | `credential-revoked`: `credentials.gitea was revoked; the owner must approve a new binding.` | +| Slot removed or altered outside the writer | A slot liveness/identity check at use and launch refuses even if memory still holds the token. Mark binding unavailable, invalidate session use and stop its runs. Never fall back to cached memory, environment or another slot. Replacement is a newly verified issue. | `credential-unavailable`: `credentials.gitea is unavailable; the owner must verify the slot.` | +| Expiry or upstream revocation | Local expiry checks and service authentication failure close the binding, invalidate use and stop runs. Rotation is a new verified slot generation and owner-approved change; no in-place replacement behind active uses. | `credential-expired`: `credentials.vikunja has expired; the owner must approve a replacement.` Upstream revoke uses `credential-revoked`. | +| Role/definition changed | Any role identity/hash/revision mismatch closes use and stops affected copies. Do not inherit old credentials even for equal service names. A narrower role requires new bounded service proof; a wider role requires approval first and fresh binding. T1 blocks edits of an in-use definition until the change-decision path exists. | `credential-role-changed`: `credentials.gitea no longer matches the approved definition; the owner must review the binding.` | +| Copy fired, source removed, excluded or shadowed | Close use, retire copyId, stop every run, revoke slots and delist the copy. Preserve run/trail evidence. A newly shadowing named instance must not load the old copy's file at the identical slot name. Require cleanup before it can use that name. | `copy-unavailable`: `copy is no longer active in this organization.` | +| Copy-name collision | Refuse attaching either binding. If introduced while running, close affected use and stop both claimants. No first-wins fallback. | `copy-refused`: `copy identity is ambiguous; the owner must resolve the definition collision.` | +| Organization archived | Close the org's admission, invalidate its running sessions and stop every org run before archive completes. Revoke all copy service slots and mark bindings unbound. Restore preserves definitions but requires fresh binding decisions. Never affect another org's runs or slots. | `org-archived`: `organization is archived.` After restore, `credential-unbound`. | +| Organization deleted | Close all admission/use and stop every org run, revoke managed remote grants and local slots before the existing quarantine flow. Preserve evidence/tombstones in deletion evidence. A new org with the same id has a new incarnation and no inherited bindings. | `org-unavailable`: `organization is not active.` Recreated org starts unbound. | +| Revoke/stop/cleanup uncertain | Persist a non-usable pending-revocation state and keep the affected copy or org fenced. Do not publish success or restore memory on write failure. Retry with the same operation id. Remote revocation uncertainty remains visible to the owner. | `credential-revoke-failed`: `credentials.gitea revocation is incomplete; the owner must retry cleanup.` | + +These archive/delete effects are proposed extensions. Current archive stops +the bus and marks the registry archived; deletion quarantines org runtime, +document/history and org secret trees, while its receipt says remote-side +revocation is out of scope (`packages/organization/src/org-service.mjs:239-267,332,343-347`). +The new build must explicitly test and add copy-run and copy-grant cleanup; +it cannot infer it from a stopped bus. + +Refusal codes in this section are proposed public contracts where new. +Use `human-required` with `Only the organization owner can approve credential +binding.` for an agent/member mutation, `credential-not-yours` with +`credentials.gitea must use this copy's own organization slot.` for a wrong +slot, `credential-scope-exceeded` with `credentials.gitea.scopes exceeds the +approved role.` for excess scopes, and `credential-scope-unverified` with +`credentials.gitea permissions could not be verified.` for missing proof. +`credential-binding-changed` says `credentials.gitea changed; read and approve +again.` for stale generation; `organization-changed` retains the existing +document-race code. Validation errors name field positions such as +`copyBindings entry 1 services field`, not untrusted submitted keys. + +All new boundaries allow only registered service/field labels and fixed +remediation text. Never interpolate a submitted value, resolved path, raw +service error, URL, bot name, secret, token hash or token fragment into an +error, receipt or trail. New metadata serializers use an allowlist and +paths-only receipts. Scan human text with the existing secret scanner and +fixed field-size limits; arbitrary unknown keys refuse with a fixed field +label. Metadata holds only validated ids, known scope names and bounded +dates/integers. The scope is every new copy-binding surface, consistent +with the brief's #1916/#1920 requirement, without changing unrelated legacy +diagnostics. Neither reviewer nor implementer needs live secrets to test it. + +## 7. Two organizations and identity reuse + +Example acceptance fixture: org A and org B both materialize root `writer` +from the same creative template. A's binding selects `(A, writer, gitea)`; +B's selects `(B, writer, gitea)`. Each uses a dedicated remote bot/token whose +resources exclude the other org. B cannot select A's slot or reuse A's grant +record. A path restriction alone would not prevent a token with both orgs' +remote grants, so service proof must check those grants too. + +Enforce isolation at all of these independent boundaries: + +1. Console resolves the target org from the authorized route and fresh + ownership, not a body-supplied org or an instance-wide owner flag. +2. The writer derives its org and slot from that context and binds the + metadata to the registry incarnation. A copied record from A has the + wrong incarnation in B and refuses before opening any slot. +3. Copy source tuple, copyId and definition digest must match B's materialized + copy. A root definition carries no binding to propagate. +4. Credential loader derives the slot, enforces private regular-file rules, + rejects alias/symlink/hardlink substitution for copy slots and verifies + remote grant ownership. Reject nlink other than one for these slots. +5. Every use derives org/instance from the authenticated session and checks + the active binding generation. A thread, room or caller-supplied instance + cannot choose the key. Session and grant records cannot cross buses. +6. Archive/delete touches only that incarnation's resources. A failed cleanup + never makes credentials valid for a later org or copy with the same name. + +Registry incarnation is a dependency of accounts design +(`docs/plans/2026-11-10_accounts-design.md:501-514`). The binding build must +use that stable value; if the target baseline lacks it, land an explicitly +approved incarnation slice first. Do not substitute timestamps, a pathname +or an org id alone. The dedicated store layout remains unchanged. + +## 8. T1 dependency and implementation slices + +T1 is in flight. Its brief is +`/mnt/storage/scratch/mosaic-lead/uidev/org-release/T1-BRIEF.md`. This design +depends on its org-local definitions, `org:` resolver, revision/hash read, +composition validation and in-use definition guard, as specified in +roles-and-rails sections 4 and 5 +(`docs/plans/2026-11-10_roles-and-rails-design.md:157-170,186-228`). Do not +patch T1's files in this lane. Project copies use those org definitions; +root copies keep a frozen root identity. Supporting root-local customized +definitions waits for that resolver to exist, rather than inventing another +definition format here. A4 supplies the authority-change decision engine; +this build extends it, rather than shipping a temporary direct bind verb. + +Every slice below is a later proposal requiring Jason's approval before +work. Each ends with focused tests plus affected organization, business, +bus, CLI and Console suites. No slice edits `roles/` or `contracts/`. + +| Slice and dependencies | Build and acceptance evidence | Protected changes needing approval | +| --- | --- | --- | +| CB0: identity foundation, after T1/accounts | Reuse incarnation or add its approved missing support. Define copyId, source/hash pin and binding generation semantics. Pure resolver tests cover identical root copies across two orgs, project dotted-name collisions, org shadowing, removed/recreated org and source-name reuse. No secret read and no launch enabled. | Org schema; registry schema only if incarnation is absent. No new rights or role files. | +| CB1: metadata and decisions, after CB0/A4 | Add `copyBindings`, guarded organization writer, owner-only credential subtype under `org.hire`, digest/expiry/single-consumption and paths-only receipts. Tests: owner binds/unbinds; member with org.hire/decide, chief, arbiter, subject agent, tester and reader all fail to resolve/write; agent may not call raw writer; only chief agent can propose; stale revision/digest/generation, expired approval, replay, wrong incarnation and generic write injection have no activation. Pending/partial binding never launches. | Organization and decision payload schema; `VERB_RIGHTS`/owner dispatch changes even though no new grantable right. Approval explicitly covers the reuse of org.hire with the owner restriction. | +| CB2: verified service slots, after CB1 | Reuse self-only store and dedicated bot identity; add complete Gitea/Vikunja verification or exact bounded mint adapters. Tests use fake services for narrower/equal/broader scopes, read/write hierarchy, extra route verbs, unrelated repos/projects, reused foreign bot/grant, opaque/unverifiable token, wrong date, symlinks/hardlinks, stale file and token replacement. No request, path or token enters metadata/evidence; lost replies and crash points remain inactive. A live scratch-service acceptance must prove each service/version's permission verifier before that service enables copy launch. | Credential handling/security boundary; grant metadata schema and any remote mint/revoke envelope changes. No automatic reuse of existing fixed Gitea seat scopes. | +| CB3: admission and use, after CB2 | Materialize only verified bound copies; new references visibly need restart. Check document revision plus separate authority digest at prepare, final admission and bindLaunch. Session-bound adapter checks every operation and slot liveness. Race tests mutate role hash, org bindings, token contents or admitted projects during async prepare and between admission/spawn/bind/use. Assert no admitted credential call, including an empty project manifest. Concurrent revoke denies new work; no cached-token fallback after unlink. | Launcher/broker credential enforcement boundary; session/launch authority fields and any store schema needed to persist fences. No live reference-reload policy. | +| CB4: lifecycle and recovery, after CB3 | Test unbind, emergency revoke, slot removal, expiry, role change, fire, collision, shadowing, archive/restore, delete/recreate. Each closes admissions, invalidates ordinary and every thread run, aborts or drains uses, and denies later calls. Inject failures before/after metadata publication, secret staging, remote revoke, stop acknowledgement and receipt write; restart never resurrects authority. Include named-instance shadowing over a former copy's slot. Other org continues. | Credential/revocation and org lifecycle boundary; durable pending-operation schema. Existing deletion quarantine must not silently claim remote revoke. | +| CB5: two-org thread acceptance and operator flow, after CB4 | Finish A3's deferred test with a shipped Gitea-requiring creative thread role in both orgs. Approve independent verified bindings; boot real buses and use fake harness/service, create one thread/session per org, launch both, write/read records, reopen each. Assert distinct session/run/trail/workspace and slot identities, no A record visible in B, no B request uses A's token, and B survives A revoke/archive/delete. Reject raw A binding/grant replay in B. Operator flow shows computed scope lines, confirmation, missing service, restart and remediation states, with sentinel redaction tests on every response/event/log. No screenshot gate for this doc lane. | Console mutation/decision flow; no further right or role changes. Any discovered schema delta returns to approval before build. | + +Existing regressions to retain include organization `credentials.test.mjs`, +`role.test.mjs`, `agent-copies.test.mjs`, `instance-credential-transitions.test.mjs`, +bus `credential-store.test.mjs`, `credential-verbs.test.mjs`, +`agent-copies.test.mjs`, and CLI `thread-launch.test.mjs` and +`launcher-final-race-probe.test.mjs`. Add meaningful new focused tests for +the guarantees above; merely checking the proposed JSON shape is inadequate. + +Acceptance samples for this document are the seven brief requirements: +storage/writer/locks in section 3; right/rail/decision in section 4; +role ceiling and verification in section 5; lifecycle/code/text in section 6; +two-org enforcement in section 7; tests/slices/protected changes in section 8; +and at most three owner questions below. No visual samples apply. + +## 9. Questions for Jason, with recommendations + +1. Approve `org.hire` as the entry right while keeping every credential + binding/unbinding approval owner-only? Recommend yes: it matches the + existing hire decision model and does not grant members token authority. +2. Approve the overlay schema and CB0-CB5 security changes, including complete + service scope/resource proof as a prerequisite and restart for new + references? Recommend yes. A service without a verifier stays refused; + implement Gitea first if its verifier is ready before Vikunja's. +3. On archive/restore, discard copy credentials and require fresh bindings? + Recommend yes. It adds setup on restore, but avoids keeping dormant + usable grants and gives archive one clear revocation meaning. + +These are recommendations, not approvals. The Lead takes this design to +Jason. No implementation or rollout is authorized by this document. -- 2.54.0