Sign bundled plugins during release with drift guard - #1298
Conversation
Add a 'Sign bundled plugins' step to the release workflow's build job, placed after 'Build application' (which emits dist/cli/maestro-cli.js) and before all electron-builder packaging steps. It writes the MAESTRO_PLUGIN_SIGNING_KEY secret to a temp PEM, loops over every examples/plugins/*/ dir signing each with maestro-cli plugin sign, then removes the key. Because extraResources copies examples/plugins/* into packaged resources, the freshly written signature.json ships with the plugin so the baked MAESTRO_PUBLISHER_KEYS anchor resolves it trusted at seed time and Agent Flow's main.js is allowed to run. The workflow is a single matrix build job, so one shared step runs on every platform leg; signing is deterministic given the same files+key, so per-leg signing is safe. shell: bash keeps it identical on Windows. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
After signing bundled plugins, verify agent-flow's signature resolves 'trusted' against the baked MAESTRO_PUBLISHER_KEYS anchor. A rotated CI secret with a stale publisher-keys.ts (or an empty anchor) now fails the release instead of shipping a permanently seed-skipped, non-runnable plugin. Adds set -euo pipefail and an explicit empty-anchor error. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…igning workflow Add examples/plugins/*/signature.json to .gitignore so a locally-run plugin sign never gets committed (it is a release-time build artifact). Document the bundled-plugin signing flow in CLAUDE-PLUGINS.md: the release CI path (CI secret -> plugin sign -> extraResources -> baked MAESTRO_PUBLISHER_KEYS anchor -> drift guard), the local dev workflow (--gen-key, add SPKI key to pluginTrustedKeys, enable + consent), and the v1 single-key/no-revocation trust model with additive-then-prune rotation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a resolveTrustedKeys describe block to the shared signing suite: merge/trim/de-dup contract with user keys, resolveTrustedKeys([]) equals the trimmed baked anchor set, and a guarded loop proving every baked MAESTRO_PUBLISHER_KEYS entry is a non-empty, valid base64 SPKI key that crypto.createPublicKey accepts (asserts nothing while the anchor is empty pre-key-mint, never hard-codes the real key value). The fixture-keypair verifyPluginSignature round trip and the CLI sign/validate round trip were already fully covered, so no duplicate suites were added. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughRelease CI signs bundled plugins and verifies their trusted status against baked publisher keys. Generated signatures are ignored, documentation describes release and development trust flows, and tests validate the baked key format. ChangesBundled plugin trust
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant ReleaseCI
participant MaestroCLI
participant BundledPlugins
participant PublisherKeys
ReleaseCI->>MaestroCLI: Sign bundled plugin directories
MaestroCLI->>BundledPlugins: Write signature.json
ReleaseCI->>PublisherKeys: Read MAESTRO_PUBLISHER_KEYS
ReleaseCI->>MaestroCLI: Validate agent-flow with trusted keys
MaestroCLI-->>ReleaseCI: Return trusted status
Possibly related PRs
Suggested labels: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Greptile SummaryThe PR adds release-time signing and validation for bundled plugins.
Confidence Score: 4/5The PR is not yet safe to merge because a release can still succeed without the signing secret and ship Agent Flow in an unusable unsigned state. The anchor is populated at current HEAD, but the workflow exits successfully whenever the private signing secret is missing; packaging then proceeds without signature.json, and the startup trust gate skips the bundled plugin. Files Needing Attention: .github/workflows/release.yml Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart LR
A[Build application] --> B[Load baked publisher anchor]
B --> C{Anchor and signing secret available?}
C -->|Yes| D[Sign bundled plugins]
D --> E[Validate Agent Flow as trusted]
E -->|Trusted| F[Package release]
E -->|Not trusted| G[Fail release]
C -->|No| F
F --> H[Seed bundled plugins at startup]
H --> I{Signature trusted?}
I -->|Yes| J[Install and enable feature]
I -->|No| K[Skip bundled plugin]
Reviews (2): Last reviewed commit: "fix(plugins): bake the publisher key tha..." | Re-trigger Greptile |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/workflows/release.yml:
- Around line 256-261: Update the release workflow’s plugin-signing block around
KEYFILE to install an EXIT trap immediately after defining the key-file path,
ensuring the temporary private key is removed on both successful completion and
any signing failure; preserve the existing signing loop and cleanup behavior.
In `@src/__tests__/shared/plugins/rpc-and-signing.test.ts`:
- Around line 115-132: Strengthen the resolveTrustedKeys tests to assert exact
anchor precedence and de-duplication: verify the resolved prefix matches the
trimmed, non-blank, de-duplicated MAESTRO_PUBLISHER_KEYS in order before user
keys, and update the empty-list expectation to use the same de-duplicated anchor
sequence. Keep the existing user-key trimming and de-duplication assertions.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 5867bcfb-c421-4d4a-a0d9-c4158993c0cb
📒 Files selected for processing (4)
.github/workflows/release.yml.gitignoreCLAUDE-PLUGINS.mdsrc/__tests__/shared/plugins/rpc-and-signing.test.ts
|
Thanks for this, @chr1syy - genuinely nice work. You correctly identified that the trust gate in
CI is green and there are no merge conflicts. There are a few things I'd like addressed before we merge, one of them blocking. 1. Blocking: this breaks every release the moment it lands on
|
|
Picked up the remaining work here. Since @pedramamini's review we now have a real release keypair, so I took his alternative path for the blocking item and landed the key instead of the no-op guard. What's now true outside this PR:
The follow-up commit (on branch
262/262 plugin tests pass with the baked anchor, prettier clean. I couldn't push to the fork branch from my environment (git writes hang), hence the branch. To land it: or any maintainer can push that branch onto this PR directly. One scope note: the signing step runs in release.yml, which only fires on maintainer-pushed tags — untrusted PR code never reaches the key. I added a comment on the step making that gate explicit. |
- release.yml: trap 'rm -f "$KEYFILE"' EXIT so the private key is removed even when a signing failure trips set -e before the explicit rm (item 2). - Drop the two resolveTrustedKeys tests that duplicate publisher-keys.test.ts and move the base64 SPKI validity check there next to its siblings (item 3). - CLAUDE-PLUGINS.md: fix the contradictory dev-testing paragraph - the seed gate continues before copy, so an unsigned bundled plugin is never installed and there is no panel; the "renders, waits forever" case is the manually-installed-unsigned path (item 4). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Thanks @pedramamini, @jSydorowicz21 - review addressed. Summary of what landed in Item 2 (private key cleanup on failure) - added Item 3 (duplicate Item 4 (docs contradiction) - rewrote the dev-testing paragraph. The seed gate Item 1 (blocking - empty anchor) - this is the maintainer-only follow-up (key mint + Actions secret + baking the public key into 262/262 plugin tests pass, prettier clean, CI green. |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/workflows/release.yml:
- Around line 256-260: Set a restrictive umask of 077 in the release signing
step before the printf command writes MAESTRO_PLUGIN_SIGNING_KEY to KEYFILE,
ensuring the temporary private key is not broadly readable while preserving the
existing cleanup trap.
In `@src/__tests__/shared/plugins/publisher-keys.test.ts`:
- Around line 5-20: Update the MAESTRO_PUBLISHER_KEYS test to validate canonical
encodings: decode each key, load it with createPublicKey, re-encode the DER
bytes with canonical base64 and compare to the original input, then assert the
loaded key’s asymmetric key type is ed25519.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: c79e21c2-78de-43da-8418-3a34d6409340
📒 Files selected for processing (3)
.github/workflows/release.ymlCLAUDE-PLUGINS.mdsrc/__tests__/shared/plugins/publisher-keys.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- CLAUDE-PLUGINS.md
- release.yml: read the baked ANCHOR first and make signing + the drift guard conditional. When no anchor is baked (or the signing secret is absent) - the current pre-key-mint state - skip signing and the drift check with a warning and let the release proceed, shipping bundled plugins unsigned (they simply are not seeded at runtime). Only when an anchor is present do we sign and hard-fail on drift. Keeps the repo releasable today and fail-closed once the key is baked (Greptile P1). - release.yml: umask 077 before writing the temp private key so it is not group/world-readable (CodeRabbit). - publisher-keys.test.ts: tighten the anchor test to reject non-canonical base64 (DER round-trip) and require asymmetricKeyType === 'ed25519', not merely any valid SPKI key (CodeRabbit). Still empty-anchor-safe today. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/workflows/release.yml:
- Around line 271-274: Update the release guard around ANCHOR and
MAESTRO_PLUGIN_SIGNING_KEY so the pre-key-mint case still exits successfully,
but a non-empty ANCHOR with a missing signing secret exits nonzero and fails the
release. Preserve the existing unsigned-plugin warning only for the no-ANCHOR
state.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: df4a555b-d046-4770-b0a7-2561a49d485b
📒 Files selected for processing (2)
.github/workflows/release.ymlsrc/__tests__/shared/plugins/publisher-keys.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- src/tests/shared/plugins/publisher-keys.test.ts
Adds a prominent header toggle to examples/plugins/agent-flow/panel.html that renders only the lane matching the snapshot's focusedSessionId. Default OFF per the playbook's Decision 3 sub-answer, since lazy lane seeding already removes the idle-dot wall and an ON default would hide the fleet view. lanesOf() now delegates to resolveLanes(), the single lane source shared by renderGraph() and the 1s health tick. When the filter is ON but no session is focused (or the focused session has no lane yet) it falls back to the whole fleet and shows a small header hint rather than a blank canvas. With the toggle OFF the all-agents view, including the focused highlight, is unchanged. Toggle state is a panel-lifetime var, so it survives snapshot replacement with no storage API. plugin.json is not bumped again: the 0.2.0 -> 0.3.0 bump landed with the lazy-seeding commit. Note: editing panel.html changes its SHA and invalidates the bundled Agent Flow signature until the next release-time re-sign (PR RunMaestro#1298 flow). Nothing is re-signed here.
seedFromSessions() no longer materializes a lane per open session, so a user with 109 configured agents stops opening the overlay onto a wall of idle dots. Session titles/agentIds/statuses now live in a sessionMeta side map that getLane() reads when activity finally creates the lane, so a lane is labelled the moment it appears. Lane-materializing events: tool.executed, usage.updated, the terminal agent.completed / agent.error / agent.exited / run.completed events, and session.activated (the focused agent always needs a node for the panel's highlight and the upcoming "current agent only" filter to land on). session.created and session.updated are metadata only: they record into sessionMeta and touch the lane only when one already exists. Exception to the decision record: a busy agent.statusChanged does NOT create a lane. That event is agentId-keyed, carries no sessionId, and fans out over existing lanes only; synthesizing a lane there previously produced a nodeless ghost lane that won an insertion-order lookup. resetModel() keeps sessionMeta (the clear command forgets activity, not who the agents are); deactivate() clears it. Note: editing main.js and plugin.json changes their file SHAs and invalidates the bundled Agent Flow signature until the release pipeline re-signs it (PR RunMaestro#1298 flow). Nothing is re-signed here; that is expected. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds a prominent header toggle to examples/plugins/agent-flow/panel.html that renders only the lane matching the snapshot's focusedSessionId. Default OFF per the playbook's Decision 3 sub-answer, since lazy lane seeding already removes the idle-dot wall and an ON default would hide the fleet view. lanesOf() now delegates to resolveLanes(), the single lane source shared by renderGraph() and the 1s health tick. When the filter is ON but no session is focused (or the focused session has no lane yet) it falls back to the whole fleet and shows a small header hint rather than a blank canvas. With the toggle OFF the all-agents view, including the focused highlight, is unchanged. Toggle state is a panel-lifetime var, so it survives snapshot replacement with no storage API. plugin.json is not bumped again: the 0.2.0 -> 0.3.0 bump landed with the lazy-seeding commit. Note: editing panel.html changes its SHA and invalidates the bundled Agent Flow signature until the next release-time re-sign (PR RunMaestro#1298 flow). Nothing is re-signed here.
Publisher key baked - A1 is now completePushed What changed (only the two files the plumbing was waiting on):
The key was sanity-checked before baking: exact canonical base64 round trip, parses via Drift guard dry run (scratch key, no production private key involved). The positive production path can only run in release CI by design, so the release step's exact command lines were exercised locally with a throwaway keypair:
Checks: scoped suite 4 passed / 0 failed; all three Still needs a human before the first tagged release: confirm the |
|
@pedramamini this one needs a repo secret only an admin can add. Everything you need: Name: The one thing that has to line upThe private key must be the counterpart of the public key already committed in If they drift the release fails loudly rather than shipping something broken - the drift guard re-validates the fresh signature against the baked anchor and exits 1. That is deliberate: a silent mismatch would ship a plugin that can never be seeded at runtime. There is a local verify command in the handoff note that derives the public key from the private one and compares it to the baked anchor, so the match can be confirmed before the key is handed over. It prints only the public key, so its output is safe to share. This PR does not need the secret to mergeThe signing step degrades rather than breaks. With no secret it warns and exits 0, and unsigned bundled plugins are simply not seeded - Full detail, including how to transfer the key safely and what rotation involves, is in |
The anchor was set to a key minted locally on 2026-08-07, but MAESTRO_PLUGIN_SIGNING_KEY was already configured on 2026-07-25 (confirmed via the Actions secrets listing: created 2026-07-25, never updated) holding the ed25519 key minted 2026-07-23 that @jSydorowicz21 landed in the pr-1298-followup branch. Those are different keys, so as it stood this branch would have made the drift guard fail EVERY release: CI signs with the secret's private half, producing a signature whose publicKey is `...gG9il...`, which does not match the baked `...NZcDO...`. The guard is doing exactly its job - the bug was the anchor. Baking the secret's public counterpart instead of rotating the secret keeps the release key where it belongs (only in CI, held by no individual) and needs no admin action. The locally minted key is discarded; it never signed anything shipped. Comment expanded to name the secret it pairs with and to state that rotation is a two-part change. Tests: 623 plugin tests pass. The key is verified to parse as an ed25519 SPKI and round-trip to the identical base64. Three tsc configs and prettier clean.
|
Correction to my previous comment - the secret is already set, and this branch had the wrong key baked. I checked the Actions secrets listing rather than continuing to assume: That matches @jSydorowicz21's comment on this PR to the day. So no admin action is needed and my earlier request to @pedramamini was wrong - apologies for the noise. The real problem it uncoveredThe secret holds the key minted 2026-07-23, whose public half @jSydorowicz21 baked on This branch was baking a different key, minted locally on 2026-08-07: As it stood, merging this would have failed every release at the drift guard: CI signs with the secret's private half, and the resulting signature's Fix -
|
|
@greptileai review please |
|
✅ Action performedReview finished.
|
…gone Review of PR RunMaestro#1298 (Greptile). The pre-key-mint escape hatch treated a missing anchor and a missing secret as the same degraded state, which was right while MAESTRO_PUBLISHER_KEYS was empty. Now that a real key is baked, that pairing lets a removed or renamed MAESTRO_PLUGIN_SIGNING_KEY exit 0 and ship unsigned plugins: the anchor promises a trust guarantee the artifact cannot honour, and Agent Flow silently stops being seeded in a shipped build. Split into the two cases they actually are: - No anchor (a fork, or this repo pre-mint): still skips with a warning. There is nothing to validate against, and breaking every fork's release would be worse than shipping plugins that are simply never seeded. - Anchor baked, secret absent: now fails with an actionable error. That state is only reachable by removing the secret after baking a key, which is a misconfigured release, not a degraded one. Same reasoning as the drift guard directly below it: a key/secret mismatch should fail at release time rather than in users' hands.
|
Thanks @chr1syy, and thanks @jSydorowicz21 for surfacing the key mismatch. Coming back to this after my July review: items 2, 3 and 4 are all properly addressed, and item 1 (the blocking one) is resolved the way we agreed, by landing the real anchor rather than softening the guard. I re-verified the pieces that matter on
CI is green on both matrix legs and there are no merge conflicts. Two small things I would like fixed before this goes in, then I am happy. 1. The empty-anchor branch's comment is now wrong, and it changes fork behavior
This is the same class of issue as item 4 in my last review, a comment that describes behavior the code no longer has. The guard itself is still worth keeping for the deliberately-emptied-anchor case that your own error message already references, so this is just about the comment and the fork case. Either reword it to drop the fork claim and say plainly that it only fires when the anchor is deliberately emptied, or, if you would rather forks keep building, gate the hard-fail on the repository (for example 2. The PR description now says the opposite of what the PR doesThe description still carries:
plus the whole "Human follow-up required" section asking a maintainer to mint the keypair, add the secret, and bake the public key. All three are done. Since this text follows the change into the merge commit and is the first thing anyone reads when they come back to this later, please bring it in line with the five-file reality. Nits, take or leave
One thing on me, not on this PRNothing in this PR can prove the baked public key is the counterpart of the private key sitting in the secret, since the anchor value reached this branch through a PR comment rather than being derived from the secret. If they have drifted, the guard fails all four matrix legs on the next tag. That is the designed behavior and is much better than shipping an unseedable plugin, but I would rather not discover it during a real release. I will confirm the match on my side before the first tagged release after this merges, so do not block on it. Ping me once 1 and 2 are pushed and I will get this merged. Nice work running the drift scenario end to end with a scratch key, that write-up made this much faster to review. |
Finding A1 - Agent Flow ships unsigned, so its code never runs
Bundled plugins (Agent Flow) ship without a signature, so
main.jsnever executes (isRunnable()requiressignature.status === 'trusted'). The panel renders but sits on "Waiting for agent activity..." forever.Change (code / CI / test / docs)
maestro-cli plugin sign), so the baked trust anchor resolves themtrustedat seed time.publisher-keys.tsdiverge (fail-closed)..gitignoreentry for the release-onlysignature.jsonartifact.CLAUDE-PLUGINS.md.publisher-keys.tsis intentionally NOT modified here (MAESTRO_PUBLISHER_KEYSstays empty).Validation
Scoped test
rpc-and-signing.test.ts-> 17/17 pass. ESLint + Prettier clean. Type-check: the only.tschange is the test file (verified type-clean in isolation); the fullnpm run lintis slow in this environment and is left to CI.This PR is not functionally complete on its own. A maintainer must still:
MAESTRO_PLUGIN_SIGNING_KEYGitHub Actions secret.MAESTRO_PUBLISHER_KEYSinsrc/shared/plugins/publisher-keys.ts.Until then bundled plugins remain unsigned; the drift guard hard-fails the release while the anchor is empty, which is the intended fail-closed behavior.
Fix plan:
.maestro/FIX-A1-01.md.Summary by CodeRabbit