One credential-store URL predicate: stop hand-synchronising five spellings of one grammar #29
Labels
No labels
blocked
bug
decision
duplicate
enhancement
handoff
help wanted
invalid
landed?
question
ready-to-merge
stale
wontfix
bug
duplicate
enhancement
help wanted
invalid
question
wontfix
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
allod/secrets#29
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Replace the hand-synchronised copies of the credential-store URL predicate with one definition its consumers read, so changing what counts as a valid credential template stops being an edit in three repos that only a comment keeps aligned. This finishes for the
local_auth_refreshsource predicate what allod/secrets#23 did for the credential vocabulary: that issue centralised the encoder list and the template schema, and left this narrower predicate as three separate implementations.Primary goals:
refresh-local-authconsumes an already-validated projection instead of re-deriving the grammar in jq.allod secretcannot evaluate Nix cheaply, so it consumes the grammar and the blank-line class as generated data rather than carrying a hand-written copy that has to be kept in step by review.Current state and specifics
The
https://user:token@hostgrammar is written out five times in four languages across three repos:archetypes/modules/netrc.nix:32archetypes/flake.nix:791(predicate at:773,:797)nix flake checktools/cmd/allod/secret_rotate.go:199(predicate at:205)nexus/scripts/refresh-local-auth:155(predicate at:144)nexus/scripts/refresh-local-auth:339The blank-line rule that decides which lines a template may carry is written out separately alongside each of them, and that is where the copies have actually diverged.
Two of the five have an independent reason to exist and are not in scope to merge away.
modules/netrc.nixis the last line of defense: it runs on the booted machine against whatever is on disk, which need not be what any registry declared, and it is shell because activation scripts are shell. The bash check atrefresh-local-auth:339validates a different claim from the jq one above it, the decrypted secret rather than the declared template, and it is what actually guards the bytes that get installed.The remaining three are the same predicate over the same input at three moments, and they are separate for a reason that is sound and is not what this issue proposes to change. Commit
4f56cb9in allod/nexus and the goals of allod/nexus#52 put the refresh in its own script on a privilege argument: it installs root-owned netrc bundles under sudo, which a command that edits a git repository must not be able to do. Three privilege domains genuinely need the answer. What does not follow is that each must carry its own copy of the rule.What holds the three in step today is prose. Each carries a comment asserting the others match it character for character, and that claim has needed correcting three times in about a week:
4193e91fixed a jq//against== nullmismatch found in cold review, whereencode: falsewould have passed one implementation and failed another.4fe01da(allod/nexus#56) corrected the parity comment itself, which had become false in two separate ways.strings.TrimSpaceatsecret_rotate.go:215also strips a carriage return and a vertical tab, so allod/tools accepts a template that allod/archetypes and allod/nexus refuse.Two facts make the unification cheaper than it looks. allod/archetypes already takes allod/secrets as a flake input (
archetypes/flake.nix:11), so it can call an exported predicate directly. Andrefresh-local-authalready evaluates that flake to read the registry, through an export that is validated on the way out:lib.forgejoTokenGroupsatsecrets/flake.nix:215is wrapped invalidateCredentialRegistry(:141), which throws, so any consumer reading the registry is already trusting the flake to have checked it. A validated projection of the refresh sources is a small addition to an export the script already depends on.Default: stage it. Land the shared vector table first, across the three existing test suites, with no production code moving. Then export the predicate from the secrets flake and cut over allod/archetypes, which is the consumer with the dependency edge already in place. Then give
refresh-local-autha pre-validated projection and delete its jq predicate. Then give allod/tools the grammar as generated data. Each step leaves the tree green and is independently revertible. Validation at every step: the three repos' flake checks,tests/refresh-local-auth.shin allod/nexus, andgo test ./...in allod/tools.Scope
In: the shared vector table; exporting the predicate from the secrets flake; the archetypes and nexus consumers reading it; generated grammar data for allod/tools; and folding the two URL grammars inside
refresh-local-authinto one.Out, deliberately: the privilege split between
allod secretandrefresh-local-auth, which is the reason the consumers are separate and is not being reconsidered; the activation parser inmodules/netrc.nix, which must stay an independent check on the deployed file; and the blank-line fix in allod/tools#214, which should land before any of this rather than wait for it, so the three implementations agree before one is made canonical.Refs allod/secrets#23, allod/nexus#52, allod/nexus#56, allod/tools#214.
There is a sixth implementation of this grammar that the table above cannot see: a deployment's private fork of this repository carries its own
isCredentialStoreUrlTemplateandisCredentialStoreUrlSource, copied fromallod/archetypesflake.nix:773and:797and kept in step by the same kind of parity comment this issue is retiring. Its single consumer is the fork's own registry validator, deciding whether alocal_auth_refreshentry's source credential is a template a netrc line can be rendered from. It is tracked privately and will follow whatever this issue exports; no scope change is needed here.One design input from that consumer, because it is a class the five public spellings do not cover. The three consumers named above are a check, a script and a Go command. The sixth is a downstream registry validator in a repository that takes this flake as an input and already reads its
libexports, the same dependency edge that makesallod/archetypesthe cheapest cutover. So an export shaped for archetypes is very likely already shaped for it, and the thing worth preserving deliberately is that the predicate stay callable as a plain function of one credential, rather than only reachable through a projection of this repository's own registry: a fork validates its own registry, not this one.The
formatguard is the one place where a fork might want a different answer than archetypes gets. Keeping it composable, whether it lives inside the exported predicate or beside it, costs nothing now and is awkward to retrofit.Worth adding to the first stage rather than the last: the shared accept and reject vector table is the half that helps the invisible consumer most, because a fork cannot be diffed against the canonical copy by anyone reading this issue. If those vectors land as data any implementation can read, a divergence in a fork fails a check there instead of surviving indefinitely, which is exactly what happened to the copy described above.
Implementation is up, with the design input above folded in.
credential-store-url.json(grammar plus 26 vectors, as data any implementation can read) and the exports.lib.isCredentialStoreUrlTemplateis the grammar test on one credential with noformatguard, andlib.isCredentialStoreUrlSourceis that guard composed with it, so a fork's registry validator composes its ownformatpolicy on the template predicate. Both are plain functions of one credential; nothing goes through this repository's own registry. Thecredential-store-urlcheck pins the composition law on the table, so a fork asserting the template predicate over the format-free vectors reads the same file.refresh-local-authreads the validatedlib.localAuthRefreshSourcesprojection and drops its jq copy; the plaintext parser takes the shared blank-line class.allod secret rotatereadslib.credentialStoreUrlat rotation time; the Go literal is gone and the test runs the shared table from a byte copy.credential-store-url-paritycheck pins the allod/tools copy to this file.Sequencing for a deployment: the private fork merges the secrets change first; the nexus and tools builds refuse an older checkout by name.
Status of the five stages, and what remains on each side.
Merged: stage 1 (shared vector table) and stage 2 (the exported predicate), allod/secrets#30; stage 5 (
allod secret rotatereads the grammar as data), allod/tools#216. Open and merge-ready: stage 4, allod/nexus#58, with master merged in at 6c782e5 and its 21 tests, flake check and real-consumer run green again. Open and reviewed: stage 3, allod/archetypes#86, the framework check calling the export with the shared table as its witnesses and acredential-store-url-paritycheck pinning the allod/tools copy; it closes this issue. Both public PRs can merge in either order; neither changes a running machine.Private handoff, for the deployment that carries its own fork of this repository. The fork needs two exports before its deploy lock takes the nexus change, and one of them already gates a merged command:
lib.credentialStoreUrl— the grammar and vector table as data (credential-store-url.jsonhere). The already-mergedallod secret rotatereads it from the checkout it rotates in and refuses an older checkout by name. As of a dev VM's closure rebuilt today, the fork does not export it, so rotation there is refused until it does.lib.localAuthRefreshSources— the validated projection oflocal_auth_refreshentries. The refresh script in allod/nexus#58 reads it and refuses a checkout without it, naming this issue. Land it in the fork before the deployment's lock takes that nexus commit, or the next credential refresh stops at preflight.formatpolicy onlib.isCredentialStoreUrlTemplatefrom the public flake and running the shared table against the result, the same way allod/archetypes#86 does.The fork mirrors these exports or re-exports the public flake's
lib; nothing about the private data changes. Verification on the deployment: a refresh that gets past preflight to decryption, and a rotation that no longer refuses the checkout.Follow-up from the private handoff: the public flake exported the validated local_auth_refresh projection only applied to its own registry, so a fork validating its own registry had to re-spell the mapping and gate it with lib.localAuthRefreshDiagnostics itself. allod/secrets#35 exports the constructor as lib.mkLocalAuthRefreshSources, a function of a registry beside its applied result, the way lib.mkPiCredentialContract sits beside lib.piCredentials. Implementation: #36. lib.localAuthRefreshSources is unchanged for allod/nexus; a fork calls the constructor on its own registry once its lock takes that commit.