One credential-store URL predicate: stop hand-synchronising five spellings of one grammar #29

Closed
opened 2026-09-18 22:05:47 +01:00 by allod-agent · 4 comments
Member

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_refresh source 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:

  • Shared witnesses before shared code — one checked-in table of accept and reject vectors that every implementation's test suite reads, so a divergence fails a test instead of surviving in a comment. This is the cheap half, it is useful on its own, and it lands first.
  • One definition the consumers read — the secrets flake exports the predicate beside the registry it already validates. The archetypes check calls it instead of redefining it, and refresh-local-auth consumes an already-validated projection instead of re-deriving the grammar in jq.
  • Go reads data, not prose — allod secret cannot 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.
  • One grammar inside refresh-local-auth — the script spells the same URL grammar twice, once against the declared template and once against the decrypted secret. Those become one expression applied twice, which is a local fix worth taking regardless of the rest.
  • Retire the parity comments — the comments in all three implementations asserting character-for-character agreement go once a mechanism enforces it, because a comment is what is enforcing it today.

Current state and specifics

The https://user:token@host grammar is written out five times in four languages across three repos:

Location Language Applied to
archetypes/modules/netrc.nix:32 sed the credential file on disk, at activation
archetypes/flake.nix:791 (predicate at :773, :797) Nix the declared template, at nix flake check
tools/cmd/allod/secret_rotate.go:199 (predicate at :205) Go the declared template, at rotation
nexus/scripts/refresh-local-auth:155 (predicate at :144) jq the declared template, at refresh
nexus/scripts/refresh-local-auth:339 bash the decrypted secret, at refresh

