-
Notifications
You must be signed in to change notification settings - Fork 0
C1-A04 follow-up — Clarify symbolic-ref resolution boundary #35
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
c743b65
fc49e9a
afba343
c8fde78
bf78cf6
7810d6e
0bef03c
838d8ed
077933d
6812607
1fa4cf6
6fa3e95
7444080
467ccc3
7a3e53b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -121,15 +121,18 @@ offending names in declaration order. | |
| Two structural invariants are enforced as configuration validity rather than as | ||
| a runtime check that could be forgotten: | ||
|
|
||
| - `repairBranch` and `protectedParentRef` denote **different branches**. A job | ||
| whose repair branch *is* the protected parent is not a quarantined repair; it | ||
| is a direct write to protected history wearing a repair job's name. | ||
| - `repairBranch` and `protectedParentRef` are **different branch refs** under | ||
| C1's canonical comparison rule. A job whose repair branch *is* the protected | ||
| parent is not a quarantined repair; it is a direct write to protected history | ||
| wearing a repair job's name. This compares canonical ref *names*; establishing | ||
| that two accepted names resolve to distinct targets in a repository is the | ||
| later trusted execution boundary's obligation, described below. | ||
| - `independentValidatorId !== repairAgentId`. A repair agent that is its own | ||
| validator defeats the quarantine the whole pipeline exists to enforce. | ||
|
|
||
| ### Branch refs have exactly one accepted spelling | ||
|
|
||
| "Different branches", not "different strings". Git resolves `main`, | ||
| "Different branch refs", not "different strings". Git resolves `main`, | ||
| `heads/main`, and `refs/heads/main` to one and the same ref, so a boundary that | ||
| compares ref strings has three names for one authority target. Configuring | ||
| `protectedParentRef: 'refs/heads/main'` beside `repairBranch: 'main'` would | ||
|
|
@@ -149,14 +152,18 @@ and refuses every other spelling of the same branch as malformed: | |
| - no segment beginning or ending with `.`, no `..` anywhere, and no segment | ||
| ending in `.lock` in any ASCII case | ||
|
|
||
| The property that buys: **two accepted refs denote the same branch if and only if | ||
| they are equal strings.** That is what makes the distinctness invariant mean | ||
| something. The conservative ASCII character set is part of the guarantee, not a | ||
| convenience — it removes Unicode normalisation, under which an NFC and an NFD | ||
| spelling of one name are unequal strings a filesystem-backed loose ref can | ||
| resolve to a single ref, and it removes `~`, `^`, `:`, `?`, `*`, `[`, `\`, `@{`, | ||
| and whitespace in one rule. Nothing is normalised, prefixed, or case-folded on | ||
| the way in: a value is accepted exactly as supplied or refused. | ||
| The property that buys is a property of ref *names*, not of repository state: | ||
| **two accepted refs are the same canonical ref name if and only if they are equal | ||
| strings.** That is what closes caller-controlled textual aliasing and what makes | ||
| the distinctness invariant mean something at this layer. It is not a claim that | ||
| two unequal canonical names denote two distinct targets in a repository; see | ||
| *What canonical ref names do and do not prove* below. The conservative ASCII | ||
| character set is part of the guarantee, not a convenience — it removes Unicode | ||
| normalisation, under which an NFC and an NFD spelling of one name are unequal | ||
| strings a filesystem-backed loose ref can resolve to a single ref, and it removes | ||
| `~`, `^`, `:`, `?`, `*`, `[`, `\`, `@{`, and whitespace in one rule. Nothing is | ||
| normalised, prefixed, or case-folded on the way in: a value is accepted exactly | ||
| as supplied or refused. | ||
|
|
||
| The same reader is applied to **every** security-relevant ref position — the two | ||
| job fields, and the `ref`, `sourceRef`, and `targetRef` request operands — so | ||
|
|
@@ -165,14 +172,6 @@ uncanonical request operand. A supplied operand that is not canonical is refused | |
| `REF_MALFORMED` before any comparison, rather than compared as though it were a | ||
| different branch. | ||
|
|
||
| **What this does not prove**, and must not be claimed to: that two unequal | ||
| accepted refs are two distinct refs on every filesystem. Git stores loose refs as | ||
| files, so on a case-insensitive filesystem `refs/heads/Main` and | ||
| `refs/heads/main` can be one ref while comparing unequal. C1 observes no | ||
| filesystem, so it refuses the ambiguous case instead of pretending it away: the | ||
| job's two configured refs are additionally compared with ASCII case folded, and a | ||
| pair that differs only by case is rejected as malformed configuration. | ||
|
|
||
| One relationship is enforced at authorization time, because it is about | ||
| freshness rather than shape: `findingHeadSha` must equal `parentHeadSha`, or | ||
| every operation is denied `FINDING_SHA_STALE`. A repair derived from a finding | ||
|
|
@@ -181,6 +180,169 @@ PR 004 remains the owner of `CURRENT` versus `STALE` for evidence; this is the | |
| narrower structural check that the job's own two SHAs agree, which C1 can decide | ||
| without importing that kernel or producing a second answer to PR 004's question. | ||
|
|
||
| ### What canonical ref names do and do not prove | ||
|
|
||
| Stated precisely, because overclaiming here would be worse than not checking. | ||
|
|
||
| **Proved.** An accepted value is a string in the one canonical `refs/heads/<name>` | ||
| shape, and two accepted values that remain unequal under C1's documented | ||
| comparison rule are two different canonical ref names. Caller-supplied textual | ||
| aliasing is closed within that structural authority: the originally proven bypass | ||
| — configuring or requesting `main`, `heads/main`, and `refs/heads/main` against | ||
| one another so the protected parent could be presented as a different branch — is | ||
| refused as `REF_MALFORMED` before any comparison, and a configured pair that | ||
| collides under the comparison rule invalidates the job rather than authorizing it. | ||
|
|
||
| **Not proved, and not claimed.** That two different canonical ref names are two | ||
| distinct branch targets in a repository. C1 does not establish repository-resolved | ||
| ref identity, does not detect whether an accepted ref is symbolic, does not | ||
| resolve a symbolic ref's target, does not determine whether two distinct canonical | ||
| names ultimately dereference to the same repository target, and observes no live | ||
| repository state. Two independent reasons stand: | ||
|
|
||
| - **Symbolic refs.** A repository may hold a canonical-looking ref — say | ||
| `refs/heads/repair` — that is itself a symbolic ref to `refs/heads/main`. | ||
| Whether such a ref exists, and what it points at, is repository state at the | ||
| moment the name is used. C1 runs no git, spawns no subprocess, opens no file, | ||
| and observes no repository, so no string comparison it performs can decide it. | ||
| - **Filesystem identity.** Git stores loose refs as files, so on a | ||
| case-insensitive filesystem `refs/heads/Main` and `refs/heads/main` can be one | ||
| ref while comparing unequal. C1 observes no filesystem, so it refuses the | ||
| ambiguous case instead of pretending it away: the job's two configured refs are | ||
| additionally compared with ASCII case folded, and a pair that differs only by | ||
| case is rejected as malformed configuration. | ||
|
|
||
| The case fold is a conservative refusal, not a resolution. It narrows one | ||
| filesystem-dependent collision that is characterisable from the strings alone; it | ||
| 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.** 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. A terminal ref-name *spelling* is not by | ||
| itself repository ref identity: where a repository applies its own ref-identity | ||
| semantics — for instance a case-insensitive ref store under which | ||
| `refs/heads/Main` and `refs/heads/main` are one ref — two terminal names that are | ||
| not equal strings may still be the same repository ref, so the boundary must | ||
| decide whether two effective referents are the same or distinct under that | ||
| repository's actual ref-identity semantics rather than by terminal-name string | ||
| (in)equality alone, and must fail closed wherever the required distinctness cannot | ||
| be safely proven under those semantics. 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 | ||
|
Comment on lines
+287
to
+291
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
For a remote push, the receiving/mutation side cannot prove the source ref's effective identity or whether it changed: the sender resolves the source and transmits an update containing the old OID, new OID, and destination ref, not the source ref name. I checked Git 2.43's documented syntax, Useful? React with 👍 / 👎. |
||
| `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 | ||
| subprocess, and no network, and remains pure TypeScript. | ||
|
|
||
| ## Operations are structured, not named | ||
|
|
||
| A generic action name is not sufficient for Cockpit write authority. There is no | ||
|
|
@@ -196,9 +358,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 branch and not | ||
| about a spelling. | ||
| 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 | ||
|
|
@@ -404,6 +571,20 @@ 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 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 | ||
|
|
||
| Single use is stated structurally. `singleUse` is typed as the literal `true` and | ||
|
|
@@ -525,8 +706,14 @@ C1 implements none of that workflow. It encodes only the minimal authority | |
| 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 an alias of the parent cannot be | ||
| presented as a different branch. | ||
| *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, 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 | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.