Skip to content

ashlar keys verb + gate decisions signed on admit (SPEC-006 v1) - #387

Merged
IanFrelinger merged 1 commit into
masterfrom
claude/keys-signing
Aug 24, 2026
Merged

ashlar keys verb + gate decisions signed on admit (SPEC-006 v1)#387
IanFrelinger merged 1 commit into
masterfrom
claude/keys-signing

Conversation

@IanFrelinger

Copy link
Copy Markdown
Owner

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 init creates the operator's local signing identity, and once it
exists 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); --rotate replaces it while retaining the old public key under
    trusted/ 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 never
    prints 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 gates now signspropose, admit, and refuse open the store with the operator
    identity 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.
  • Reads never need the operator keygates 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 the write paths, which have to sign, load the key
    and fail closed when it is corrupt.
  • Corrupt-key hardening (kernel)OperatorKey.TryLoad now normalises every corrupt-key shape
    (garbled base64, wrong-length seed) to the same loud InvalidOperationException the rotation-
    mismatch case already raised, so no caller sees a raw FormatException escape its guards as a
    stack 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.
  • Registration guard — a RootCommand_RegistersKeysCommand test in the house pattern, so keys
    cannot silently vanish from the root the way trust once did.

Verification

  • e2e-loop: 65/65 scenarios (was 47; +18 for keys / signing / corrupt-key), run against the
    real CLI binary, each a fresh process. Proven end to end: after keys init, a gate record carries
    Sig+Signer; an admit re-signs and the record reads back ADMITTED (the keyed re-decide is NOT
    bricked 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.
  • Kernel signing tests: 27/27 (adds the corrupt-key normalisation test).
  • CLI registration tests: 7/7 including the new guard.
  • Public-API snapshots unaffected (they cover Authoring/SDK assemblies, not the CLI).
  • Builds clean.

Explicitly out of scope (next)

The runtime-side self-extend bridge (src/Ashlar.BackgroundAgents.HostRunners) still records its
automatic gate proposals unsigned — wiring the operator identity into that path is a kernel-side
change and its own slice. And verify still prints unsigned; real signature display arrives with
the 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-facing
activation 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. No src/
paths are touched. The only required check on master, cert-gate, gates the merge as usual.

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>
@IanFrelinger
IanFrelinger merged commit af9c28f into masterAug 24, 2026
19 checks passed
@IanFrelinger
IanFrelinger deleted the claude/keys-signing branch August 24, 2026 02:48
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@IanFrelinger@PlzTouchGrass