identity: #549's answer is the field table that already shipped, plus the door test it was missing - #585
Merged
Conversation
…nd table CheckText has exactly one non-test call site, internal/policy/resolve.go, in Resolve's profile loop. Two tests already covered it and neither holds the door for a leaf that does not exist yet: TestCheckTextRefusesAForgingRuneAtEveryIdentityLeaf drives identityFields, so it grows with the struct, but calls id.CheckText directly and says nothing about whether resolution reaches it. TestIdentityFieldsRefuseControlCharacters does go through Resolve, with the real config-injection payloads, but its table is hand written: seven rows for the eight leaves Identity has today. So an eighth leaf added tomorrow is refused by neither on the path a profile actually takes, which is the gap issue #549 names in one sentence: a new identity field must not be expressible without passing through CheckText. The new test drives identityFields through Resolve and asserts the refusal carries CheckText's own wording. That last assertion is not decoration. identity.ssh.host is independently refused by the host-pairing rule when gh.host is unset, so with the door removed that leaf still fails to resolve -- for a reason that has nothing to do with a forging rune. Measured by deleting the CheckText call: all eight subtests fail, and ssh.host fails on the wording assertion rather than on err == nil. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Section 2 is the table a reader consults to answer which credential is inside the sandbox and which code puts it there. Two of its three code citations were false: internal/cli/identity.go:330 was offered as the oauth_token: write. That line is a pol.Replace inside the allowed_signers staging issue #453 inserted above it; the real write is :471, in stageGhConfig. internal/policy/gitextract.go:131 was offered as the insteadOf rewrite. That line is the id.Git.Email override; the real rewrite is :176, in GitConfigFrom. Neither edit touched the table, nothing failed, and both citations read as precise. A line number is a copy of state held in the file it names, so the fix is to cite the symbol, which moves with the code. The test holds both halves: the symbol a row names is really in the file it names, and no row carries a file.go:NNN at all. Scope is this one table on purpose -- the rest of .claude/design cites lines in hundreds of places, and sweeping them is a change of its own. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Answers #549's decision, and the answer is option 2, already shipped. Nothing here redesigns anything; it closes the one gap left over and corrects the design doc the ticket is about. Everything measured at
1487aec, not at the ticket'sdedaf7a.The decision
Option 2 — a structural field table — is the ruling, and #454 built it.
internal/policy/identity.go:The type is the table.
CheckTextwalks it,Resolve's expand-and-symlink loop walks it,mustIdentityFieldspanics at package init on a missingsnug:tag,TestIdentityHasNoReferenceKindedFieldwalks the type independently, andinternal/profile/identitynested_test.goasserts the TOML decode structs agree leaf for leaf. The ticket's acceptance criterion —— is what that derivation buys, and the comment above it says so.
Empirical confirmation: #453 was the ticket's own test case. It called a signing key "the seventh field" and blocked it on this decision. It landed anyway, as
git.signing_key(41192d0, #573), with no seam, no adapter registry, and no edit to the hand-maintained injection list the ticket calls site 2 — because that list stopped being hand-maintained inb994603(#575).Option 1 — a real adapter seam — is refused. Not on cost. On the set:
SECRETS.md§6ghhas no credentials inside"A seam abstracts "acquire on the host, filter through a whitelist, author a file inside". Three of the four adapters are slated to stop doing that. Building the abstraction now fixes the shape of a set that is not staying at four and is not staying this shape.
§4.1 already refuses the mechanism on its own grounds — "There is no adapter mechanism, and that is deliberate", with a bar in place of a registry, because an adapter runs on the HOST and a plugin layer loads from a config path
XDG_CONFIG_HOMEcan repoint at a checked-out repository.Option 3 — documentation only — is already met. §2 tabulates what is inside today with its code; §4.1 states the absence of a mechanism as a decision rather than a gap. That is the ticket's "documented as adhoc rather than presented as a design".
The ticket body is stale in four places
SECRETS.mdis 1055 lines, not 2271. TheStatus: DRAFT. Nothing here is built.line it quotes at line 3 is gone. §5's D3, which option 1 was said to overturn, no longer exists — the pruning pass took §5 with it, so option 1 needs no overturning; §4.1 is what stands. And its blocker #453 is closed.What this PR actually changes
1. The
CheckTextdoor (internal/policy/identitytext_test.go)CheckTexthas exactly one non-test call site, inResolve's profile loop. Two tests cover it and neither holds it for a leaf that does not exist yet:TestCheckTextRefusesAForgingRuneAtEveryIdentityLeafdrivesidentityFields, so it grows with the struct — but callsid.CheckTextdirectly and never reachesResolve.TestIdentityFieldsRefuseControlCharactersgoes throughResolvewith the real config-injection payloads — but its table is hand-written, ten rows over seven of the eight leaves.So an eighth leaf is held by neither on the path a profile takes.
The new
TestEveryIdentityLeafIsRefusedThroughResolvedrivesidentityFieldsthroughResolveand asserts the error carriesCheckText's own wording. That last assertion is load-bearing, not decoration. Measured with theCheckTextcall deleted — all eight subtests fail, and three of them fail on the wording rather than onerr == nil:refuseHalfNamedHostandParseSSHModerefusessh.host,gh.hostandssh.agenton their own, so a bareerr != nilon those three keeps passing after the door is removed. Same measurement on the two existing tests: the direct-CheckTextone passes entirely with the door gone; the hand table fails 7 of its 10 rows and passes 3, for the same reason —gh.hosttrips the host-pairing refusal,git.signing_key's payload leavesagentunset so it trips thesigning_keyneedsagent = "proxy"refusal, andssh.agent's"none\n"failsParseSSHMode.Both existing tests are unchanged. The hand table carries the real payloads (
x\n[core]\n\tsshCommand = evil, the C1 and bidi rows) and is worth keeping as written.2.
SECRETS.md§2 cited two lines that had movedSection 2 is the table a reader consults to answer which credential is inside and which code puts it there. Two of its three citations were false:
internal/cli/identity.go:330was offered as theoauth_token:write. That line is apol.Replaceinside theallowed_signersstaging [identity] ssh_key cannot express a signing key: git signs with a different key than it authenticates with #453 inserted above it. Real write::471, instageGhConfig.internal/policy/gitextract.go:131was offered as theinsteadOfrewrite. That line is theid.Git.Emailoverride. Real rewrite::176, inGitConfigFrom.Neither edit touched the table, nothing failed, both read as precise. Fixed to symbol citations, which move with the code.
internal/cli/secretstable_test.goholds both halves — the symbol a row names is in the file it names, and no row carries afile.go:NNNat all:Scope is that one table. The rest of
.claude/designcites lines in hundreds of places and sweeping them is a change of its own.Verification
make gategreen. No production code changes — tests and a design document — so no golden argv diff and no red-team round: this touches no profile, no mount generation, no seccomp filter and no host-integration surface.Closes #549.
🤖 Generated with Claude Code