Skip to content

feat(plugins): Add universal plugin support#113

Open
dcramer wants to merge 35 commits into
mainfrom
codex/add-plugin-support
Open

feat(plugins): Add universal plugin support#113
dcramer wants to merge 35 commits into
mainfrom
codex/add-plugin-support

Conversation

@dcramer

@dcramer dcramer commented Jun 12, 2026

Copy link
Copy Markdown
Member

This adds first-class plugin support using .agents/plugins as the canonical local layout while treating the Codex plugin manifest and marketplace shape as the generalized input model. Plugin declarations now flow through config loading, lockfiles, install, sync, list, doctor, gitignore handling, and specs/docs.

The implementation separates the host runtime concerns so the plugin work is easier to review and maintain: targets/ owns agent definitions plus MCP/hook config writers, subagents/ owns subagent source/runtime handling, and plugins/ owns plugin schema, storage, target selection, and runtime projection. The old agents/index.ts remains as a compatibility barrel, but the implementation no longer concentrates plugins, subagents, target definitions, and config writers in one overloaded module tree.

Plugin runtime output stays intentionally narrow per target. Claude and Cursor get generated native manifests inside each canonical plugin bundle plus runtime marketplaces whose local sources resolve relative to their marketplace files (../.agents/plugins/<name>). Codex gets the repo-scoped .agents/plugins/marketplace.json with file-relative { "source": "local", "path": "./<name>" } entries plus a generated .codex-plugin/plugin.json; Grok gets a managed bundle copy. OpenCode does not get generated JavaScript or TypeScript plugin modules from dotagents bundles; supported bundle skills are symlinked into .opencode/skills/ and Markdown agents into .opencode/agents/. Pi support is limited to the surface it can consume today: plugin skills are symlinked into .agents/skills/.

The review and QA passes tightened the edge cases around that model: unsupported marketplace extension sources are skipped instead of crashing discovery, marketplace-local selectors are resolved from the marketplace file directory while still enforcing source-root containment, frozen wildcard lockfile skill names are validated before resolution/copy, plugin discovery rejects symlinks that resolve outside the source root, runtime component paths are checked before manifest/projection generation, OpenCode and Pi component projections avoid unmanaged collisions and repair dangling managed links, .agents/.gitignore ignores projected Pi skill symlinks without hiding user-authored colliding skills or same-project plugin bundles, install preserves resolved lock entries before canonical subagent file writes, failed plugin replacement keeps the backup if rollback cannot restore it, missing plugin source roots now report controlled errors with the filesystem cause attached, and Claude plugin manifests no longer project the invalid agents field.

Validation includes focused plugin runtime/store/schema regressions covering unsafe component paths, marketplace-file-relative local selectors, Pi skill-name traversal, canonical/explicit/marketplace plugin symlink escapes, failed plugin replacement backup preservation, and missing source-root controlled error reporting; affected install/sync/remove/doctor/gitignore tests including the same-project plugin gitignore and concurrent doctor error cases; pnpm qa:example; Docker-backed OpenCode QA proving opencode debug skill and opencode agent list can see projected plugin components; pnpm --filter @sentry/dotagents test src/plugins src/cli/commands/doctor.test.ts --testTimeout 90000; pnpm --filter @sentry/dotagents lint; and pnpm --filter @sentry/dotagents typecheck. Garfield passed after the latest checkpoints; Garfield Codify did not recommend new tooling beyond the regressions already added.

@vercel

vercel Bot commented Jun 12, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
dotagents Ready Ready Preview, Comment Jul 7, 2026 8:34pm

Request Review

Comment thread packages/dotagents/src/agents/plugin-writer.ts Outdated
Comment thread packages/dotagents/src/cli/commands/sync.ts
Comment thread packages/dotagents/src/agents/plugin-writer.ts Outdated
Comment thread packages/dotagents/src/cli/commands/doctor.ts
Comment thread packages/dotagents/src/plugins/store.ts
Comment thread packages/dotagents/src/cli/commands/install.ts Outdated
Comment thread packages/dotagents/src/cli/commands/sync.ts
Comment thread packages/dotagents/src/agents/plugin-store.ts Outdated
Comment thread packages/dotagents/src/plugins/store.ts
Comment thread packages/dotagents/src/cli/commands/install.ts Outdated
Comment thread packages/dotagents/src/cli/commands/sync.ts
Comment thread packages/dotagents/src/agents/plugin-writer.ts Outdated
Comment thread packages/dotagents/src/cli/commands/install.ts Outdated
Comment thread packages/dotagents/src/cli/commands/sync.ts
Comment thread packages/dotagents/src/plugins/store.ts
Comment thread packages/dotagents/src/cli/commands/sync.ts
Comment thread packages/dotagents/src/cli/commands/doctor.ts
Comment thread packages/dotagents/src/plugins/store.ts Outdated
Comment thread packages/dotagents/src/cli/commands/sync.ts
Comment thread packages/dotagents/src/plugins/store.ts
Comment thread packages/dotagents/src/cli/commands/install.ts Outdated
Comment thread packages/dotagents/src/cli/commands/remove.ts
Comment thread packages/dotagents/src/plugins/schema.ts
Skip malformed marketplace files during plugin discovery so conventional fallback paths can still resolve valid plugins.

