diff --git a/docs/architecture/C1-repair-job-authority.md b/docs/architecture/C1-repair-job-authority.md index 7d07781..bc9fabc 100644 --- a/docs/architecture/C1-repair-job-authority.md +++ b/docs/architecture/C1-repair-job-authority.md @@ -267,12 +267,22 @@ mutation will advance, and the boundary must bind the two before it acts: The boundary must bind that effective `HEAD` referent to the authorized repair ref and refuse to commit if the worktree is detached, attached to the protected parent, attached to any other ref, or its safe binding cannot be established. -- **`repair.push`.** The authorized repair ref identifies a source, not a - destination. The boundary must bind the push's **effective destination ref** to - the authorized repair ref and must not let a caller-selected destination refspec - redirect the push; the receiving/mutation side must fail closed if the effective - destination is the protected parent or cannot be proven to be the authorized - repair ref. +- **`repair.push`.** A push carries both a source and a destination ref, and the + authorized repair ref governs **both**. The boundary must bind the push's + **effective source ref** and its **effective destination ref** — each by its + effective ref-name referent, not by commit-object identity — to the authorized + repair ref, and must not let a caller-selected source or destination refspec + redirect either half. The source must be **present**: an absent source, the + deletion refspec `:refs/heads/…`, is not a `repair.push` at all but a + `branch.delete`, which is denied, so a destination that still names the repair + ref does not make it authorized. No alternate branch, tag, or commit-ish may + stand in for the authorized repair ref on either half. The receiving/mutation + side must fail closed if either effective half is the protected parent, is not + provably the authorized repair ref, or ceases to be between the check and the + push — the authorized source-to-destination relationship must hold through to + that consuming boundary, not only at an earlier pre-check. An ordinary + `refs/heads/repair:refs/heads/repair` push, whose effective source and + destination are both the authorized repair ref, remains authorized. *Operands that set direction without mutating a ref.* The obligation is not limited to ref-mutating operations. `repair.change_request` mutates no ref, but diff --git a/src/domain/repair-job.ts b/src/domain/repair-job.ts index 8287b5b..272885b 100644 --- a/src/domain/repair-job.ts +++ b/src/domain/repair-job.ts @@ -524,10 +524,15 @@ function endsWithDotLockSuffix(value: string, start: number, end: number): boole * records, resolve the requested ref's effective ref-name referent against the * actual repository — the terminal ref reached through a symbolic-ref chain, not * commit-object identity, since a fresh repair branch may legitimately share the - * protected parent's commit OID — and bind the effective mutation target it will - * actually advance: the worktree's effective `HEAD` referent for a commit, and the - * effective destination ref for a push. It must **fail closed** — refusing the - * operation — if that effective mutation target is or dereferences to the + * protected parent's commit OID — and bind each effective identity the operation + * acts on to the authorized repair ref. A commit advances one mutation target: the + * worktree's effective `HEAD` referent, the ref it will actually advance. A push + * binds two effective identities in distinct roles — its destination ref, the + * receiving/mutation target the push advances, and its source ref, the input the + * push consumes to select what is sent — each of which must be the authorized + * repair ref, so an absent (deletion) or redirected source is refused. It must + * **fail closed** — refusing the + * operation — if any such effective identity is or dereferences to the * protected parent, if it changes between comparison and update, or if it cannot * be safely established; a resolve-then-act pre-check is not itself atomic, so the * invariant is enforced at the boundary that actually consumes each identity — the