Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/honest-cursors-validate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"agent-bundle": minor
---

Add honest Cursor plugin host validation that probes the installed CLI for version evidence while always validating generated bytes locally against the vendored pinned schemas and loader contract.
9 changes: 9 additions & 0 deletions docs/diagnostics.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,15 @@ gate a build, a validation, or a dev rebuild.
| `AB8xxx` | Development server configuration. |
| `AB9xxx` | Eval selection, harnesses, and persisted runs. |

## Cursor built-artifact validation (`AB6026`–`AB6029`)

| Code | Severity | Trigger | Recovery |
| --- | --- | --- | --- |
| `AB6026` | info | Every Cursor host-validation report states that Cursor publishes no plugin-validate devtools verb and names the vendored schema pin used for local validation. | Review the pinned Cursor schema provenance before changing the local validator contract. |
| `AB6027` | error | A required generated Cursor document is missing or a present plugin, marketplace, MCP, or hooks document is unreadable, invalid JSON, or rejected by its pinned schema. | Repair the generated Cursor JSON document so it satisfies the vendored pinned schema, then rebuild. |
| `AB6028` | error | Generated bytes violate pinned Cursor loader evidence: manifest-candidate precedence selects a fallback manifest, a symlink resolves outside the bundle, or `CURSOR_PLUGIN_ROOT` appears outside loader-substituted fields. | Repair the generated Cursor layout, token locations, or symlinks to match the pinned loader evidence, then rebuild. |
| `AB6029` | info / warning | The Cursor Agent version probe is unavailable (`ENOENT`, info) or cannot complete successfully (warning). Local pinned-schema validation still runs. | Install Cursor Agent or repair `cursor-agent --version` when local CLI version evidence is required, then rerun artifact validation. |

## Codex host validation (`AB6030`–`AB6033`)

Codex 0.147.0 publishes plugin installation commands but no plugin-validation
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@
"Installed cursor-agent-exec loader candidates: .cursor-plugin/plugin.json, .claude-plugin/plugin.json, plugin.json.",
"Installed loader substitutes CURSOR_PLUGIN_ROOT in MCP command, args, env, and cwd fields and in hook commands.",
"Local-plugin symlinks are realpath checked and rejected when their targets escape ~/.cursor/plugins/local.",
"2026-09-02: /home/zack/.local/bin/cursor-agent --version exited 0 and printed exactly `2026.08.31-4057e58`.",
"2026-09-02: /home/zack/.local/bin/cursor-agent --help exited 0, listed `plugin` as `Manage plugins and plugin marketplaces`, and listed no `validate` command; `cursor-agent plugin --help` exited 0 and exposed only the `marketplace` subcommand, while `cursor-agent plugins --help` exited 0 by returning the top-level help. No plugin-validate devtools verb was exposed.",
"2026-09-01: cursor/plugins@070189284e702e8a4d2e3cc8913994b204c5337a schemas/plugin.schema.json defines the commands component pointer; https://cursor.com/docs documents agent chat commands as plain Markdown prompt files in commands/ named by filename.",
"2026-08-31: cursor/plugins@070189284e702e8a4d2e3cc8913994b204c5337a schemas/plugin.schema.json defines the rules component pointer; https://cursor.com/docs/plugins documents the rules component.",
"2026-09-02: https://cursor.com/docs/hooks#workspaceopen documents workspaceOpen input as sessionless ({ hook_event_name, cursor_version, workspace_roots, user_email }; conversation_id/generation_id/model/session_id/transcript_path omitted) and its output pluginPaths as string[] (optional), so an empty response is legal; the generated event-route wrapper validates that envelope and projects the observation-only canonical workspace/open family to no output — the native pluginPaths return channel is deliberately not modeled and never emitted.",
Expand Down
32 changes: 23 additions & 9 deletions packages/agent-bundle/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,10 @@ import {
validateCodexPlugin,
type CodexPluginValidationReport,
} from './host-contracts/codex-plugin-validation.ts';
import {
validateCursorPlugin,
type CursorPluginValidationReport,
} from './host-contracts/cursor-plugin-validation.ts';
import type { EvalComparison } from './eval/compare.ts';
import { EvalRunStoreError } from './eval/errors.ts';
import {
Expand Down Expand Up @@ -169,8 +173,8 @@ export type {
NativeHost,
RedactedEventEnvelope,
} from './host-contracts/host-contract.ts';
export { validateClaudePlugin, validateCodexPlugin };
export type { ClaudePluginValidationReport, CodexPluginValidationReport };
export { validateClaudePlugin, validateCodexPlugin, validateCursorPlugin };
export type { ClaudePluginValidationReport, CodexPluginValidationReport, CursorPluginValidationReport };

export { HookService } from './services/hook-service.ts';
export type { HookListOptions, HookSimulationOptions } from './services/hook-service.ts';
Expand Down Expand Up @@ -248,7 +252,11 @@ export interface ValidateOptions extends ProjectOptions {

export interface ValidateResult {
readonly diagnostics: readonly Diagnostic[];
readonly hostValidation?: readonly (ClaudePluginValidationReport | CodexPluginValidationReport)[];
readonly hostValidation?: readonly (
| ClaudePluginValidationReport
| CodexPluginValidationReport
| CursorPluginValidationReport
)[];
readonly model?: NormalizedPlugin;
}

Expand Down Expand Up @@ -484,18 +492,24 @@ export const validate = async (options: ValidateOptions): Promise<ValidateResult
return Object.freeze({ diagnostics: freezeDiagnostics(validated.diagnostics) });
}
const reports = await Promise.all(validated.snapshot.manifest.targets
.filter((target) => target.name === 'claude' || target.name === 'codex' || target.name === 'plugin')
.filter((target) =>
target.name === 'claude' || target.name === 'codex' || target.name === 'cursor' || target.name === 'plugin')
.map((target) => target.name === 'codex'
? validateCodexPlugin({
pluginDirectory: join(artifact, target.name),
strict: options.strict,
target: target.name,
})
: validateClaudePlugin({
pluginDirectory: join(artifact, target.name),
strict: options.strict,
target: target.name,
})));
: target.name === 'cursor'
? validateCursorPlugin({
pluginDirectory: join(artifact, target.name),
target: target.name,
})
: validateClaudePlugin({
pluginDirectory: join(artifact, target.name),
strict: options.strict,
target: target.name,
})));
return Object.freeze({
diagnostics: freezeDiagnostics([
...validated.diagnostics,
Expand Down
Loading
Loading