Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 140 additions & 21 deletions docs/architecture/C1-repair-job-authority.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,13 +217,119 @@ filesystem-dependent collision that is characterisable from the strings alone; i
establishes nothing about symbolic refs, which are not decidable from a string at
all.

**A future trusted repository/Git execution boundary must close the rest.** Before
exercising any ref-mutating authority represented by an `ExecutionPermit`, that
boundary must resolve the requested ref against the actual repository, resolve or
reject repository-dependent symbolic refs, and establish that the repair ref's
resolved target is not the protected parent's. It must **fail closed** — refuse
the operation — if the requested repair ref resolves or dereferences to the
protected parent, or if safe target identity cannot be established at all.
**A future trusted repository/Git execution boundary must close the rest.** C1
establishes structural canonical ref-name authority; it cannot establish live
repository identity, cannot bind the target a mutation will actually reach, and
cannot enforce anything across a concurrent change. Before acting on any authority
an `ExecutionPermit` records — not only operations that write a ref — that
boundary must satisfy the requirements below, and must **fail closed** — refuse
the operation — wherever a required identity cannot be safely established,
wherever resolution cycles or is otherwise indeterminate, or wherever an effective
identity is or dereferences to a ref the operand's role is not authorized to
denote.

*The protected-parent rule is role-bound.* Protected-parent identity is forbidden
only where it is unauthorized for the operand's role — which is every role but
one. The effective mutation target of a `repair.commit` or a `repair.push`, and a
`repair.change_request` `sourceRef`, must each be the authorized repair ref, so
for all three an effective identity that is or dereferences to the protected
parent is a refusal. A `repair.change_request` `targetRef` is the single operand
whose *required* effective identity **is** the protected parent ref — the same
operand C1's string layer already singles out as the only one that may name it —
so for that role, and only that role, reaching the protected parent is the
authorized outcome and reaching anything else is the refusal. Stated role-blind
instead, the rule would forbid the one direction the quarantine depends on. No
role widens past this: an operand authorized to denote the protected parent as a
change-request *target* acquires no authority to denote it anywhere else, and the
exemption never reaches an operand that would mutate the parent.

*Which identity is compared.* The isolation question is about the **effective
ref-name referent** — the terminal ref reached by resolving a symbolic-ref chain —
not about commit-object identity. A freshly created repair branch may legitimately
point at the **same commit object** as the protected parent until its first repair
commit, so distinct commit OIDs are neither necessary nor sufficient: two
different branch refs may share one commit OID, and commit-object equality does not
make two refs the same authority target. The boundary must therefore compare
effective ref-name referents, and must not rest the check on whether two refs
currently resolve to the same commit. What that comparison must *yield* is fixed
by the operand's role: for an operand whose required identity is the authorized
repair ref, a symbolic or effective ref-name identity that aliases the protected
parent must be detected and rejected; for the one operand whose required identity
is the protected parent ref — the `repair.change_request` `targetRef` — the alias
to detect and reject is the converse one, an effective identity that is not the
protected parent ref.

*Binding the effective mutation target.* A resolved ref *name* is not the target a
mutation will advance, and the boundary must bind the two before it acts:

