Credential binding slice CB4 (#1873): copy credential lifecycle and recovery. Design: PR #1923, approved by Jason on 11-11 at 17:55Z with CB0 to CB5 as written. Rulings in force: org.hire is the entry right, binding approvals are owner-only, there is no org.credentials right, and archive discards copy credentials (Q3).
packages/bus/src/copy-revoke.mjs (new): one revoke job per copy for every lifecycle event, in this order: close the gate, record the pending op, drain in-flight requests, stop every run and wait for each ack, remove the slot, revoke the token at Gitea, then finish. Only a definite finish reopens the gate, and then only to the tombstone's own refusal code.
Durable pending op (copy-bindings.mjs, organization.mjs): new binding state revoking and a revoke record with op id, cause, time, failed steps and per-service remote outcomes. A revoking record never resolves. It answers credential-revoking, or credential-revoke-failed after an uncertain step.
Restart and sweep (runtime.mjs): at start, resume finishes each pending op under its own id before anything can use the copy. sweep checks each copy holding a grant at start and every 60 s, for expiry, slot loss, retire, role change, collision and shadowing.
Gate triggers (copy-launch.mjs): the CB3 gate reports a permanent loss through onLoss, which starts the job.
Remote step: Gitea revoke goes through the organization's managed admin slot. 204 is revoked, 404 is absent. With no admin slot, or one on another server, the outcome is not-managed: the copy is tombstoned and denied, but the token stays valid at Gitea until it expires, and the record never claims it was revoked. Any uncertain answer is failed and keeps the copy fenced for the next pass.
Archive and delete (org-service.mjs, CLI org-bus.mjs, Console org-admin.mjs): archive (Q3) and delete revoke each copy's grant through the same step. Receipts list each copy's remote outcome and never claim a revoke that did not happen. Restore brings no copy credential back.
Repository guard: the owner's updateOrg and project.remote.set refuse project-repo-bound while a copy binding still grants the project (pending, bound or revoking). This is a new refusal on existing verbs; the code itself already existed (F7).
Refusal table: a new org-archived org-unavailable group for copies, with a next step that names no path.
Nothing in roles/ or contracts/.
Tests: copy-revoke.test.mjs (new), copy-revoke-docker.test.mjs (live revoke against scratch Gitea 1.27.1: old token 401 at Gitea), plus additions in copy-launch, repo-remote, CLI org-delete and org-restore-bus, and Console org-admin. Red before: copy-revoke cannot load on BASE; with the module copied in, 0/34. Crash injection at eight points, each followed by a restart. Mutation tables are in the lane report. Full gate on the head: 8325/2. Both failures are the known CHAT-01 jsonschema base reds in packages/conversation, which this branch does not touch. The Lead check ran separately under env -i.
Reviews (fresh Opus delegates): build review changes 8, security review changes 4, all fixed. The security re-check found one P2 (project.remote.set could change the repository under a live binding), fixed in the last commit. The Lead read that fix: 9 source lines, plus a test and a mutant (F11) that it kills.
Deferred:
Tests for re-check P3-1 (shadow-loop isolation), P3-2 (runtime wiring) and P3-3 (the ordinary run path).
Kill re-send has no backoff.
The binding does not record the bound origin when the credential is issued (suggested as defense in depth).
Credential binding slice CB4 (#1873): copy credential lifecycle and recovery. Design: PR #1923, approved by Jason on 11-11 at 17:55Z with CB0 to CB5 as written. Rulings in force: org.hire is the entry right, binding approvals are owner-only, there is no org.credentials right, and archive discards copy credentials (Q3).
- `packages/bus/src/copy-revoke.mjs` (new): one revoke job per copy for every lifecycle event, in this order: close the gate, record the pending op, drain in-flight requests, stop every run and wait for each ack, remove the slot, revoke the token at Gitea, then finish. Only a definite finish reopens the gate, and then only to the tombstone's own refusal code.
- Durable pending op (`copy-bindings.mjs`, `organization.mjs`): new binding state `revoking` and a `revoke` record with op id, cause, time, `failed` steps and per-service `remote` outcomes. A revoking record never resolves. It answers `credential-revoking`, or `credential-revoke-failed` after an uncertain step.
- Restart and sweep (`runtime.mjs`): at start, `resume` finishes each pending op under its own id before anything can use the copy. `sweep` checks each copy holding a grant at start and every 60 s, for expiry, slot loss, retire, role change, collision and shadowing.
- Gate triggers (`copy-launch.mjs`): the CB3 gate reports a permanent loss through `onLoss`, which starts the job.
- Remote step: Gitea revoke goes through the organization's managed admin slot. 204 is `revoked`, 404 is `absent`. With no admin slot, or one on another server, the outcome is `not-managed`: the copy is tombstoned and denied, but the token stays valid at Gitea until it expires, and the record never claims it was revoked. Any uncertain answer is `failed` and keeps the copy fenced for the next pass.
- Archive and delete (`org-service.mjs`, CLI `org-bus.mjs`, Console `org-admin.mjs`): archive (Q3) and delete revoke each copy's grant through the same step. Receipts list each copy's remote outcome and never claim a revoke that did not happen. Restore brings no copy credential back.
- Repository guard: the owner's `updateOrg` and `project.remote.set` refuse `project-repo-bound` while a copy binding still grants the project (pending, bound or revoking). This is a new refusal on existing verbs; the code itself already existed (F7).
- Refusal table: a new `org-archived org-unavailable` group for copies, with a next step that names no path.
Nothing in `roles/` or `contracts/`.
Tests: `copy-revoke.test.mjs` (new), `copy-revoke-docker.test.mjs` (live revoke against scratch Gitea 1.27.1: old token 401 at Gitea), plus additions in copy-launch, repo-remote, CLI org-delete and org-restore-bus, and Console org-admin. Red before: copy-revoke cannot load on BASE; with the module copied in, 0/34. Crash injection at eight points, each followed by a restart. Mutation tables are in the lane report. Full gate on the head: 8325/2. Both failures are the known CHAT-01 jsonschema base reds in packages/conversation, which this branch does not touch. The Lead check ran separately under `env -i`.
Reviews (fresh Opus delegates): build review changes 8, security review changes 4, all fixed. The security re-check found one P2 (`project.remote.set` could change the repository under a live binding), fixed in the last commit. The Lead read that fix: 9 source lines, plus a test and a mutant (F11) that it kills.
Deferred:
- Tests for re-check P3-1 (shadow-loop isolation), P3-2 (runtime wiring) and P3-3 (the ordinary run path).
- Kill re-send has no backoff.
- The binding does not record the bound origin when the credential is issued (suggested as defense in depth).
Every lifecycle event that ends a copy's authority (unbind, emergency
revoke, slot removal or expiry, role change, fire, collision, shadowing,
archive, delete and recreate) now closes admissions, drains in-flight
uses, stops runs with a confirmed ack, removes the slot and revokes the
Gitea token through the org admin slot. The binding moves through a
durable revoking state, so a restart resumes the same op and never
resurrects authority; an uncertain outcome stays fenced.
Build review (changes 8) and security review (changes 4):
- P1-1: an owner-approved fire no longer writes the revoke before the
apply (decision-stale). The fire stops the copy's runs, the fire is
written, then the retire revoke starts for the fired instance.
- P2 (both): archive and delete from the CLI and the Console pass the
organization's managed Gitea remote step (copyRemoteFor). A remote
step that was not attempted or failed keeps the record revoking with
failed remote, so the bus finishes it after restore; no done tombstone
over a live token.
- Security P2-1: a bound server that cannot be read is a failed remote
step, not not-managed. updateOrg refuses project-repo-bound when a
project a copy binding grants loses or changes its remote or git.
- P3-1 (security): a delete whose organization file cannot be read says
copiesUnreadable, never an empty copy list.
- P3-2 (security): the remote step requires the record's incarnation to
equal the registry's live one.
- P3-5 (build): the archive's slot discard checks each parent directory
(no symlink, own uid, private mode).
- P3-6 (build): resume keeps credential-revoke-failed as the gate code
while a failed op runs again.
- P3-4 (build): a test pins the refused-begin reopen guard.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Credential binding slice CB4 (#1873): copy credential lifecycle and recovery. Design: PR #1923, approved by Jason on 11-11 at 17:55Z with CB0 to CB5 as written. Rulings in force: org.hire is the entry right, binding approvals are owner-only, there is no org.credentials right, and archive discards copy credentials (Q3).
packages/bus/src/copy-revoke.mjs(new): one revoke job per copy for every lifecycle event, in this order: close the gate, record the pending op, drain in-flight requests, stop every run and wait for each ack, remove the slot, revoke the token at Gitea, then finish. Only a definite finish reopens the gate, and then only to the tombstone's own refusal code.copy-bindings.mjs,organization.mjs): new binding staterevokingand arevokerecord with op id, cause, time,failedsteps and per-serviceremoteoutcomes. A revoking record never resolves. It answerscredential-revoking, orcredential-revoke-failedafter an uncertain step.runtime.mjs): at start,resumefinishes each pending op under its own id before anything can use the copy.sweepchecks each copy holding a grant at start and every 60 s, for expiry, slot loss, retire, role change, collision and shadowing.copy-launch.mjs): the CB3 gate reports a permanent loss throughonLoss, which starts the job.revoked, 404 isabsent. With no admin slot, or one on another server, the outcome isnot-managed: the copy is tombstoned and denied, but the token stays valid at Gitea until it expires, and the record never claims it was revoked. Any uncertain answer isfailedand keeps the copy fenced for the next pass.org-service.mjs, CLIorg-bus.mjs, Consoleorg-admin.mjs): archive (Q3) and delete revoke each copy's grant through the same step. Receipts list each copy's remote outcome and never claim a revoke that did not happen. Restore brings no copy credential back.updateOrgandproject.remote.setrefuseproject-repo-boundwhile a copy binding still grants the project (pending, bound or revoking). This is a new refusal on existing verbs; the code itself already existed (F7).org-archived org-unavailablegroup for copies, with a next step that names no path.Nothing in
roles/orcontracts/.Tests:
copy-revoke.test.mjs(new),copy-revoke-docker.test.mjs(live revoke against scratch Gitea 1.27.1: old token 401 at Gitea), plus additions in copy-launch, repo-remote, CLI org-delete and org-restore-bus, and Console org-admin. Red before: copy-revoke cannot load on BASE; with the module copied in, 0/34. Crash injection at eight points, each followed by a restart. Mutation tables are in the lane report. Full gate on the head: 8325/2. Both failures are the known CHAT-01 jsonschema base reds in packages/conversation, which this branch does not touch. The Lead check ran separately underenv -i.Reviews (fresh Opus delegates): build review changes 8, security review changes 4, all fixed. The security re-check found one P2 (
project.remote.setcould change the repository under a live binding), fixed in the last commit. The Lead read that fix: 9 source lines, plus a test and a mutant (F11) that it kills.Deferred: