Skip to content

🤖 feat: record the reviewed fingerprint of each imported plugin component - #6534

Merged
ThomasK33 merged 7 commits into
mainfrom
fix/plugin-import-fingerprints
Oct 14, 2026
Merged

ThomasK33 merged 7 commits into
mainfrom
fix/plugin-import-fingerprints

Conversation

@ThomasK33

@ThomasK33 ThomasK33 commented Oct 14, 2026 •

Copy link
Copy Markdown
Member

Summary

Xum now records the fingerprint the user reviewed for each imported component of a managed plugin. Nothing reads the records yet, so loading does not change in this PR. A later PR in this stack uses them to refuse a component whose fingerprint changed. Refs #6498 (part 1 of 4).

Background

Saved imports in plugins.json name a component, but they do not bind what the user reviewed. So when a newer Xum parses more of an installed package, a changed skill or MCP server can load under an approved name. #6498 has two examples: an extra Codex skills root, and a stdio server that an older parser skipped. The design and its security review are summarized in #6498.

Implementation

  1. New componentConsent.ts builds the fingerprints. Review time and load time use the same functions.
    • MCP server: Xum hashes the whole normalized MCPServerInfo. A Record<keyof …, "include" | "exclude"> map names every field, so a new field is a compile error until someone decides. Without that, a future parser widening would slip past. Excluded fields are the ones the user owns (disabled, toolAllowlist), provenance, and the location of the OAuth client secret file. The plugin root and the PLUGIN_DATA directory become tags, so a staged clone and the installed tree hash alike. Literal % and < are escaped first, so a manifest cannot fake a tag.
    • Skill: Xum hashes the root-relative directory (for example extra/foo) plus the whole SKILL.md text.
    • A scheme version is part of every hash.
  2. Registry entries get an optional top-level componentConsent: { skills, mcpServers } (name → fingerprint). It is top-level because older builds spread importedComponents on save and would leave a nested record stale.
  3. Writers:
    • install() records the fingerprints of the imported components, because the consent preview showed them at the pinned SHA.
    • setComponents() records a fingerprint only for components that are new to the selection. A component that stays selected keeps its old record, or stays without one. Without this rule, a component picker save could approve a component that changed since its review. Narrowing a legacy import-all entry approves nothing.
    • getComponents() never sends fingerprints to the renderer.

Accepted limitations, documented in componentConsent.ts:

  • A bare command resolves through PATH at launch.
  • The bytes of a ./ command, and the other files in a skill directory, are not hashed.

Validation

  • Unit tests in componentConsent.test.ts cover these cases:
    • The fingerprint stays the same across install locations and env key order.
    • Every launch-relevant field change produces a new fingerprint.
    • A literal tag in a manifest cannot collide with the plugin root.
    • The skill directory and the body both count.
    • The selection rules.
  • New installService.test.ts cases cover install, a picker save that adds a component while keeping a stale record, and a narrowed legacy install.

Risks

Low. The new field is optional and nothing reads it yet. Older builds ignore it.


Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: high • Cost: $0.00

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-14T14:13:19.534868Z 06c6680 Manual request
🔒 Security Review ✅ Completed 2026-10-14T14:16:35.532205Z 06c6680 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@ThomasK33 ThomasK33 changed the title fix/plugin import fingerprints 🤖 feat: record the reviewed fingerprint of each imported plugin component Oct 14, 2026
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review · Automatically triggered

Security review completed. No security issues were found in this pull request.

Reviewed commit: 3dba7cd744

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3dba7cd744

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/node/services/agentPlugins/installService.ts Outdated
Comment thread src/node/services/agentPlugins/installService.ts Outdated
Comment thread src/node/services/agentPlugins/componentConsent.ts Outdated
@ThomasK33
ThomasK33 force-pushed the fix/plugin-import-fingerprints branch from 3dba7cd to f6c32f7 Compare October 14, 2026 11:19
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bb57a9d14a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/node/services/agentPlugins/installService.ts
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

Round 3: the round 2 finding (refresh consent after an update) is handled in #6557, the next PR in this stack. No open threads remain.

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review · Automatically triggered

Security review completed. No security issues were found in this pull request.

Reviewed commit: bb57a9d14a

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bb57a9d14a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/common/config/schemas/agentPluginInstalls.ts Outdated
Comment thread src/node/services/agentPlugins/componentConsent.ts Outdated
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

Round 4: the __proto__ finding is fixed (9b507f9, 6e0846b). The client-secret finding is answered on its thread. From this round on I fix blockers only.

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