- **`repair.commit`.** A commit advances the branch reached through the authorized
worktree's effective `HEAD` referent, not whatever ref name the request carried.
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`.** 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
its `sourceRef` and `targetRef` fix the effective direction of the stacked
validation request, which the quarantine requires to run **from** the repair
branch **to** the protected parent. The boundary must establish that the effective
source identity is the authorized repair ref and the effective target identity is
the protected parent ref, reject a symbolic or effective alias that changes that
direction, and fail closed if either effective identity cannot be safely
established. Because this operation performs no ref update to guard, the boundary
that consumes these identities is the change-request/provider creation — or
update — request itself, and the established source and target identities must be
**bound through to that provider request**: the provider must create the request
from exactly the authorized effective source and target. Provider-side resolution
of the supplied ref names is not itself forbidden — a create/update API may have
to resolve the source and target names against its own authoritative repository
state — but it must yield exactly those authorized effective identities: it must
not let re-resolution, ambient repository state, or any substitution cause the
request to be created from, or to consume, a **materially different** effective
source or target than the one authorized. Resolution that preserves the exact
authorized source-to-target relationship conforms; resolution that would consume
a materially different effective identity does not. If that authorized
source-to-target relationship cannot be maintained through to the provider
request — because an effective identity has changed, cannot be safely
re-established, or cannot be shown equivalent to the authorized one at that
boundary — the boundary must fail closed and create no change request.

*Concurrency is not closed by a pre-check.* A resolve-then-check-then-act
sequence is **not** an atomic security guarantee: an effective ref or referent can
change between the comparison and the moment the identity is consumed, so a name
observed as an ordinary repair ref can become symbolic to the protected parent —
or a target can cease to denote it — after the check and before the act. The
invariant must be enforced **at the actual trusted execution boundary that
consumes each identity, not only where a ref is mutated**, by a mechanism whose
semantics prevent an unchecked identity change between the comparison and that
consumption — not by an earlier client-side observation the boundary later trusts.
That consuming boundary differs by operation and the obligation is identical at
each: for `repair.commit` it is the commit mutation boundary, for `repair.push`
the push receiving/mutation boundary, and for `repair.change_request` — which
mutates no ref — the change-request/provider creation boundary at which the source
and target identities are actually consumed. An operation whose authorized
effective-identity relationship cannot be held through to its consuming boundary
must fail closed.

This document states the required invariant, not an implementation: it names no
git command, lock, or transaction mechanism, and it claims no more atomicity than
the eventual executor's own primitives can actually provide.

Writing that obligation down adds no runtime git authority to C1 and grants no new
authority anywhere: C1 gains no git invocation, no filesystem access, no
Expand All @@ -244,11 +350,14 @@ authority cannot be checked against an exact operand has no place in the model.
| `repair.push` | ref, non-force | ref is exactly the repair branch and the push is not forced |
| `repair.change_request` | source ref, target ref | repair branch → protected parent ref |

Every ref operand is read through the same canonical branch-ref reader the job
envelope uses, so "exactly the repair branch" is a claim about a canonical ref
name and not about a caller's chosen spelling. It is not a claim about what that
name resolves to in a repository, which only the later trusted execution boundary
can establish.
Every ref operand — the `repair.commit` and `repair.push` ref, and the
`repair.change_request` source and target refs alike — is read through the same
canonical branch-ref reader the job envelope uses, so "exactly the repair branch"
is a claim about a canonical ref name and not about a caller's chosen spelling. It
is not a claim about the effective ref-name referent that name reaches in a
repository, about which ref a commit or push would actually advance, or about the
effective direction of a change request — all of which only the later trusted
execution boundary can establish.

`repair.change_request` is the **only** operation that may name the protected
parent ref, and only as a change-request *target*. Opening a change request
Expand Down Expand Up @@ -454,12 +563,19 @@ actually holds:
Forgery therefore buys nothing, and a permit widens no authority — it records
authority already derived from trusted configuration.

A permit is also **not a repository-safety finding**. That a `repair.commit` or
`repair.push` ref operand passed C1's canonical syntax validation says nothing
about what that ref resolves to in the repository the operation would touch, so a
permit must never be read as proof that repository-level ref resolution is safe.
The trusted execution boundary that acts on a permit performs its own resolution
and fails closed; see *What canonical ref names do and do not prove* above.
A permit is also **not a repository-safety finding**. That a ref operand — a
`repair.commit` or `repair.push` ref, or a `repair.change_request` `sourceRef` or
`targetRef` — passed C1's canonical syntax validation says nothing about the
effective ref-name referent it reaches in the repository the operation would
touch, about which ref a commit or push would actually advance, or about the
effective direction of a change request, so a permit must never be read as proof
that repository-level ref identity, the effective mutation target, or the
change-request direction is safe. The trusted execution boundary that acts on a
permit binds the effective identity, resolves it, and enforces it at the boundary
that actually consumes that identity — the mutation/receiving boundary for a
`repair.commit` or `repair.push`, and the change-request/provider creation boundary
for a `repair.change_request` — and fails closed; see *What canonical ref names do
and do not prove* above.

### Single use

Expand Down Expand Up @@ -584,9 +700,12 @@ invariants that stop a later layer from bypassing the quarantine by accident:
- The protected parent ref is never a write target of any operation, under any
*spelling*: refs are canonical everywhere, so a caller cannot present a textual
alias of the parent as a different branch. Repository-dependent aliasing — a
canonical repair ref that is symbolic to the parent — is not visible to a pure
string boundary, and is the later trusted execution boundary's to resolve or
reject before any ref-mutating operation runs.
canonical repair ref that is symbolic to the parent, a worktree `HEAD` or push
destination whose effective target is the parent, a change-request source or
target whose effective direction is reversed, or an effective ref that changes
after a pre-check — is not visible to a pure string boundary, and is the later
trusted execution boundary's to bind, resolve, or reject before it acts on any
authority a permit records.
- Filesystem-shaped operations are bound to the repair worktree, so an edit
cannot land in the parent's checkout.
- The stacked change request must run from the repair branch to the protected
Expand Down
51 changes: 39 additions & 12 deletions src/domain/repair-job.ts
Original file line number Diff line number Diff line change
Expand Up @@ -518,15 +518,39 @@ function endsWithDotLockSuffix(value: string, start: number, end: number): boole
* than pretended away here. C1 observes no filesystem and cannot do better
* than refuse the ambiguous case.
*
* The symbolic-ref residue is not narrowable from a string at all. A later
* trusted repository/Git execution boundary must resolve the requested ref
* against the actual repository and **fail closed** — refusing the operation —
* before exercising any ref-mutating authority carried by an `ExecutionPermit`,
* if the repair ref resolves or dereferences to the protected parent or if safe
* target identity cannot be established. Nothing here acquires git invocation,
* filesystem access, a subprocess, or network to decide it. See
* `docs/architecture/C1-repair-job-authority.md`, "What canonical ref names do
* and do not prove".
* The symbolic-ref residue is not narrowable from a string at all, and neither is
* the effective target a mutation would reach. A later trusted repository/Git
* execution boundary must, before acting on any authority an `ExecutionPermit`
* 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 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
* mutation/receiving boundary for a commit or push, and the
* change-request/provider creation boundary for a change request. That refusal is
* bound to the operand's role rather than being a blanket ban on the protected
* parent's identity: a `repair.change_request` `targetRef` is *required* to reach
* the protected parent ref, and fails closed when it reaches anything else. Because
* that operation mutates no ref, the identity it consumes is bound at its provider
* create/update request: the effective source must remain the authorized repair ref
* and the effective target the protected parent ref through to that request. The
* provider may resolve those ref names at its own boundary, but must not let
* re-resolution or ambient repository state substitute a materially different
* effective identity for either end, and the boundary must fail closed if the
* authorized relationship cannot be maintained or shown equivalent there.
* Nothing here acquires git invocation, filesystem access, a subprocess, or
* network to decide it. See `docs/architecture/C1-repair-job-authority.md`,
* "What canonical ref names do and do not prove".
*
* The value is returned exactly as supplied, or not at all. No normalisation,
* no prefixing, no case folding: a boundary that repaired the spelling would be
Expand Down Expand Up @@ -596,8 +620,11 @@ export function readCanonicalBranchRef(value: unknown): string | null {
* A `false` result means only that the two names are distinct under this rule.
* It is **not** a finding that they denote distinct targets in a repository: this
* compares strings and resolves nothing, so a canonical name that is a symbolic
* ref to the other still answers `false` here. Repository-resolved identity is
* the later trusted repository/Git execution boundary's to establish; see
* ref to the other still answers `false` here, and — since two different branch
* refs may legitimately share one commit object — commit-object equality is not
* the question either. Repository-resolved identity, meaning the effective
* ref-name referent reached by resolving a symbolic-ref chain, is the later
* trusted repository/Git execution boundary's to establish; see
* {@link readCanonicalBranchRef}.
*/
function mayDenoteSameBranchRef(left: string, right: string): boolean {
Expand Down Expand Up @@ -755,7 +782,7 @@ export interface RepairJobAuthorization {
* that the two names resolve to distinct targets in a repository — a canonical
* repair ref that is symbolic to the parent is invisible to a pure string
* boundary — which the later trusted repository/Git execution boundary must
* resolve or reject before any ref-mutating operation runs. See
* resolve or reject before it acts on any authority a permit records. See
* {@link readCanonicalBranchRef}.
*/
readonly repairBranch: string;
Expand Down
Loading