ashlar keys verb + gate decisions signed on admit (SPEC-006 v1) - #387
Merged
Conversation
The application surface that activates the signing kernel already on master (#386). `ashlar keys init` creates the operator's local Ed25519 identity; once it exists the gate signs every verdict it records — presence-activated, so a project with no key keeps working unsigned and honest. - keys init: generates the operator keypair under $ASHLAR_KEY_DIR (else ~/.ashlar/keys); --rotate replaces it and retains the old public key under trusted/; refuses to overwrite without --rotate. - keys show: prints the operator fingerprint, or says plainly there is none (S-3, never a fingerprint for an absent key). Only the public fingerprint is ever printed; the seed never leaves the key directory. - gates signs: propose/admit/refuse open the store with the operator identity when present. Held verdicts are signed, an admit re-signs over the new content, no key means unsigned. - reads never need the key: gates list/show verify each record against its OWN embedded key, so they open a signer-less store — a mangled or missing operator key can no longer block an operator from seeing the queue; only write paths load the key and fail closed when corrupt. - corrupt-key hardening (kernel): OperatorKey.TryLoad normalises every corrupt-key shape (garbled base64, wrong-length seed) to the same loud InvalidOperationException the mismatch case already raised, so no caller sees a raw FormatException escape as a stack trace. A present-but-unreadable key fails loud; it never degrades to "no key". (From this slice's adversarial review.) - registration guard: RootCommand_RegistersKeysCommand, so `keys` cannot silently vanish. e2e-loop 65/65 (real CLI binary, fresh process each): the integration is proven end to end — signed records carry Sig+Signer, admit re-signs and reads back, signed records survive process death, a corrupt key fails writes cleanly yet reads still work. Kernel signing 27/27; CLI registration 7/7. Public-API snapshots unaffected. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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 freeto 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.
ashlar keys verb + gate decisions signed on admit (SPEC-006 v1)
What this is
The application half of SPEC-006 v1: the CLI surface that activates the signing kernel already
on master (#386).
ashlar keys initcreates the operator's local signing identity, and once itexists the gate signs every verdict it records — presence-activated, so a project with no key
keeps working exactly as before, unsigned and honest.
What landed
ashlar keys init— generates the single Ed25519 operator keypair under$ASHLAR_KEY_DIR(else
~/.ashlar/keys);--rotatereplaces it while retaining the old public key undertrusted/so records it already signed keep verifying; refuses to overwrite without--rotate.ashlar keys show— prints the operator fingerprint, or says plainly there is none. It neverprints a fingerprint for an absent key (S-3), and a corrupt key pair surfaces the kernel's loud
error rather than being papered over. Only the PUBLIC fingerprint is ever printed — the private
seed never leaves the key directory.
ashlar gatesnow signs —propose,admit, andrefuseopen the store with the operatoridentity when one is present. A held verdict is signed; an admit re-signs over the new content;
with no key, records are written unsigned (S-2). Writes open the store inside the command's try,
so a corrupt operator key becomes a clean exit-1 message, not a stack trace.
gates list/showverify each record against its OWNembedded key, so they open a signer-less store. A mangled or missing operator key can no longer
block an operator from seeing the queue; only the write paths, which have to sign, load the key
and fail closed when it is corrupt.
OperatorKey.TryLoadnow normalises every corrupt-key shape(garbled base64, wrong-length seed) to the same loud
InvalidOperationExceptionthe rotation-mismatch case already raised, so no caller sees a raw
FormatExceptionescape its guards as astack trace. A present-but-unreadable key fails loud; it never degrades to "no key" (which would
silently drop signing). Prompted by the adversarial review of this slice.
RootCommand_RegistersKeysCommandtest in the house pattern, sokeyscannot silently vanish from the root the way
trustonce did.Verification
real CLI binary, each a fresh process. Proven end to end: after
keys init, a gate record carriesSig+Signer; an admit re-signs and the record reads backADMITTED(the keyed re-decide is NOTbricked by a stale signature); a signed record survives process death; a corrupt operator key
fails writes with a clean message and no stack trace, yet reads still succeed.
Explicitly out of scope (next)
The runtime-side self-extend bridge (
src/Ashlar.BackgroundAgents.HostRunners) still records itsautomatic gate proposals unsigned — wiring the operator identity into that path is a kernel-side
change and its own slice. And
verifystill printsunsigned; real signature display arrives withthe SPEC-003 ledger.
[coordinated-integration]
Rationale for the layer-boundary hatch: this change is entirely under
application/(the CLI host)and depends on the signing kernel that just landed on master under
src/. It is the user-facingactivation of that kernel — there is no meaningful kernel/application split to make here, and the
application verbs it joins (
init,verify,gates,run) already live on master. Nosrc/paths are touched. The only required check on master,
cert-gate, gates the merge as usual.