Round 4, now with the fix pushed (head 6e0846b). My previous request ran before the push landed.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Nice work!

Reviewed commit: 6e0846b2ac

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review · Automatically triggered

Security review completed. No security issues were found in this pull request.

Reviewed commit: 6e0846b2ac

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

Round 5 (head 06c6680, blockers only): the consent fingerprint now records whether a server sends an OAuth client secret (a presence marker, no path or value). The #6587 review found this as a P1.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Hooray!

Reviewed commit: 06c668067b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review · Automatically triggered

Security review completed. No security issues were found in this pull request.

Reviewed commit: 06c668067b

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

Merged via the queue into main with commit 31afb32 Oct 14, 2026
32 of 33 checks passed
@ThomasK33
ThomasK33 deleted the fix/plugin-import-fingerprints branch October 14, 2026 15:28
yermakoffivan pushed a commit to yermakoffivan/mux that referenced this pull request Oct 14, 2026
… changes only (coder#6557)

## Summary

This PR adds the backend half of the plugin component review. A new
review entry point lists the imported skills and MCP servers whose
current fingerprint the user has not reviewed, and a confirm call
records consent for them. Updates now record consent only for the
changes the user saw. Nothing gates loading yet, so this PR does not
change what loads. Refs coder#6498 (part 2 of the stack, on top of coder#6534).

## Background

coder#6534 stores a reviewed fingerprint for each imported component. A later
PR will refuse a component whose fingerprint has no matching record.
Before that gate can land, users need a way to review components. Every
install from before coder#6498 has no records, so every managed plugin needs
one review after the upgrade. This PR adds that review and makes updates
keep the records current.

## Implementation

1. **Component review:** `previewComponentReview({ name })` returns the
existing update review shape (`fromSha` equals `toSha`), so Settings can
reuse the update review panel. Each row says "Not reviewed on this
install" or "Changed since you reviewed it". It shows the same display
text as the update review and carries a component reference `{ kind,
name, fingerprint }`.
2. **Confirm:** `confirmComponentReview({ name, expectedLockedSha,
reviewed })` recomputes the rows under the mutation lock. It records
only the references that still match, so a component that changed after
the review keeps needing review. It then reconciles MCP servers.
3. **Updates:** `AgentPluginUpdateConsentSchema` gets an optional
`reviewed` list, and each skill or server change row carries its
component reference.
- `update()` records consent only for recomputed changes that appear in
`reviewed` at the same fingerprint.
   - Every other record is kept.
- Without `reviewed`, the changed components need a component review
afterwards (fails closed).
4. **Update diff:** the diff now uses the consent fingerprints.
- A server change compares the whole normalized server plus the
client-secret digest, so a parser widening also shows up in the update
review.
- A skill change compares its location and full frontmatter, and the
summary says "changes the definition of skill" when the advertisement
did not change.
5. **List rows:** managed rows get `reviewRequiredCount`, the number of
imported components that need review.
6. **Consent merge:** writes merge the raw `componentConsent` object, so
consent kinds that a newer build stored survive.

The renderer changes (the "Needs review" badge, a notice, and the review
panel reuse that sends `reviewed`) come in the next PR of the stack.

## Validation

New `installService.test.ts` cases:
- A legacy install (no records) lists both components. Confirming after
one of them changed records only the unchanged one.
- An update records only the reviewed change, and the unreviewed one
keeps its old record.
- A manifest that names the final install path literally needs no review
after install.

## Risks

Low. Loading does not change yet. An update applied from the current UI,
which does not send `reviewed` yet, leaves the changed components
counted as needing review. Nothing shows that count until the next PR.

---

_Generated with `xum` • Model: `anthropic:claude-opus-5-5` • Thinking:
`high` • Cost: `$0.00`_

<!-- mux-attribution: model=anthropic:claude-opus-5-5 thinking=high
costs=0.00 -->
yermakoffivan pushed a commit to yermakoffivan/mux that referenced this pull request Oct 14, 2026
…view (coder#6587)

## Upgrade effect (read first)

After this PR, every managed plugin's skills and MCP servers stop
loading once, right after the upgrade, until the user reviews them.
"Managed" means a plugin installed from **Settings → Plugins** (a row in
`plugins.json`). Unmanaged plugins (`~/.agents/plugins`, repo plugins)
are not affected.

1. Settings → Plugins shows a "Needs review" badge on each affected
plugin and one notice that says the components do not load until
reviewed, with a **Review** button (coder#6570). The command palette has the
same review command.
2. One review per plugin restores loading. The review uses the existing
update review panel and lists each skill and MCP server.
3. Agents in a workspace see fewer skills and MCP tools until then.
Nothing is deleted: imports, enablement and plugin data stay as they
are.
4. Downgrade: older builds ignore the consent records and load by name,
as before. Upgrading again re-applies the gate.

5. Release text: the settings notice says why the components stop
loading and that each plugin needs one review after the upgrade. The
Agent Skills docs have a "Component review" section, and the MCP servers
docs link to it.

## Why no backfill

coder#6498 is about a newer Xum parsing more of an installed package than the
user saw at install time: an extra Codex skills root, or a stdio server
that an older parser skipped. Those components load under a name the
user approved. A first-run backfill would fingerprint whatever the
current parser sees, which is exactly the content the user never
reviewed. It would turn the bug into a stored approval. So an install
without consent records counts as "nothing reviewed", and the user
reviews once.

## Summary

A managed plugin's skill or MCP server now loads only when a stored
consent record matches its current fingerprint. Closes coder#6498 (part 4 of
the stack, on top of coder#6570).

## Implementation

1. Skills: one predicate, `isPluginSkillConsented`
(`componentConsent.ts`), fingerprints the skill's directory (relative to
the plugin root) plus its parsed frontmatter, as the install review
does. The skill index, `agent_skill_read`, `agent_skill_list` and
`skill://` reads all call it. A refused skill is skipped, so a same-name
skill from another source can still serve.
2. MCP servers: `loadPluginMcpServers` stores `consentFingerprint` on
the plugin provenance. `isPluginMcpServerAllowed` (`registry.ts`)
requires a matching record, in addition to the import and enablement
checks. The provider drops an unreviewed server from the list, and the
manager checks the same rule again at launch.
3. Records come from coder#6534 (install), coder#6557 (review and update) and
coder#6570 (UI).

## Validation

New tests cover refusal and acceptance in the registry, the real MCP
provider (including an `args` change under a reviewed name), the skill
index, `agent_skill_list` and an end-to-end install. The end-to-end test
installs a plugin, sees its skill and server load, strips the consent
records, sees both stop loading, runs `confirmComponentReview`, and sees
both load again. Each new test fails with the gate turned off. Existing
fixtures now record real consent through the production fingerprint
code.

## Risks

1. The upgrade effect above is intended. It is user-visible and needs
one review per managed plugin.
2. A fingerprint mismatch between the install review and the load path
would block a component the user approved. The fixtures compute consent
through the same production functions as the load path, and the
end-to-end test covers the full cycle.

3. A body-only update of a skill (its `SKILL.md` instructions) still
applies without a review, as on main. The consent fingerprint covers the
skill's folder and frontmatter, which closes the coder#6498 case. Body review
is a separate product proposal:
coder#6617.

Size: about 400 production lines changed, the rest is tests and
fixtures.

---

_Generated with `xum` • Model: `anthropic:claude-opus-5-5` • Thinking:
`high` • Cost: `$42.85`_

<!-- mux-attribution: model=anthropic:claude-opus-5-5 thinking=high
costs=42.85 -->

---------

Signed-off-by: Thomas Kosiewski <tk@coder.com>
yermakoffivan pushed a commit to yermakoffivan/mux that referenced this pull request Oct 14, 2026
…stom_header without headerName (coder#6487)

## Summary

Xum now loads three Codex `.mcp.json` shapes that it used to skip or
misreport. A stdio server with `env_vars` loads and receives the named
host variables. A remote entry with a `url` and no `type` loads as
streamable HTTP. An `api_key` auth block with `headerScheme:
custom_header` and no `headerName` is reported as malformed.

Refs coder#6297. This PR covers three of its items: "Codex `env_vars` on
stdio servers", "URL entries without `type`" and "`custom_header`
without `headerName`".

## Background

Codex review found these gaps after track A merged main (review threads
`PRRT_kwDOPxxmWM6rv0UV`, `PRRT_kwDOPxxmWM6rv0Ua` and
`PRRT_kwDOPxxmWM6rv0Uh`).

1. Codex defines `env_vars` as the way to pass selected host environment
variables to a stdio server ([OpenAI plugins
guide](https://developers.openai.com/api/docs/guides/agents-api/tools/plugins)).
Xum rejected the field as unknown, so the server did not load.
2. The Codex plugin parser reads a `url` entry without `type` as
streamable HTTP
([`codex-mcp/src/plugin_config.rs`](https://github.com/openai/codex/blob/main/codex-rs/codex-mcp/src/plugin_config.rs)).
Xum skipped it as an unknown type.
3. OpenAI's auth reference requires `headerName` for `custom_header`
([fixed bearer tokens and API
keys](https://developers.openai.com/plugins/build/plugins#fixed-bearer-tokens-and-api-keys)).
Xum listed the block as an unsupported setting instead of a malformed
one.

## Implementation

All three changes apply to the Codex dialect only. A root `mcp.json`
stays strict: `env_vars` and an untyped `url` are still errors there. A
new test covers that.

**`env_vars`.** The Codex translator turns `env_vars` into a new
`forwardEnv` list of names on the stdio server record. Xum copies the
semantics from Codex:

- `env_vars: null` and `source: null` count as absent, as Codex's
optional fields do.
- An item is a bare name or `{ name, source }` (`McpServerEnvVar` in
[`config/src/mcp_types.rs`](https://github.com/openai/codex/blob/main/codex-rs/config/src/mcp_types.rs)).
- Xum refuses `source: "remote"`. Codex refuses it for a plugin that
runs in the local environment (`bind_environment_env_vars` in
`plugin_config.rs`).
- Xum refuses `env_vars` on a remote server. Codex does the same
(`throw_if_set` in `mcp_types.rs`).
- At launch, `prepareStdioLaunch` copies only the named variables from
the host. It reads only own properties of the host environment, so a
name such as `constructor` is not an inherited value. It skips a name
that the host does not set, as Codex's `create_env_for_mcp_server` does
([`rmcp-client/src/utils.rs`](https://github.com/openai/codex/blob/main/codex-rs/rmcp-client/src/utils.rs)).
A configured `env` value wins over a host value.
- Xum refuses `PLUGIN_ROOT` and `PLUGIN_DATA` in `env_vars`, as it
already does in `env`.

Xum reads only the names at load time. It never stores or shows a host
value.

The install consent preview shows the names, for example `node (host
env: 'API_TOKEN')`. The consent fingerprint includes the names in the
order they first appear (duplicates are dropped), so an update that
adds, removes or reorders a name needs new consent. The server signature
includes them too, so a changed list restarts the server.

**Untyped `url`.** The translator infers `streamable-http` when an entry
has a `url` and no `type`. This mirrors how it already infers `stdio`
from a `command`.

**`custom_header` without `headerName`.** `readAuth` now adds a warning
that `headerName` is required, and it drops `headerScheme`. A bad
`headerName` already used this warning class.

## Validation

- I wrote each test first and saw it fail on `origin/main`. Then I made
it pass.
- `bun test` (pinned Bun 1.4.3) of `mcpConfig.test.ts`,
`openaiMcpAuth.test.ts`, `installService.test.ts` and
`mcpServerManager.test.ts`: 558 pass, 0 fail.
- `make static-check` passes.

## Resolved since the hold

- Consent for newly recognized servers is fixed on main by coder#6498 (stack
coder#6534, coder#6557, coder#6570, coder#6587). A managed plugin's server loads only when
its consent fingerprint matches what the user reviewed. This branch
merges that main: `forwardEnv` is an included field in the fingerprint
table (`componentConsent.ts`), so adding an `env_vars` name changes the
fingerprint and needs a review. The old update-only `stdioFingerprint`
helper is gone, because main's update review uses the same consent
fingerprint.
- The launch environment is fixed on main by coder#6581 (coder#6509). A plugin
stdio server starts from a minimal host environment. `env_vars` names
are copied into the server's declared `env`, so they reach the server on
top of that base. A new test launches a plugin server through the real
host runtime and checks that a named variable arrives and an unnamed one
does not.
- Docs: docs/config/mcp-servers.mdx ("Plugin server environment") now
describes `env_vars`.

## Risks

The risk is low. The `env_vars` and untyped-`url` changes apply only to
Codex `.mcp.json` files, and those entries failed to load before. A
stdio server without `env_vars` keeps the same launch environment,
fingerprint and signature, because each of them leaves out an unset
`forwardEnv`. Adding `forwardEnv` to the consent fingerprint does not
change the fingerprint of an existing server, because `pick` skips an
unset field.

---

_Generated with `xum` • Model: `anthropic:claude-opus-5-5` • Thinking:
`high` • Cost: `$0.00`_

<!-- mux-attribution: model=anthropic:claude-opus-5-5 thinking=high
costs=0.00 -->
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.

1 participant