When removing plugins, use the removed config block as ownership evidence in addition to lockfile entries. This lets remove clean up installed plugin bundles and projections even if agents.lock is missing that plugin row.

Co-Authored-By: Codex <noreply@openai.com>
Remove the broad runtime-auth QA reference and keep runtime guidance focused on plugin verification.

Collapse repeated unmanaged plugin manifest tests into one table-driven case while preserving per-runtime coverage.

Co-Authored-By: Codex <noreply@openai.com>
Comment thread packages/dotagents/src/plugins/store.ts
Comment thread packages/dotagents/src/plugins/runtime/marketplace.ts
Generate plugin marketplace sources relative to each marketplace file directory so projected marketplace entries resolve when consumed directly.

Co-Authored-By: OpenAI Codex <noreply@openai.com>
Allow marketplace-local plugin selectors to resolve relative to nested marketplace files while still enforcing source-root containment.

Update generated marketplace expectations and plugin specs to match the file-relative path contract.

Co-Authored-By: OpenAI Codex <noreply@openai.com>
Comment thread packages/dotagents/src/plugins/schema.ts Outdated
Only local marketplace selectors need file-relative traversal. Keep unsupported extension source paths on the stricter component path schema and clarify the discovery docs.

Co-Authored-By: OpenAI Codex <noreply@openai.com>
Comment thread packages/dotagents/src/plugins/store.ts Outdated
Allow non-local marketplace source objects without local paths so unsupported entries do not invalidate the entire marketplace file.

Keep local source objects strict and add regression coverage for mixed marketplace files.

Co-Authored-By: OpenAI Codex <noreply@openai.com>
Exercise the unsupported-source skip path with a matching plugin name before the valid local marketplace entry.

Co-Authored-By: OpenAI Codex <noreply@openai.com>

@cursor cursor 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.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 3d25295. Configure here.

Comment thread packages/dotagents/src/cli/commands/doctor.ts
Aggregate project-scope plugin doctor diagnostics so same-project declarations do not hide missing external plugin installs.

Co-Authored-By: OpenAI Codex <noreply@openai.com>
Keep the existing plugin backup when replacing an installed plugin fails and rollback cannot restore it. This avoids deleting the only recoverable copy after a partial install failure.

Also convert missing plugin source roots into a controlled error that preserves the original filesystem cause.

Co-Authored-By: OpenAI Codex <noreply@openai.com>
Comment thread packages/dotagents/src/plugins/store.ts
Assert plugin source-root realpath failures use the controlled error path and preserve the original filesystem cause.

Co-Authored-By: OpenAI Codex <noreply@openai.com>
Comment on lines +91 to +92
written += await writeComponentProjections("opencode", agentIds, selected, projectRoot, warnings);
written += await writeComponentProjections("pi", agentIds, selected, projectRoot, warnings);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Plugin bundle symlinks escape containment and expose arbitrary host files to AI agents

A third-party plugin (e.g. a git-sourced bundle) can include a symlink such as agents/evil.md -> ../../../../../etc/passwd or -> /proc/self/environ; installPluginBundle copies it verbatim (cp defaults to dereference:false) and writeComponentProjections links .opencode/agents/evil.md to it without a realpath boundary check, so agents reading the projected component follow the chain out of the plugin root to arbitrary host files. Dereference/realpath containment should be enforced on plugin component files during install and projection.

Evidence
  • copyDir (packages/dotagents-lib/src/utils/fs.ts) calls cp(src, dest, { recursive: true, filter }) with no dereference, and grep shows dereference is never set anywhere, so symlinks inside a plugin bundle are copied with their original (possibly out-of-bundle) targets by installPluginBundle in store.ts.
  • markdownComponentLinks accepts entry.isSymbolicLink() entries and builds a ComponentLink{ sourcePath: join(agentsDir, entry.name) } with no realpath check against the plugin root; skillComponentLinks likewise allows symlink dir entries needing only a resolvable SKILL.md.
  • writeManagedComponentLink creates .opencode/agents/<name> as a symlink to sourcePath, inheriting the escaping chain to the out-of-bundle target.
  • assertInsideSourceRoot (realpath-based) is applied to the plugin directory during discovery, not to individual symlinked files under agents//skills/, so relative/absolute traversal symlinks in the bundle are never contained.
  • Boundary: an untrusted plugin author should be confined to the plugin bundle; the chain lets projected components read arbitrary developer files (SSH keys, env, /etc).
Also found at 1 additional location
  • packages/dotagents/src/plugins/store.ts:171

Identified by Warden security-review · EPX-SL3

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk: high PR risk score: high

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants