From c90a2fb234c14b2682bb458cfa6ccde1c5d07e3b Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Thu, 3 Sep 2026 00:39:20 +0000 Subject: [PATCH] feat(claude): record agents-component parity evidence under G5 gate --- .changeset/quiet-agents-evidence.md | 5 + .../adapters/capabilities/claude-2.1.250.json | 125 ++++++++++++++++++ packages/agent-bundle/src/adapters/claude.ts | 9 +- packages/agent-bundle/src/adapters/plugin.ts | 18 ++- .../adapters/schemas/claude/PROVENANCE.json | 1 + .../tests/adapter-capability-states.test.ts | 62 +++++++++ .../tests/adapter-metadata.test.ts | 7 +- .../tests/host-adapters.native.test.ts | 70 ++++++++++ 8 files changed, 293 insertions(+), 4 deletions(-) create mode 100644 .changeset/quiet-agents-evidence.md diff --git a/.changeset/quiet-agents-evidence.md b/.changeset/quiet-agents-evidence.md new file mode 100644 index 000000000..f5160fe63 --- /dev/null +++ b/.changeset/quiet-agents-evidence.md @@ -0,0 +1,5 @@ +--- +"agent-bundle": patch +--- + +Record Claude plugin-agent capability and native validation evidence while keeping agents directory emission deliberately deferred by the G5 gate. diff --git a/packages/agent-bundle/src/adapters/capabilities/claude-2.1.250.json b/packages/agent-bundle/src/adapters/capabilities/claude-2.1.250.json index d18b30e6a..4a55e52e2 100644 --- a/packages/agent-bundle/src/adapters/capabilities/claude-2.1.250.json +++ b/packages/agent-bundle/src/adapters/capabilities/claude-2.1.250.json @@ -47,6 +47,131 @@ }, "observedCliVersion": "2.1.250", "plugin": { + "agents": { + "component": { + "state": "unavailable", + "reason": "The #100 stage-2 G5 gate, merged in PR #220 and preserved by #107 revision 3, deliberately defers Agent Bundle's authored plugin agents/ component until a separate evidence-backed increment.", + "evidence": [ + "retrieved 2026-09-02: https://code.claude.com/docs/en/plugins-reference documents plugin agents as Markdown files in the plugin-root agents/ directory, recursively scanned and registered under plugin-scoped identifiers.", + "retrieved 2026-09-02: https://github.com/ScriptedAlchemy/agent-bundle/pull/220 and https://github.com/ScriptedAlchemy/agent-bundle/issues/107 revision 3 record the #100 stage-2 G5 narrowing: re-admitting agents requires its own evidence-backed increment, so this compiler emits no agents/ directory." + ] + }, + "name": { + "state": "unavailable", + "reason": "The #100 stage-2 G5 gate, merged in PR #220 and preserved by #107 revision 3, deliberately withholds an authored plugin-agent name field together with agents/ emission.", + "evidence": [ + "retrieved 2026-09-02: https://code.claude.com/docs/en/sub-agents documents name as the agent identifier using lowercase letters and hyphens, while https://code.claude.com/docs/en/plugins-reference documents that a plugin agent without name falls back to its filename.", + "retrieved 2026-09-02: https://github.com/ScriptedAlchemy/agent-bundle/pull/220 and https://github.com/ScriptedAlchemy/agent-bundle/issues/107 revision 3 keep every authored plugin-agent field behind the #100 stage-2 G5 re-admission increment." + ] + }, + "description": { + "state": "unavailable", + "reason": "The #100 stage-2 G5 gate, merged in PR #220 and preserved by #107 revision 3, deliberately withholds an authored plugin-agent description field together with agents/ emission.", + "evidence": [ + "retrieved 2026-09-02: https://code.claude.com/docs/en/sub-agents documents description as the delegation signal that tells Claude when to invoke the subagent.", + "retrieved 2026-09-02: https://github.com/ScriptedAlchemy/agent-bundle/pull/220 and https://github.com/ScriptedAlchemy/agent-bundle/issues/107 revision 3 keep every authored plugin-agent field behind the #100 stage-2 G5 re-admission increment." + ] + }, + "model": { + "state": "unavailable", + "reason": "The #100 stage-2 G5 gate, merged in PR #220 and preserved by #107 revision 3, deliberately withholds an authored plugin-agent model field together with agents/ emission.", + "evidence": [ + "retrieved 2026-09-02: https://code.claude.com/docs/en/sub-agents documents model as sonnet, opus, haiku, fable, a full model ID, or inherit, with host model-order selection when omitted.", + "retrieved 2026-09-02: https://github.com/ScriptedAlchemy/agent-bundle/pull/220 and https://github.com/ScriptedAlchemy/agent-bundle/issues/107 revision 3 keep every authored plugin-agent field behind the #100 stage-2 G5 re-admission increment." + ] + }, + "effort": { + "state": "unavailable", + "reason": "The #100 stage-2 G5 gate, merged in PR #220 and preserved by #107 revision 3, deliberately withholds an authored plugin-agent effort field together with agents/ emission.", + "evidence": [ + "retrieved 2026-09-02: https://code.claude.com/docs/en/sub-agents documents effort as a per-agent override of the session effort level with low, medium, high, xhigh, and max values subject to model availability.", + "retrieved 2026-09-02: https://github.com/ScriptedAlchemy/agent-bundle/pull/220 and https://github.com/ScriptedAlchemy/agent-bundle/issues/107 revision 3 keep every authored plugin-agent field behind the #100 stage-2 G5 re-admission increment." + ] + }, + "maxTurns": { + "state": "unavailable", + "reason": "The #100 stage-2 G5 gate, merged in PR #220 and preserved by #107 revision 3, deliberately withholds an authored plugin-agent maxTurns field together with agents/ emission.", + "evidence": [ + "retrieved 2026-09-02: https://code.claude.com/docs/en/sub-agents documents maxTurns as the maximum agentic turns before the subagent stops and returns partial output that can be resumed.", + "retrieved 2026-09-02: https://github.com/ScriptedAlchemy/agent-bundle/pull/220 and https://github.com/ScriptedAlchemy/agent-bundle/issues/107 revision 3 keep every authored plugin-agent field behind the #100 stage-2 G5 re-admission increment." + ] + }, + "tools": { + "state": "unavailable", + "reason": "The #100 stage-2 G5 gate, merged in PR #220 and preserved by #107 revision 3, deliberately withholds an authored plugin-agent tools field together with agents/ emission.", + "evidence": [ + "retrieved 2026-09-02: https://code.claude.com/docs/en/sub-agents documents tools as an allowlist, with all tools available to subagents inherited when the field is omitted.", + "retrieved 2026-09-02: https://github.com/ScriptedAlchemy/agent-bundle/pull/220 and https://github.com/ScriptedAlchemy/agent-bundle/issues/107 revision 3 keep every authored plugin-agent field behind the #100 stage-2 G5 re-admission increment." + ] + }, + "disallowedTools": { + "state": "unavailable", + "reason": "The #100 stage-2 G5 gate, merged in PR #220 and preserved by #107 revision 3, deliberately withholds an authored plugin-agent disallowedTools field together with agents/ emission.", + "evidence": [ + "retrieved 2026-09-02: https://code.claude.com/docs/en/sub-agents documents disallowedTools as a denylist removed from the inherited or explicitly selected tool pool.", + "retrieved 2026-09-02: https://github.com/ScriptedAlchemy/agent-bundle/pull/220 and https://github.com/ScriptedAlchemy/agent-bundle/issues/107 revision 3 keep every authored plugin-agent field behind the #100 stage-2 G5 re-admission increment." + ] + }, + "skills": { + "state": "unavailable", + "reason": "The #100 stage-2 G5 gate, merged in PR #220 and preserved by #107 revision 3, deliberately withholds an authored plugin-agent skills field together with agents/ emission.", + "evidence": [ + "retrieved 2026-09-02: https://code.claude.com/docs/en/sub-agents documents skills as startup preloads whose full content is injected into the subagent context without preventing invocation of other discoverable skills.", + "retrieved 2026-09-02: https://github.com/ScriptedAlchemy/agent-bundle/pull/220 and https://github.com/ScriptedAlchemy/agent-bundle/issues/107 revision 3 keep every authored plugin-agent field behind the #100 stage-2 G5 re-admission increment." + ] + }, + "memory": { + "state": "unavailable", + "reason": "The #100 stage-2 G5 gate, merged in PR #220 and preserved by #107 revision 3, deliberately withholds an authored plugin-agent memory field together with agents/ emission.", + "evidence": [ + "retrieved 2026-09-02: https://code.claude.com/docs/en/sub-agents documents memory as persistent cross-session learning scoped to user, project, or local storage.", + "retrieved 2026-09-02: https://github.com/ScriptedAlchemy/agent-bundle/pull/220 and https://github.com/ScriptedAlchemy/agent-bundle/issues/107 revision 3 keep every authored plugin-agent field behind the #100 stage-2 G5 re-admission increment." + ] + }, + "background": { + "state": "unavailable", + "reason": "The #100 stage-2 G5 gate, merged in PR #220 and preserved by #107 revision 3, deliberately withholds an authored plugin-agent background field together with agents/ emission.", + "evidence": [ + "retrieved 2026-09-02: https://code.claude.com/docs/en/sub-agents documents background: true as forcing the subagent to remain in the background even when Claude requests a foreground run.", + "retrieved 2026-09-02: https://github.com/ScriptedAlchemy/agent-bundle/pull/220 and https://github.com/ScriptedAlchemy/agent-bundle/issues/107 revision 3 keep every authored plugin-agent field behind the #100 stage-2 G5 re-admission increment." + ] + }, + "isolationWorktree": { + "state": "unavailable", + "reason": "The #100 stage-2 G5 gate, merged in PR #220 and preserved by #107 revision 3, deliberately withholds authored plugin-agent isolation: worktree together with agents/ emission.", + "evidence": [ + "retrieved 2026-09-02: https://code.claude.com/docs/en/plugins-reference documents worktree as the only valid isolation value for plugin agents, and https://code.claude.com/docs/en/sub-agents documents that it runs the subagent in a temporary isolated git worktree.", + "retrieved 2026-09-02: https://github.com/ScriptedAlchemy/agent-bundle/pull/220 and https://github.com/ScriptedAlchemy/agent-bundle/issues/107 revision 3 keep every authored plugin-agent field behind the #100 stage-2 G5 re-admission increment." + ] + }, + "hooks": { + "state": "unavailable", + "reason": "The #100 stage-2 G5 gate, merged in PR #220 and preserved by #107 revision 3, deliberately defers the plugin-agent surface on which Agent Bundle would omit hooks under the host security contract.", + "evidence": [ + "retrieved 2026-09-02: https://code.claude.com/docs/en/plugins-reference and https://code.claude.com/docs/en/sub-agents document that hooks is unsupported and ignored when an agent is loaded from a plugin.", + "retrieved 2026-09-02: local Claude Code 2.1.257 `claude plugin validate --strict` accepts a plugin agent declaring hooks without warning, proving the validator does not enforce the loader's ignore contract.", + "retrieved 2026-09-02: https://github.com/ScriptedAlchemy/agent-bundle/pull/220 and https://github.com/ScriptedAlchemy/agent-bundle/issues/107 revision 3 keep plugin-agent omission enforcement behind the #100 stage-2 G5 re-admission increment." + ] + }, + "mcpServers": { + "state": "unavailable", + "reason": "The #100 stage-2 G5 gate, merged in PR #220 and preserved by #107 revision 3, deliberately defers the plugin-agent surface on which Agent Bundle would omit mcpServers under the host security contract.", + "evidence": [ + "retrieved 2026-09-02: https://code.claude.com/docs/en/plugins-reference and https://code.claude.com/docs/en/sub-agents document that mcpServers is unsupported and ignored when an agent is loaded from a plugin.", + "retrieved 2026-09-02: local Claude Code 2.1.257 `claude plugin validate --strict` accepts a plugin agent declaring mcpServers without warning, proving the validator does not enforce the loader's ignore contract.", + "retrieved 2026-09-02: https://github.com/ScriptedAlchemy/agent-bundle/pull/220 and https://github.com/ScriptedAlchemy/agent-bundle/issues/107 revision 3 keep plugin-agent omission enforcement behind the #100 stage-2 G5 re-admission increment." + ] + }, + "permissionMode": { + "state": "unavailable", + "reason": "The #100 stage-2 G5 gate, merged in PR #220 and preserved by #107 revision 3, deliberately defers the plugin-agent surface on which Agent Bundle would omit permissionMode under the host security contract.", + "evidence": [ + "retrieved 2026-09-02: https://code.claude.com/docs/en/plugins-reference and https://code.claude.com/docs/en/sub-agents document that permissionMode is unsupported and ignored when an agent is loaded from a plugin.", + "retrieved 2026-09-02: local Claude Code 2.1.257 `claude plugin validate --strict` accepts a plugin agent declaring permissionMode: bypassPermissions without warning, proving the validator does not enforce the loader's ignore contract.", + "retrieved 2026-09-02: https://github.com/ScriptedAlchemy/agent-bundle/pull/220 and https://github.com/ScriptedAlchemy/agent-bundle/issues/107 revision 3 keep plugin-agent omission enforcement behind the #100 stage-2 G5 re-admission increment." + ] + } + }, "bin": { "bareCommands": true, "bashPath": true, diff --git a/packages/agent-bundle/src/adapters/claude.ts b/packages/agent-bundle/src/adapters/claude.ts index 83f373b58..400faa51b 100644 --- a/packages/agent-bundle/src/adapters/claude.ts +++ b/packages/agent-bundle/src/adapters/claude.ts @@ -424,12 +424,18 @@ const hookContract = Object.freeze({ wrapperSource: (entry) => nativeHookWrapperSource(entry, 'Claude'), } satisfies TargetHookContract); const metadata = Object.freeze({ - adapterRevision: '1.17.0', + adapterRevision: '1.18.0', observedVersion: capabilityTable.observedCliVersion, schemas: schemaDescriptorsFrom(schemaProvenance, schemaProvenance.observedCliVersion), }); const evidence = capabilityEvidence(claudeName, metadata); const distributionPolicy = capabilityTable.plugin.distributionPolicy; +const agentCapabilities = Object.freeze(Object.fromEntries( + Object.entries(capabilityTable.plugin.agents).map(([rowName, row]) => [ + rowName === 'component' ? 'agents' : `agents.${rowName}`, + unavailableCapability(row.reason), + ]), +)); export const claudeArtifactValidation = deepFreeze({ documents: [ @@ -3138,6 +3144,7 @@ export const claudeAdapter: TargetAdapter = Object.freeze({ artifactValidation: claudeArtifactValidation, artifactLayout, capabilities: Object.freeze({ + ...agentCapabilities, ...eventRouteCapabilitiesFrom(capabilityTable.hooks.eventRoutes, evidence), bin: capabilityStateFromSupport( capabilityTable.plugin.bin.directory === 'bin' && diff --git a/packages/agent-bundle/src/adapters/plugin.ts b/packages/agent-bundle/src/adapters/plugin.ts index 0ae9acc2f..974b71b3f 100644 --- a/packages/agent-bundle/src/adapters/plugin.ts +++ b/packages/agent-bundle/src/adapters/plugin.ts @@ -186,7 +186,7 @@ const artifactValidation = deepFreeze({ }); const metadata = Object.freeze({ - adapterRevision: '1.15.0', + adapterRevision: '1.16.0', observedVersion: `${claudeAdapter.metadata.observedVersion}+${codexAdapter.metadata.observedVersion}+${cursorAdapter.metadata.observedVersion}`, // Metadata schemas must exactly match the validation contract: each host's // documents, with one shared Claude-format hook schema (the pinned Codex @@ -579,10 +579,26 @@ const componentCapabilities = Object.freeze(Object.fromEntries( ]), )); +const agentCapabilities = Object.freeze(Object.fromEntries( + Object.keys(claudeCapabilityTable.plugin.agents).map((rowName) => { + const capability = rowName === 'component' ? 'agents' : `agents.${rowName}`; + return [ + capability, + intersectCapabilityStates( + claudeAdapter.capabilities[capability]!, + unavailableCapability( + 'The pinned Codex and Cursor plugin contracts publish no shared plugin agents component or agent-frontmatter surface.', + ), + ), + ]; + }), +)); + export const pluginAdapter: TargetAdapter = Object.freeze({ artifactValidation, artifactLayout, capabilities: Object.freeze({ + ...agentCapabilities, ...compositeEventCapabilities, bin: unavailableCapability( 'The unified bundle emits the Claude-only bin directory, but the pinned Codex and Cursor contracts declare no shared plugin executable surface.', diff --git a/packages/agent-bundle/src/adapters/schemas/claude/PROVENANCE.json b/packages/agent-bundle/src/adapters/schemas/claude/PROVENANCE.json index 2b555e2c6..0633e0aab 100644 --- a/packages/agent-bundle/src/adapters/schemas/claude/PROVENANCE.json +++ b/packages/agent-bundle/src/adapters/schemas/claude/PROVENANCE.json @@ -4,6 +4,7 @@ "schemaSource": "https://docs.anthropic.com/en/docs/claude-code/plugins", "notes": "lsp.schema.json and plugin.json's `lspServers` property were pinned from the Claude Code 2.1.x plugin reference LSP servers section (retrieved 2026-09-01), which documents `.lsp.json` at the plugin root or inline `lspServers` in the manifest, required `command` / `extensionToLanguage`, and the optional `args`, `transport`, `env`, `initializationOptions`, `settings`, `workspaceFolder`, `startupTimeout`, `shutdownTimeout`, `restartOnCrash`, `maxRestarts`, and `diagnostics` fields. `restartOnCrash` and `shutdownTimeout` require Claude Code v2.1.205 or later, which the pinned 2.1.250 revision satisfies. Manifest `lspServers` keeps the documented `string|array|object` union rather than being narrowed to the one emitted form the way `hooks` is; the emitted document itself is `.lsp.json` at the plugin root. Two agent-bundle tightenings over the documented text: a server map and an `extensionToLanguage` map must both be nonempty, because an empty map claims no extension and can never start a server. The current hooks reference at https://code.claude.com/docs/en/hooks supplies the SubagentStart/SubagentStop wire and decision evidence recorded in claude-2.1.250.json. settings.schema.json was pinned (retrieved 2026-09-01) from the \"Ship default settings with your plugin\" section of https://code.claude.com/docs/en/plugins and the file-locations row of https://code.claude.com/docs/en/plugins-reference, which bound the plugin-root settings.json to the `agent` and `subagentStatusLine` keys, plus https://code.claude.com/docs/en/statusline for the subagentStatusLine command-object shape. Three agent-bundle tightenings over the documented text: the closed schema rejects the unknown keys the host \"silently ignores\", so a requested default never disappears at runtime; minProperties 1 rejects an empty settings.json, which declares no default configuration at all; and subagentStatusLine admits only the two fields its own examples show (`type` and `command`) - statusLine's optional `padding` is documented for the user status line, never for the plugin default, so it stays out of the pinned shape. The plugins-reference placeholder table (\"Which fields substitute them inline depends on the plugin component\") lists Skill and agent content, hook and monitor commands, MCP servers, and LSP servers but not settings.json, so the adapter rejects Agent Bundle path tokens in settings values rather than emitting a placeholder Claude Code never resolves. plugin.json's `userConfig` property and closed `userConfigOption` definition were pinned from https://code.claude.com/docs/en/plugins-reference (retrieved 2026-09-01). Agent Bundle deliberately tightens the documented \"valid identifiers\" requirement to `^[A-Za-z_][A-Za-z0-9_]*$`, rejects option keys that collide after uppercasing because both would map to one `CLAUDE_PLUGIN_OPTION_` environment variable, requires the userConfig record to be nonempty, and rejects `sensitive: true` with `default` because a baked-in secure-storage default would ship a secret in the manifest. plugin.schema.json's `dependencies` property was pinned (retrieved 2026-09-01) from https://code.claude.com/docs/en/plugin-dependencies and the manifest schema in https://code.claude.com/docs/en/plugins-reference: a nonempty array whose entries are nonempty plugin-name strings or closed objects with required name and optional version and marketplace strings. Agent Bundle tightens dependency names to the manifest's existing lowercase kebab-case name pattern, rejects an empty array, and closes object fields so malformed declarations fail before distribution; semver range grammar remains plan-time validation because JSON Schema cannot honestly encode npm range syntax. plugin.schema.json's `displayName`, `metadata`, and `defaultEnabled` properties were pinned from https://code.claude.com/docs/en/plugins-reference (retrieved 2026-09-01). Agent Bundle tightens Claude Code's warning-and-ignore handling for a non-object `metadata` value into build error claude.manifest.metadata.invalid, rejects an empty or whitespace-only `displayName` with claude.manifest.displayName.invalid, and rejects a non-boolean `defaultEnabled` with claude.manifest.defaultEnabled.invalid. The normalized generic model currently carries description but not homepage, repository, license, keywords, or `$schema`, so this slice deliberately emits only the three new Claude host-config fields and does not widen the generic model. Component path fields are deliberately excluded from the emitted schema and config surface: the generator owns the canonical default commands/, skills/, hooks/hooks.json, .mcp.json, .lsp.json, settings.json, workflows/, and output-styles/ layout, while custom replace/add path rules remain documented host-discovery evidence in claude-2.1.250.json. plugin.json's `channels` property was pinned from the Channels section of https://code.claude.com/docs/en/plugins-reference (retrieved 2026-09-01): a nonempty array of closed declarations with required nonempty `server` and optional per-channel `userConfig` reusing the top-level option definition. Agent Bundle tightens the documented contract by rejecting an empty channels array, empty per-channel userConfig, unknown channel fields, invalid or uppercase-colliding option identifiers, and any server name absent from the MCP server map successfully planned for the selected target. Duplicate channel declarations for one server remain allowed and preserve authored order because the reference imposes no uniqueness rule. Per-channel `sensitive: true` follows the top-level secure-storage semantics because the reference says the channel field uses the same schema; the existing prohibition on sensitive defaults therefore applies unchanged. Claude Code 2.1.257 strict validation accepts both valid bindings and deliberately dangling server names, so claude.channels.server.unknown is an intentional compiler tightening and the only pre-enable cross-document binding guard. Workflows and output styles deliberately reuse the bin slice's normalized directory/file payload shape and byte-faithful copy entries, but retain separate registry hooks, normalized fields, destination paths, and diagnostics so the executable policy cannot leak into non-executable components. The shared enumerator preserves source file modes through copy entries and rejects lexical or realpath escapes, including a configured directory symlink that resolves outside the project. The adapter emits only the canonical plugin-root workflows/ and output-styles/ directories, so it does not emit the optional `workflows` or `outputStyles` manifest path fields and leaves plugin.schema.json plus its SHA-256 pin unchanged. https://code.claude.com/docs/en/output-styles (retrieved 2026-09-01) explicitly defines output styles as Markdown, so Agent Bundle tightens the directory to `.md` files with claude.outputStyles.file.invalid. It does not validate frontmatter: `name` is optional because the filename supplies it, other documented fields are optional, and Claude Code 2.1.257 strict plugin validation accepts a Markdown output style with no frontmatter. The plugins reference gives workflow scripts no deeper file schema, so workflow file contents and suffixes remain opaque. monitors.schema.json and theme.schema.json were pinned from https://code.claude.com/docs/en/plugins-reference (retrieved 2026-09-01), which marks both components experimental and warns their manifest schema may change between releases. The monitor schema is a nonempty array of closed entries requiring nonempty unique name, command, and description; optional when admits only always or on-skill-invoke:, and the plan tightens the latter by requiring the named skill to be emitted by this plugin. The schema and plan both reject ${user_config.*} in monitor commands because Claude Code rejects it at shell execution time and supplies no CLAUDE_PLUGIN_OPTION_ variables to monitors. The theme schema is closed, requires a nonempty base, and admits optional nonempty name plus a sparse string-valued overrides map. Agent Bundle further requires a nonempty themes declaration, safe file-stem keys, defaults omitted name to that key, and rejects empty override strings; it deliberately accepts any nonempty color string because the reference shows hex examples but does not mandate hex syntax. Generated themes and monitors use the default locations, so no unstable experimental.* manifest fields are emitted. Local proof against Claude Code 2.1.257 shows strict validation accepts valid emitted themes and monitors but does not inspect either default-location document's contents: malformed themes missing base and carrying non-string overrides pass, as do monitors missing command. The pinned schemas and compiler diagnostics are therefore the content guard. The same strict host rejects the deprecated top-level monitors manifest key, so default-location emission also avoids the warning-to-error migration path. The artifact contract records themes/*.json as a schema family and validates every concrete generated theme path. marketplace.schema.json was re-pinned from https://code.claude.com/docs/en/plugin-marketplaces, https://code.claude.com/docs/en/plugins-reference, https://code.claude.com/docs/en/plugin-dependencies, and the linked relevance contract (retrieved 2026-09-02). It loosens the old generated-only snapshot by making top-level description and plugin-entry description/version optional, allowing documented owner email/url, top-level $schema/version/metadata/renames/allowCrossMarketplaceDependenciesOn, and every documented plugin-entry metadata, component, relevance, authentication, dependency, channel, userConfig, settings, and experimental field; plugin-entry version is now any nonempty documented version string rather than generated-only semver. It deliberately retains only the relative ./ source form: github, git URL, git-subdir, npm, archive, and command sources are deferred to the source-matrix follow-up. The authored overlay is narrower than the validation schema and does not expose plugin name/source or component routing, because generated identity/layout remains authoritative and the agents component remains gate-deferred. Agent Bundle tightens the prose contract by closing every structured object (free-form metadata uses recursive patternProperties with additionalProperties false), requiring nonempty arrays/maps where an empty declaration has no effect, requiring unique kebab-case cross-marketplace allowlist names and kebab-case rename keys/targets, rejecting the documented exact reserved marketplace names, requiring relative sources and component paths to stay inside the marketplace, limiting owner/author URLs to absolute HTTP(S), requiring metadata.pluginRoot to be an internal ./ path, requiring nonempty header names/values, and requiring explicit strict:false with headersHelper. Relevance enforces the documented topic, item-count, character-count, bare-hostname, and regular-expression bounds; Claude Code 2.1.257 strict validation does not reject an empty allowCrossMarketplaceDependenciesOn entry. The same native validator warns that headers and headersHelper have no effect on the generated relative source and promotes that warning to failure under --strict, so the authored overlay rejects both fields with claude.marketplace.plugin.headers.inapplicable and claude.marketplace.plugin.headersHelper.inapplicable rather than emitting a manifest the host's strict validation refuses; the pinned schema keeps the documented fields, including the strict:false requirement beside headersHelper, for the URL-capable source-matrix follow-up.", "sourceMatrixNotes": "Source-matrix addendum (retrieved 2026-09-02): marketplace.schema.json now admits the documented relative, github, git URL, git-subdir, npm, archive, and command plugin sources as closed alternatives. Agent Bundle tightens relative paths and git-subdir paths against lexical escapes; GitHub shorthand to one owner/repo pair; git URLs to HTTPS or git@ forms; npm names to npm's lowercase package grammar and npm versions to exact, ^, or ~ semantic versions; registry URLs to HTTP(S); archive URLs to HTTPS with lexical rejection for localhost, IPv4/IPv6 loopback, 169.254/16 and fe80::/10 link-local addresses, and known AWS, Azure, and Google metadata hostnames; git sha to 40 hex; archive sha256 to 64 case-insensitive hex; command text to 1-500 printable ASCII characters without four-space runs; timeout to integer seconds from 1 through 600; and mode to copy or link. Entry headers remain a nonempty record of nonempty string values, and headersHelper requires a nonempty command plus strict:false. These checks do not resolve DNS, follow redirects, contact registries or repositories, download archives, verify archive bytes, execute command sources, inspect produced directories, or attempt to predict Windows link-mode support; those remain native install-time behavior.", + "agentsComponentNotes": "Agents-component addendum (retrieved 2026-09-02): https://code.claude.com/docs/en/plugins-reference documents plugin-root agents/ Markdown files with name, description, model, effort, maxTurns, tools, disallowedTools, skills, memory, background, and isolation frontmatter, with worktree as the only valid plugin-agent isolation value. https://code.claude.com/docs/en/sub-agents documents each field's runtime behavior and states that hooks, mcpServers, and permissionMode are ignored for plugin subagents. Local Claude Code 2.1.257 `claude plugin validate --strict` accepts one plugin agent exercising all supported fields and also accepts a negative fixture declaring hooks, mcpServers, and permissionMode: bypassPermissions without warning, so strict validation does not enforce the loader-side ignore contract. Agent Bundle adds no agent schema or emission surface in this revision: merged PR #220 and #107 revision 3 deliberately keep agents behind the #100 stage-2 G5 gate pending a separate evidence-backed re-admission increment.", "schemas": { "hooks.schema.json": { "bytes": 1108, diff --git a/packages/agent-bundle/tests/adapter-capability-states.test.ts b/packages/agent-bundle/tests/adapter-capability-states.test.ts index e6313e474..097bcb5d9 100644 --- a/packages/agent-bundle/tests/adapter-capability-states.test.ts +++ b/packages/agent-bundle/tests/adapter-capability-states.test.ts @@ -180,6 +180,68 @@ it('reports Claude plugin settings support and honest unavailable composite cove expect(registry.supports('plugin', 'settings')).toBe(false); }); +const claudeAgentCapabilityRows = { + background: 'agents.background', + component: 'agents', + description: 'agents.description', + disallowedTools: 'agents.disallowedTools', + effort: 'agents.effort', + hooks: 'agents.hooks', + isolationWorktree: 'agents.isolationWorktree', + maxTurns: 'agents.maxTurns', + mcpServers: 'agents.mcpServers', + memory: 'agents.memory', + model: 'agents.model', + name: 'agents.name', + permissionMode: 'agents.permissionMode', + skills: 'agents.skills', + tools: 'agents.tools', +} as const; + +it('records dated unavailable Claude agent rows and mirrors them through the unified adapter', () => { + const registry = createDefaultRegistry(); + const agents = ( + claudeCapabilityTable.plugin as unknown as { + readonly agents?: Readonly>; + } + ).agents; + + expect(agents).toBeDefined(); + if (agents === undefined) return; + expect(Object.keys(agents).sort()).toEqual(Object.keys(claudeAgentCapabilityRows).sort()); + + for (const [rowName, capability] of Object.entries(claudeAgentCapabilityRows)) { + const row = agents[rowName as keyof typeof claudeAgentCapabilityRows]; + expect(row).toMatchObject({ + reason: expect.stringMatching(/#100 stage-2 G5|#100 stage 2 G5/u), + state: 'unavailable', + }); + expect(row.reason).toContain('PR #220'); + expect(row.reason).toContain('#107 revision 3'); + expect(row.evidence.length).toBeGreaterThan(0); + expect(row.evidence.every((line) => line.startsWith('retrieved 2026-09-02:'))).toBe(true); + expect(registry.get('claude').capabilities[capability]).toEqual({ + reason: row.reason, + state: 'unavailable', + }); + expect(registry.get('plugin').capabilities[capability]).toEqual(intersectCapabilityStates( + registry.get('claude').capabilities[capability]!, + unavailableCapability( + 'The pinned Codex and Cursor plugin contracts publish no shared plugin agents component or agent-frontmatter surface.', + ), + )); + expect(registry.supports('claude', capability)).toBe(false); + expect(registry.supports('plugin', capability)).toBe(false); + } +}); + it('reports Claude userConfig support and honest unavailable composite coverage', () => { const registry = createDefaultRegistry(); diff --git a/packages/agent-bundle/tests/adapter-metadata.test.ts b/packages/agent-bundle/tests/adapter-metadata.test.ts index b038be468..cfc7430ca 100644 --- a/packages/agent-bundle/tests/adapter-metadata.test.ts +++ b/packages/agent-bundle/tests/adapter-metadata.test.ts @@ -93,7 +93,7 @@ it('records exact immutable metadata for every built-in target', () => { ], }); expect(registryMetadata(registry, 'claude')).toEqual({ - adapterRevision: '1.17.0', + adapterRevision: '1.18.0', observedVersion: '2.1.250', schemas: [ { @@ -164,7 +164,7 @@ it('records exact immutable metadata for every built-in target', () => { }, ], }); - expect(registryMetadata(registry, 'plugin').adapterRevision).toBe('1.15.0'); + expect(registryMetadata(registry, 'plugin').adapterRevision).toBe('1.16.0'); }); it('records observed capability versions and rehashes schema snapshots against pinned provenance', async () => { @@ -204,6 +204,9 @@ it('records observed capability versions and rehashes schema snapshots against p expect(schema.revision).toBe(metadata.observedVersion); } + if (target === 'claude') { + expect(sha256Hex(capability)).toBe('4db46064cce1e3f74bceb9e36a4c6da860acf183c1c1ccbb65f692d22d4e17d6'); + } if (target === 'cursor') { expect(sha256Hex(capability)).toBe('d42dc98d3c7f1f91dd6ef733d6727618c30a7d8d41fdf030165e06ce46345223'); const pluginSchema = JSON.parse(await readFile( diff --git a/packages/agent-bundle/tests/host-adapters.native.test.ts b/packages/agent-bundle/tests/host-adapters.native.test.ts index f2d39265e..a2f3c81c0 100644 --- a/packages/agent-bundle/tests/host-adapters.native.test.ts +++ b/packages/agent-bundle/tests/host-adapters.native.test.ts @@ -578,6 +578,76 @@ nativeIt('records that strict native validation never inspects plugin settings.j } }); +nativeIt('records strict native validation behavior for documented and security-sensitive plugin agent fields', async () => { + const root = await mkdtemp(join(tmpdir(), 'agent-bundle-claude-agents-')); + const cases = [ + { + fileName: 'test-agent.md', + frontmatter: [ + 'name: test-agent', + 'description: Exercises every documented plugin-agent field.', + 'model: inherit', + 'effort: high', + 'maxTurns: 3', + 'tools:', + ' - Read', + 'disallowedTools:', + ' - Write', + 'skills:', + ' - review', + 'memory: project', + 'background: true', + 'isolation: worktree', + ], + label: 'documented fields', + }, + { + fileName: 'security-sensitive.md', + frontmatter: [ + 'name: security-sensitive', + 'description: Probes plugin-agent fields that the host security contract ignores.', + 'hooks: {}', + 'mcpServers: []', + 'permissionMode: bypassPermissions', + ], + label: 'security-sensitive fields', + }, + ] as const; + + try { + for (const { fileName, frontmatter, label } of cases) { + const caseRoot = join(root, fileName.replace('.md', '')); + const configRoot = join(caseRoot, 'config'); + const pluginRoot = join(caseRoot, 'plugin'); + await writeClaudeArtifact(pluginRoot, model); + await Promise.all([ + mkdir(join(pluginRoot, 'agents'), { recursive: true }), + mkdir(configRoot, { recursive: true }), + ]); + await writeFile( + join(pluginRoot, 'agents', fileName), + `---\n${frontmatter.join('\n')}\n---\n\nInspect the repository and report findings.\n`, + ); + + const validation = await runClaude( + pluginRoot, + ['plugin', 'validate', '--strict', pluginRoot], + configRoot, + ); + + expect(validation.code, `${label}: ${validation.output}`).toBe(0); + expect(validation.output).toContain('Validation passed'); + if (label === 'security-sensitive fields') { + expect(validation.output).not.toContain('hooks'); + expect(validation.output).not.toContain('mcpServers'); + expect(validation.output).not.toContain('permissionMode'); + } + } + } finally { + await rm(root, { force: true, recursive: true }); + } +}); + nativeIt('accepts emitted Claude experimental themes and monitors under strict native validation', async () => { const root = await mkdtemp(join(tmpdir(), 'agent-bundle-claude-experimental-'));