Skip to content

feat(invoke,sign-manifest): first-class operator-signed control#95

Open
craigm26 wants to merge 2 commits into
mainfrom
feat/operator-signed-control
Open

feat(invoke,sign-manifest): first-class operator-signed control#95
craigm26 wants to merge 2 commits into
mainfrom
feat/operator-signed-control

Conversation

@craigm26
Copy link
Copy Markdown
Contributor

Why

A robot-md-gateway with REQUIRE_ENVELOPE_SIGNATURE=1 verifies two signatures against RRF /v2/keys/<kid> before it will actuate: the manifest's ROBOT-MD-SIG provenance 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:

  • invoke signs with the robot's own ~/.robot-md/keys/<rrn>.signing.json keypair, whose kid the gateway can't resolve (it resolves operator kids) → 403 envelope_signature: kid … not registered.
  • There's no command to write the manifest's provenance footer → 403 manifest_provenance: no ROBOT-MD-SIG footer.
  • invoke hard-errors when metadata.ruri is absent, even though construct_ruri can derive it.

What

  • robot-md sign-manifest <ROBOT.md> --operator-key <pem> --kid <kid> — writes (and idempotently replaces) the ROBOT-MD-SIG footer, signed over exactly the gateway's verify pre-image (text[: footer]). New manifest_sig.py keeps the producer in lock-step with the gateway's verify_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-constructs ruri via construct_ruri when the manifest omits it.
  • Back-compat: the existing robot-keystore signing path is unchanged; sign_envelope now delegates to a new sign_envelope_with_ed25519.

Verification

  • End-to-end against a live robot-md-gateway: robot-md sign-manifest then robot-md invoke --operator-key … --kid bob-operator-2026 --actuator so-arm101 --tool read_state200 executed with telemetry (the gateway resolved the operator kid at prod RRF and verified both signatures).
  • 30 new/updated tests: manifest sign/verify/strip/tamper/wrong-key; operator envelope signing verifies against the advertised key; Ed25519 PEM loading (+ rejects non-Ed25519); CLI e2e with a mock gateway (asserts the operator kid is advertised, the signature verifies against the operator key, actuator_name is set, and ruri is 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 (have POST /v2/authorities/register also write the kid: mapping the resolver scans) plus a robot-md operator register command — a separate, prod-gated PR.

🤖 Generated with Claude Code

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>
Copilot AI review requested due to automatic review settings May 29, 2026 22:24
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_sig module (idempotent footer sign/strip/verify, byte-compatible with the gateway's _SIG_RE and text[: match.start()] pre-image) and new sign-manifest CLI command.
  • invoke.py: adds sign_envelope_with_ed25519, load_operator_ed25519, and actuator_name support in build_envelope; sign_envelope becomes a thin wrapper.
  • __main__.py invoke: adds --operator-key, --kid (now required with operator key), --actuator; auto-fills ruri via construct_ruri and 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>
Sign up for free to 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