feat(invoke,sign-manifest): first-class operator-signed control#95
feat(invoke,sign-manifest): first-class operator-signed control#95craigm26 wants to merge 2 commits into
Conversation
A robot-md-gateway with REQUIRE_ENVELOPE_SIGNATURE=1 verifies two signatures against RRF /v2/keys: the manifest's ROBOT-MD-SIG provenance footer and the INVOKE envelope — both by an *operator* authority kid. The CLI could produce neither: `invoke` signed with the robot's own keypair (whose kid the gateway can't resolve), there was no command to write the manifest footer, and `invoke` hard-errored when metadata.ruri was absent. So a cookbook reader who installed robot-md, registered, and ran `invoke` got a 403. Make operator-signed control first-class: - `robot-md sign-manifest <ROBOT.md> --operator-key <pem> --kid <kid>` writes (and idempotently replaces) the ROBOT-MD-SIG footer, signed to match the gateway's verify pre-image exactly (manifest_sig.py). - `robot-md invoke --operator-key <pem> --kid <kid>` signs the envelope with the operator keypair and advertises the operator kid; `--actuator <name>` sets actuator_name for multi-actuator gateways; ruri is auto-constructed via construct_ruri when the manifest omits it. Verified end-to-end against a live robot-md-gateway: signed read_state returns 200 executed. 30 new/updated tests (manifest sign/verify/strip/tamper; operator envelope signing; PEM loading; CLI e2e against a mock gateway). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds first-class operator-signed control so that a robot-md-gateway configured with REQUIRE_ENVELOPE_SIGNATURE=1 can be driven turnkey from the CLI. Introduces a new sign-manifest command that writes/replaces the ROBOT-MD-SIG provenance footer with an operator Ed25519 key, and extends invoke with --operator-key/--kid so the envelope is signed by an operator authority (whose kid resolves at RRF /v2/keys/<kid>) rather than by the robot's own keypair. Also adds --actuator for multi-actuator dispatch and auto-derives metadata.ruri via construct_ruri when absent.
Changes:
- New
robot_md.manifest_sigmodule (idempotent footer sign/strip/verify, byte-compatible with the gateway's_SIG_REandtext[: match.start()]pre-image) and newsign-manifestCLI command. invoke.py: addssign_envelope_with_ed25519,load_operator_ed25519, andactuator_namesupport inbuild_envelope;sign_envelopebecomes a thin wrapper.__main__.py invoke: adds--operator-key,--kid(now required with operator key),--actuator; auto-fillsruriviaconstruct_ruriand gates rrn-check behind keystore path.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| cli/src/robot_md/manifest_sig.py | New producer-side ROBOT-MD-SIG footer signer/verifier matching gateway's pre-image. |
| cli/src/robot_md/invoke.py | Adds raw-Ed25519 signing path, PEM operator-key loader, and actuator_name envelope field. |
| cli/src/robot_md/main.py | Adds --operator-key/--kid/--actuator to invoke, auto-derives ruri, and new sign-manifest command. |
| cli/tests/test_manifest_sig.py | Tests roundtrip, footer format, preimage parity, idempotent re-sign, strip, tamper, wrong key, missing footer. |
| cli/tests/test_invoke.py | Tests actuator_name, raw-Ed25519 signing, PEM loading (Ed25519-only), and CLI e2e operator-key path. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…r-key is omitted Completes the self-authorization turnkey path. `robot-md register` already binds the robot's own keypair as its operator-envelope authority (register.py), and — once RRF resolves that kid (RobotRegistryFoundation #84/#106) — the robot can sign its own manifest provenance. So make --operator-key/--kid optional: - `robot-md sign-manifest ROBOT.md` (no flags) loads the robot keypair from the manifest's metadata.rrn (~/.robot-md/keys/<rrn>.signing.json) and advertises its pq_kid — mirroring `invoke`'s default keystore signing path. - `--operator-key <pem> --kid <kid>` still signs with a separate operator key. So register -> sign-manifest -> invoke all work with the robot's own key, no separate PEM. 4 new CLI tests (operator-key mode; self-sign keystore mode + advertises pq_kid; missing-keystore error; --out leaves the source untouched). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Why
A
robot-md-gatewaywithREQUIRE_ENVELOPE_SIGNATURE=1verifies two signatures against RRF/v2/keys/<kid>before it will actuate: the manifest'sROBOT-MD-SIGprovenance footer, and the INVOKE envelope — both signed by an operator authority kid.Today the CLI can produce neither, so the cookbook's "actuation step" 403s for a fresh user:
invokesigns with the robot's own~/.robot-md/keys/<rrn>.signing.jsonkeypair, whose kid the gateway can't resolve (it resolves operator kids) →403 envelope_signature: kid … not registered.403 manifest_provenance: no ROBOT-MD-SIG footer.invokehard-errors whenmetadata.ruriis absent, even thoughconstruct_rurican derive it.What
robot-md sign-manifest <ROBOT.md> --operator-key <pem> --kid <kid>— writes (and idempotently replaces) theROBOT-MD-SIGfooter, signed over exactly the gateway's verify pre-image (text[: footer]). Newmanifest_sig.pykeeps the producer in lock-step with the gateway'sverify_manifest.robot-md invoke --operator-key <pem> --kid <kid>— signs the envelope with the operator keypair and advertises the operator kid (instead of the robot key). Adds--actuator <name>(required by multi-actuator gateways) and auto-constructsruriviaconstruct_ruriwhen the manifest omits it.sign_envelopenow delegates to a newsign_envelope_with_ed25519.Verification
robot-md-gateway:robot-md sign-manifestthenrobot-md invoke --operator-key … --kid bob-operator-2026 --actuator so-arm101 --tool read_state→200 executedwith telemetry (the gateway resolved the operator kid at prod RRF and verified both signatures).actuator_nameis set, andruriis auto-filled).Scope / follow-up
This makes operator-signed control turnkey for an operator identity that already exists (i.e. whose kid resolves at
/v2/keys). Minting a brand-new operator identity turnkey needs a small RRF-side change (havePOST /v2/authorities/registeralso write thekid:mapping the resolver scans) plus arobot-md operator registercommand — a separate, prod-gated PR.🤖 Generated with Claude Code