The 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.nix is 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 at refresh-local-auth:339 validates 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 4f56cb9 in 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:

  • allod/nexus commit 4193e91 fixed a jq // against == null mismatch found in cold review, where encode: false would have passed one implementation and failed another.
  • allod/nexus commit 4fe01da (allod/nexus#56) corrected the parity comment itself, which had become false in two separate ways.
  • allod/tools#214 is the live, still-open consequence: strings.TrimSpace at secret_rotate.go:215 also 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. And refresh-local-auth already evaluates that flake to read the registry, through an export that is validated on the way out: lib.forgejoTokenGroups at secrets/flake.nix:215 is wrapped in validateCredentialRegistry (: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-auth a 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.sh in allod/nexus, and go 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-auth into one.

Out, deliberately: the privilege split between allod secret and refresh-local-auth, which is the reason the consumers are separate and is not being reconsidered; the activation parser in modules/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.

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_refresh` source 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: - **Shared witnesses before shared code** — one checked-in table of accept and reject vectors that every implementation's test suite reads, so a divergence fails a test instead of surviving in a comment. This is the cheap half, it is useful on its own, and it lands first. - **One definition the consumers read** — the secrets flake exports the predicate beside the registry it already validates. The archetypes check calls it instead of redefining it, and `refresh-local-auth` consumes an already-validated projection instead of re-deriving the grammar in jq. - **Go reads data, not prose** — `allod secret` cannot 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. - **One grammar inside refresh-local-auth** — the script spells the same URL grammar twice, once against the declared template and once against the decrypted secret. Those become one expression applied twice, which is a local fix worth taking regardless of the rest. - **Retire the parity comments** — the comments in all three implementations asserting character-for-character agreement go once a mechanism enforces it, because a comment is what is enforcing it today. ### Current state and specifics The `https://user:token@host` grammar is written out five times in four languages across three repos: | Location | Language | Applied to | |---|---|---| | `archetypes/modules/netrc.nix:32` | sed | the credential file on disk, at activation | | `archetypes/flake.nix:791` (predicate at `:773`, `:797`) | Nix | the declared template, at `nix flake check` | | `tools/cmd/allod/secret_rotate.go:199` (predicate at `:205`) | Go | the declared template, at rotation | | `nexus/scripts/refresh-local-auth:155` (predicate at `:144`) | jq | the declared template, at refresh | | `nexus/scripts/refresh-local-auth:339` | bash | the decrypted secret, at refresh | The 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.nix` is 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 at `refresh-local-auth:339` validates 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 `4f56cb9` in 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: - allod/nexus commit `4193e91` fixed a jq `//` against `== null` mismatch found in cold review, where `encode: false` would have passed one implementation and failed another. - allod/nexus commit `4fe01da` (allod/nexus#56) corrected the parity comment itself, which had become false in two separate ways. - allod/tools#214 is the live, still-open consequence: `strings.TrimSpace` at `secret_rotate.go:215` also 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. And `refresh-local-auth` already evaluates that flake to read the registry, through an export that is validated on the way out: `lib.forgejoTokenGroups` at `secrets/flake.nix:215` is wrapped in `validateCredentialRegistry` (`: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-auth` a 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.sh` in allod/nexus, and `go 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-auth` into one. Out, deliberately: the privilege split between `allod secret` and `refresh-local-auth`, which is the reason the consumers are separate and is not being reconsidered; the activation parser in `modules/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 isCredentialStoreUrlTemplate and isCredentialStoreUrlSource, copied from allod/archetypes flake.nix:773 and :797 and 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 a local_auth_refresh entry'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 lib exports, the same dependency edge that makes allod/archetypes the 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 format guard 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.

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 `isCredentialStoreUrlTemplate` and `isCredentialStoreUrlSource`, copied from `allod/archetypes` `flake.nix:773` and `:797` and 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 a `local_auth_refresh` entry'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 `lib` exports, the same dependency edge that makes `allod/archetypes` the 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 `format` guard 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.
Author
Member

Implementation is up, with the design input above folded in.

  • #30 — the definition: credential-store-url.json (grammar plus 26 vectors, as data any implementation can read) and the exports. lib.isCredentialStoreUrlTemplate is the grammar test on one credential with no format guard, and lib.isCredentialStoreUrlSource is that guard composed with it, so a fork's registry validator composes its own format policy on the template predicate. Both are plain functions of one credential; nothing goes through this repository's own registry. The credential-store-url check pins the composition law on the table, so a fork asserting the template predicate over the format-free vectors reads the same file.
  • allod/nexus#58 — refresh-local-auth reads the validated lib.localAuthRefreshSources projection and drops its jq copy; the plaintext parser takes the shared blank-line class.
  • allod/tools#216 — allod secret rotate reads lib.credentialStoreUrl at rotation time; the Go literal is gone and the test runs the shared table from a byte copy.
  • allod/archetypes follows after the first and third merge, because its lock must point at master commits: the check calls the exported predicate, its witnesses are the table, and a new credential-store-url-parity check 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.

Implementation is up, with the design input above folded in. - https://forge.anarch.diy/allod/secrets/pulls/30 — the definition: `credential-store-url.json` (grammar plus 26 vectors, as data any implementation can read) and the exports. `lib.isCredentialStoreUrlTemplate` is the grammar test on one credential with no `format` guard, and `lib.isCredentialStoreUrlSource` is that guard composed with it, so a fork's registry validator composes its own `format` policy on the template predicate. Both are plain functions of one credential; nothing goes through this repository's own registry. The `credential-store-url` check pins the composition law on the table, so a fork asserting the template predicate over the format-free vectors reads the same file. - https://forge.anarch.diy/allod/nexus/pulls/58 — `refresh-local-auth` reads the validated `lib.localAuthRefreshSources` projection and drops its jq copy; the plaintext parser takes the shared blank-line class. - https://forge.anarch.diy/allod/tools/pulls/216 — `allod secret rotate` reads `lib.credentialStoreUrl` at rotation time; the Go literal is gone and the test runs the shared table from a byte copy. - allod/archetypes follows after the first and third merge, because its lock must point at master commits: the check calls the exported predicate, its witnesses are the table, and a new `credential-store-url-parity` check 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.
Author
Member

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 rotate reads 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 a credential-store-url-parity check 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.json here). The already-merged allod secret rotate reads 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 of local_auth_refresh entries. 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.
  • The fork's own copy of the predicate (the sixth spelling named above) retires by composing its format policy on lib.isCredentialStoreUrlTemplate from 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.

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 rotate` reads 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 a `credential-store-url-parity` check 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.json` here). The already-merged `allod secret rotate` reads 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 of `local_auth_refresh` entries. 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. - The fork's own copy of the predicate (the sixth spelling named above) retires by composing its `format` policy on `lib.isCredentialStoreUrlTemplate` from 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.
vnprc closed this issue 2026-09-20 16:32:36 +01:00
Author
Member

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.

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: https://forge.anarch.diy/allod/secrets/pulls/36. lib.localAuthRefreshSources is unchanged for allod/nexus; a fork calls the constructor on its own registry once its lock takes that commit.
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
allod/secrets#29
No description provided.