diff --git a/.changeset/steady-plugins-report.md b/.changeset/steady-plugins-report.md new file mode 100644 index 000000000..5a09de647 --- /dev/null +++ b/.changeset/steady-plugins-report.md @@ -0,0 +1,5 @@ +--- +"agent-bundle": minor +--- + +Record Claude Code distribution, managed-policy, and CLI lifecycle boundaries as dated capability evidence without claiming compiler control over host installation behavior. 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 e5795b014..76ff87484 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 @@ -77,6 +77,149 @@ "semverRanges": true, "tagConvention": "{name}--v{version}" }, + "distributionPolicy": { + "skillsDirectoryPlugins": { + "state": "unavailable", + "reason": "Agent Bundle emits a plugin directory but does not place it in a Claude skills directory or register the host-owned @skills-dir identity.", + "evidence": [ + "retrieved 2026-09-02: https://code.claude.com/docs/en/plugins and https://code.claude.com/docs/en/plugins-reference document that a folder with .claude-plugin/plugin.json under a skills directory auto-loads on the next session as @skills-dir, in place, with no marketplace or install record.", + "retrieved 2026-09-02: https://code.claude.com/docs/en/plugins-reference documents personal discovery under ~/.claude/skills/ and project discovery under /.claude/skills/; project discovery does not walk up from a subdirectory." + ] + }, + "skillsDirectoryProjectTrust": { + "state": "unavailable", + "reason": "Agent Bundle cannot grant or inspect the workspace trust required before Claude loads a project-scope @skills-dir plugin.", + "evidence": [ + "retrieved 2026-09-02: https://code.claude.com/docs/en/plugins-reference requires explicit workspace trust for the project folder itself before a project-scope @skills-dir plugin loads; trust for a parent folder and a -p session do not satisfy the gate." + ] + }, + "skillsDirectoryMcpApproval": { + "state": "unavailable", + "reason": "Agent Bundle can emit plugin MCP configuration but cannot complete Claude's per-server approval for a project-scope @skills-dir plugin.", + "evidence": [ + "retrieved 2026-09-02: https://code.claude.com/docs/en/plugins-reference sends MCP servers declared by a project-scope @skills-dir plugin through the same per-server approval as a project .mcp.json." + ] + }, + "skillsDirectoryLspTrust": { + "state": "unavailable", + "reason": "Agent Bundle can emit plugin LSP configuration but cannot satisfy Claude's workspace-trust gate for a project-scope @skills-dir plugin.", + "evidence": [ + "retrieved 2026-09-02: https://code.claude.com/docs/en/plugins-reference starts LSP servers from a project-scope @skills-dir plugin only after the user trusts that workspace." + ] + }, + "skillsDirectoryMonitors": { + "state": "unavailable", + "reason": "Agent Bundle cannot make Claude load background monitors from a project-scope @skills-dir plugin, which the host explicitly skips.", + "evidence": [ + "retrieved 2026-09-02: https://code.claude.com/docs/en/plugins-reference states that background monitors do not load for project-scope @skills-dir plugins; personal-scope plugins do not have that restriction." + ] + }, + "pluginInstallScopes": { + "state": "unavailable", + "reason": "Agent Bundle emits installation instructions but cannot choose or persist a Claude plugin installation scope.", + "evidence": [ + "retrieved 2026-09-02: https://code.claude.com/docs/en/plugins-reference maps user, project, and local installs to ~/.claude/settings.json, .claude/settings.json, and .claude/settings.local.json respectively, while managed scope is read-only and update-only.", + "retrieved 2026-09-02: https://code.claude.com/docs/en/plugins-reference documents --scope user|project|local for install and uninstall, scope auto-detection for enable and disable, and --scope user|project|local|managed for update." + ] + }, + "pluginReload": { + "state": "unavailable", + "reason": "Agent Bundle cannot reload or restart a running Claude session after plugin files or installed versions change.", + "evidence": [ + "retrieved 2026-09-02: https://code.claude.com/docs/en/plugins documents /reload-plugins for development changes and https://code.claude.com/docs/en/plugins-reference says @skills-dir SKILL.md edits are immediate while hooks, MCP, agents, and output styles require /reload-plugins or restart.", + "retrieved 2026-09-02: https://code.claude.com/docs/en/plugins-reference keeps old hook, monitor, MCP, and LSP paths after a mid-session update; /reload-plugins switches hooks, MCP, and LSP, but monitors require restart, and unchanged MCP connections remain live.", + "retrieved 2026-09-02: local Claude Code 2.1.257 `claude plugin --help` describes update as restart-required." + ] + }, + "pluginTrustGates": { + "state": "unavailable", + "reason": "Agent Bundle cannot accept Claude workspace-trust, server-managed-settings approval, or command-source confirmation prompts on a user's behalf.", + "evidence": [ + "retrieved 2026-09-02: https://code.claude.com/docs/en/plugin-marketplaces runs project-declared headersHelper commands only after trust for that exact folder, runs server-managed commands only after security approval, and cannot show the approval dialog in -p or SDK sessions.", + "retrieved 2026-09-02: https://code.claude.com/docs/en/plugin-marketplaces requires explicit per-install or per-update acceptance for plugin entry headersHelper and command sources; non-interactive users must pass --yes." + ] + }, + "syncedPlugins": { + "state": "unavailable", + "reason": "Agent Bundle cannot synchronize or administer the @synced plugins attached to a user's claude.ai account.", + "evidence": [ + "retrieved 2026-09-02: https://code.claude.com/docs/en/plugins-reference limits @synced downloads to Cowork and cloud session environments, with no marketplace or install record; local terminal sessions do not load them.", + "retrieved 2026-09-02: https://code.claude.com/docs/en/plugins-reference permits enable and disable by @synced ID but says install, update, and uninstall do not apply; account-level removal takes effect in the next synced session." + ] + }, + "managedPluginScope": { + "state": "unavailable", + "reason": "Agent Bundle cannot create, remove, enable, or disable managed-scope plugins; Claude exposes that host-owned scope as read-only and update-only.", + "evidence": [ + "retrieved 2026-09-02: https://code.claude.com/docs/en/plugins-reference lists managed plugins under Managed settings as read-only and update-only, and only plugin update accepts --scope managed." + ] + }, + "managedStrictKnownMarketplaces": { + "state": "unavailable", + "reason": "Agent Bundle does not emit or override the organization-managed strictKnownMarketplaces allowlist.", + "evidence": [ + "retrieved 2026-09-02: https://code.claude.com/docs/en/plugin-marketplaces defines strictKnownMarketplaces as undefined for no restriction, [] for complete lockdown, or a source list for allowlisting; it restricts additions but does not register marketplaces.", + "retrieved 2026-09-02: https://code.claude.com/docs/en/plugin-marketplaces checks the managed allowlist before network or filesystem access on marketplace add and plugin install, update, refresh, and auto-update, and users or projects cannot override it." + ] + }, + "managedBlockedMarketplaces": { + "state": "unavailable", + "reason": "Agent Bundle does not emit or override the organization-managed blockedMarketplaces denylist.", + "evidence": [ + "retrieved 2026-09-02: https://code.claude.com/docs/en/plugin-marketplaces applies blockedMarketplaces at the same pre-I/O add, install, update, refresh, and auto-update gates as strictKnownMarketplaces.", + "retrieved 2026-09-02: https://code.claude.com/docs/en/plugin-marketplaces documents GitHub owner wildcards from 2.1.223 and URL clone matching from 2.1.232, ignoring .git and appended refs for the latter." + ] + }, + "managedDisableSideloadFlags": { + "state": "unavailable", + "reason": "Agent Bundle cannot configure or bypass disableSideloadFlags in Claude managed settings.", + "evidence": [ + "retrieved 2026-09-02: https://code.claude.com/docs/en/plugin-marketplaces documents disableSideloadFlags as the managed companion to strictKnownMarketplaces that rejects one-run CLI sideload flags for plugins, agents, and MCP servers; Agent Bundle's agents component remains gate-deferred (#100 stage 2 G5, PR #220) as recorded by claude.settings.agent.deferred." + ] + }, + "managedDisableCommandPluginSources": { + "state": "unavailable", + "reason": "Agent Bundle emits no managed setting that can permit, block, or exempt Claude command plugin sources.", + "evidence": [ + "retrieved 2026-09-02: https://code.claude.com/docs/en/plugin-marketplaces documents disableCommandPluginSources as the separate control needed because strictKnownMarketplaces allowlists marketplaces rather than their command-source entries.", + "retrieved 2026-09-02: https://code.claude.com/docs/en/plugin-marketplaces says disableCommandPluginSources blocks command sources and headersHelper commands across the organization, except commands declared by managed settings themselves." + ] + }, + "managedAllowManagedHooksOnly": { + "state": "unavailable", + "reason": "Agent Bundle cannot configure allowManagedHooksOnly or claim that emitted plugin commands are exempt from that managed policy.", + "evidence": [ + "retrieved 2026-09-02: https://code.claude.com/docs/en/plugin-marketplaces says allowManagedHooksOnly blocks command sources by default and blocks headersHelper unless disableCommandPluginSources is explicitly false; a marketplace declared by managed settings remains exempt for its own command.", + "retrieved 2026-09-02: the existing pinned settings evidence records that plugin subagentStatusLine commands do not run under allowManagedHooksOnly even when the plugin is force-enabled in managed enabledPlugins, so this row does not invent a settings.json exemption." + ] + }, + "managedPluginSuggestions": { + "state": "unavailable", + "reason": "Agent Bundle does not emit the managed pluginSuggestionMarketplaces allowlist or marketplace relevance signals for contextual installation suggestions.", + "evidence": [ + "retrieved 2026-09-02: https://code.claude.com/docs/en/plugin-marketplaces documents pluginSuggestionMarketplaces as the managed allowlist for marketplaces whose plugins may appear as contextual install suggestions.", + "retrieved 2026-09-02: https://code.claude.com/docs/en/plugin-marketplaces documents marketplace-entry relevance signals as effective only for an administrator-allowlisted marketplace; suggested command-source plugins are refused in bulk and direct users to the plugin's own review view." + ] + }, + "pluginCliLifecycle": { + "state": "unavailable", + "reason": "Agent Bundle emits Claude plugin artifacts but does not invoke Claude's plugin creation, installation, state, update, inspection, or release commands.", + "commands": ["init", "new", "install", "uninstall", "prune", "enable", "disable", "update", "list", "details", "tag"], + "evidence": [ + "retrieved 2026-09-02: https://code.claude.com/docs/en/plugins-reference documents init with new alias, install, uninstall, prune, enable, disable, update, list, details, and tag, including their scope and safety options.", + "retrieved 2026-09-02: local Claude Code 2.1.257 `claude plugin --help` exposes init|new, install, uninstall, prune, enable, disable, update, list, details, and tag." + ] + }, + "marketplaceCliLifecycle": { + "state": "unavailable", + "reason": "Agent Bundle emits marketplace manifests but does not add, list, remove, or update marketplaces in a user's Claude configuration.", + "commands": ["add", "list", "remove", "update"], + "evidence": [ + "retrieved 2026-09-02: https://code.claude.com/docs/en/plugin-marketplaces documents marketplace add, list, remove (rm alias), and update; remove can target an editable scope, update can target one or all, and seed-managed marketplaces reject remove and update.", + "retrieved 2026-09-02: local Claude Code 2.1.257 `claude plugin marketplace --help` exposes add, list, remove|rm, and update." + ] + } + }, "experimentalThemes": { "defaultDirectory": "themes", "experimental": true, @@ -214,7 +357,7 @@ "workspaceRoot": "${CLAUDE_PROJECT_DIR}" }, "provenance": { - "observedAt": "2026-09-01", + "observedAt": "2026-09-02", "source": "https://docs.anthropic.com/en/docs/claude-code/plugins", "evidence": [ "LSP servers section: \"Location: .lsp.json in plugin root, or inline in plugin.json\"; the file-locations table lists .lsp.json as the default LSP location, alongside .mcp.json, at the plugin root rather than inside .claude-plugin/.", diff --git a/packages/agent-bundle/src/adapters/claude.ts b/packages/agent-bundle/src/adapters/claude.ts index 32eba1b0b..36327bb4b 100644 --- a/packages/agent-bundle/src/adapters/claude.ts +++ b/packages/agent-bundle/src/adapters/claude.ts @@ -295,11 +295,12 @@ const hookContract = Object.freeze({ wrapperSource: (entry) => nativeHookWrapperSource(entry, 'Claude'), } satisfies TargetHookContract); const metadata = Object.freeze({ - adapterRevision: '1.13.0', + adapterRevision: '1.14.0', observedVersion: capabilityTable.observedCliVersion, schemas: schemaDescriptorsFrom(schemaProvenance, schemaProvenance.observedCliVersion), }); const evidence = capabilityEvidence(claudeName, metadata); +const distributionPolicy = capabilityTable.plugin.distributionPolicy; const artifactValidation = deepFreeze({ documents: [ @@ -2024,6 +2025,28 @@ export const claudeAdapter: TargetAdapter = Object.freeze({ evidence, 'The pinned Claude plugin contract does not document manifest dependencies.', ), + managedAllowManagedHooksOnly: unavailableCapability( + distributionPolicy.managedAllowManagedHooksOnly.reason, + ), + managedBlockedMarketplaces: unavailableCapability( + distributionPolicy.managedBlockedMarketplaces.reason, + ), + managedDisableCommandPluginSources: unavailableCapability( + distributionPolicy.managedDisableCommandPluginSources.reason, + ), + managedDisableSideloadFlags: unavailableCapability( + distributionPolicy.managedDisableSideloadFlags.reason, + ), + managedPluginScope: unavailableCapability(distributionPolicy.managedPluginScope.reason), + managedPluginSuggestions: unavailableCapability( + distributionPolicy.managedPluginSuggestions.reason, + ), + managedStrictKnownMarketplaces: unavailableCapability( + distributionPolicy.managedStrictKnownMarketplaces.reason, + ), + marketplaceCliLifecycle: unavailableCapability( + distributionPolicy.marketplaceCliLifecycle.reason, + ), install: supportedCapability(evidence), marketplace: supportedCapability(evidence), hooks: supportedCapability(evidence), @@ -2081,6 +2104,10 @@ export const claudeAdapter: TargetAdapter = Object.freeze({ evidence, 'The pinned Claude plugin contract does not document the plugin-root output-styles surface.', ), + pluginCliLifecycle: unavailableCapability(distributionPolicy.pluginCliLifecycle.reason), + pluginInstallScopes: unavailableCapability(distributionPolicy.pluginInstallScopes.reason), + pluginReload: unavailableCapability(distributionPolicy.pluginReload.reason), + pluginTrustGates: unavailableCapability(distributionPolicy.pluginTrustGates.reason), rules: unavailableCapability( 'The pinned Claude Code plugin contract (2.1.250) defines no rules component; project guidance ships through CLAUDE.md memory, not a rules directory.', ), @@ -2096,6 +2123,22 @@ export const claudeAdapter: TargetAdapter = Object.freeze({ evidence, 'The pinned Claude plugin contract does not support skills.', ), + skillsDirectoryLspTrust: unavailableCapability( + distributionPolicy.skillsDirectoryLspTrust.reason, + ), + skillsDirectoryMcpApproval: unavailableCapability( + distributionPolicy.skillsDirectoryMcpApproval.reason, + ), + skillsDirectoryMonitors: unavailableCapability( + distributionPolicy.skillsDirectoryMonitors.reason, + ), + skillsDirectoryPlugins: unavailableCapability( + distributionPolicy.skillsDirectoryPlugins.reason, + ), + skillsDirectoryProjectTrust: unavailableCapability( + distributionPolicy.skillsDirectoryProjectTrust.reason, + ), + syncedPlugins: unavailableCapability(distributionPolicy.syncedPlugins.reason), themes: capabilityStateFromSupport( capabilityTable.plugin.experimentalThemes.defaultDirectory === 'themes' && capabilityTable.plugin.experimentalThemes.experimental && diff --git a/packages/agent-bundle/src/adapters/plugin.ts b/packages/agent-bundle/src/adapters/plugin.ts index e10cfff59..afc99ee39 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.12.0', + adapterRevision: '1.13.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 @@ -602,6 +602,54 @@ export const pluginAdapter: TargetAdapter = Object.freeze({ 'The pinned Codex and Cursor plugin contracts publish no dependency declaration or resolution surface; manifest dependencies reach Claude Code only.', ), ), + managedAllowManagedHooksOnly: intersectCapabilityStates( + claudeAdapter.capabilities.managedAllowManagedHooksOnly!, + unavailableCapability( + 'The unified bundle cannot configure a Claude-only managed hook policy, and the pinned Codex and Cursor contracts publish no shared allowManagedHooksOnly surface.', + ), + ), + managedBlockedMarketplaces: intersectCapabilityStates( + claudeAdapter.capabilities.managedBlockedMarketplaces!, + unavailableCapability( + 'The unified bundle cannot configure a Claude-only managed marketplace denylist, and the pinned Codex and Cursor contracts publish no shared blockedMarketplaces surface.', + ), + ), + managedDisableCommandPluginSources: intersectCapabilityStates( + claudeAdapter.capabilities.managedDisableCommandPluginSources!, + unavailableCapability( + 'The unified bundle cannot configure Claude-only command-source policy, and the pinned Codex and Cursor contracts publish no shared disableCommandPluginSources surface.', + ), + ), + managedDisableSideloadFlags: intersectCapabilityStates( + claudeAdapter.capabilities.managedDisableSideloadFlags!, + unavailableCapability( + 'The unified bundle cannot configure Claude-only sideload policy, and the pinned Codex and Cursor contracts publish no shared disableSideloadFlags surface.', + ), + ), + managedPluginScope: intersectCapabilityStates( + claudeAdapter.capabilities.managedPluginScope!, + unavailableCapability( + 'The unified bundle has no cross-host managed installation transaction, and the pinned Codex and Cursor contracts publish no shared managed plugin scope.', + ), + ), + managedPluginSuggestions: intersectCapabilityStates( + claudeAdapter.capabilities.managedPluginSuggestions!, + unavailableCapability( + 'The unified bundle cannot configure Claude-only contextual plugin suggestions, and the pinned Codex and Cursor contracts publish no shared pluginSuggestionMarketplaces surface.', + ), + ), + managedStrictKnownMarketplaces: intersectCapabilityStates( + claudeAdapter.capabilities.managedStrictKnownMarketplaces!, + unavailableCapability( + 'The unified bundle cannot configure a Claude-only managed marketplace allowlist, and the pinned Codex and Cursor contracts publish no shared strictKnownMarketplaces surface.', + ), + ), + marketplaceCliLifecycle: intersectCapabilityStates( + claudeAdapter.capabilities.marketplaceCliLifecycle!, + unavailableCapability( + 'The unified bundle emits host marketplace documents but cannot add, list, remove, or update marketplaces as one cross-host lifecycle transaction.', + ), + ), install: unavailableCapability( 'Plugin is a multi-host distribution profile, not one host runtime with a single installation transaction.', ), @@ -639,6 +687,30 @@ export const pluginAdapter: TargetAdapter = Object.freeze({ outputStyles: unavailableCapability( 'The unified bundle emits Claude-only output styles, but the pinned Codex and Cursor contracts declare no shared output styles surface.', ), + pluginCliLifecycle: intersectCapabilityStates( + claudeAdapter.capabilities.pluginCliLifecycle!, + unavailableCapability( + 'The unified bundle emits host artifacts but cannot run Claude-only plugin creation, installation, state, inspection, update, or release commands.', + ), + ), + pluginInstallScopes: intersectCapabilityStates( + claudeAdapter.capabilities.pluginInstallScopes!, + unavailableCapability( + 'The unified bundle has no shared user, project, local, or managed installation-scope transaction across its three hosts.', + ), + ), + pluginReload: intersectCapabilityStates( + claudeAdapter.capabilities.pluginReload!, + unavailableCapability( + 'The unified bundle cannot reload or restart running host sessions, and the pinned hosts publish no shared plugin reload lifecycle.', + ), + ), + pluginTrustGates: intersectCapabilityStates( + claudeAdapter.capabilities.pluginTrustGates!, + unavailableCapability( + 'The unified bundle cannot accept host trust or security prompts, and the pinned hosts publish no shared plugin trust-gate transaction.', + ), + ), // The bundle exposes Cursor's real rules directory; the composite row is // the honest three-host intersection, so it stays non-supported while // Claude and Codex cannot consume rules. @@ -659,6 +731,42 @@ export const pluginAdapter: TargetAdapter = Object.freeze({ intersectCapabilityStates(claudeAdapter.capabilities.skills!, codexAdapter.capabilities.skills!), cursorAdapter.capabilities.skills!, ), + skillsDirectoryLspTrust: intersectCapabilityStates( + claudeAdapter.capabilities.skillsDirectoryLspTrust!, + unavailableCapability( + 'The pinned Codex and Cursor contracts publish no shared @skills-dir LSP trust gate.', + ), + ), + skillsDirectoryMcpApproval: intersectCapabilityStates( + claudeAdapter.capabilities.skillsDirectoryMcpApproval!, + unavailableCapability( + 'The pinned Codex and Cursor contracts publish no shared @skills-dir per-server MCP approval gate.', + ), + ), + skillsDirectoryMonitors: intersectCapabilityStates( + claudeAdapter.capabilities.skillsDirectoryMonitors!, + unavailableCapability( + 'The pinned Codex and Cursor contracts publish no shared project-scope @skills-dir monitor policy.', + ), + ), + skillsDirectoryPlugins: intersectCapabilityStates( + claudeAdapter.capabilities.skillsDirectoryPlugins!, + unavailableCapability( + 'The unified bundle does not install into host skills directories, and the pinned Codex and Cursor contracts publish no shared @skills-dir identity.', + ), + ), + skillsDirectoryProjectTrust: intersectCapabilityStates( + claudeAdapter.capabilities.skillsDirectoryProjectTrust!, + unavailableCapability( + 'The pinned Codex and Cursor contracts publish no shared project-scope @skills-dir workspace-trust gate.', + ), + ), + syncedPlugins: intersectCapabilityStates( + claudeAdapter.capabilities.syncedPlugins!, + unavailableCapability( + 'The pinned Codex and Cursor contracts publish no shared claude.ai-style account plugin synchronization surface.', + ), + ), themes: unavailableCapability( 'The unified bundle emits Claude-only experimental themes, but the pinned Codex and Cursor contracts declare no shared theme surface.', ), diff --git a/packages/agent-bundle/tests/adapter-capability-states.test.ts b/packages/agent-bundle/tests/adapter-capability-states.test.ts index be71cd31a..5d1a23257 100644 --- a/packages/agent-bundle/tests/adapter-capability-states.test.ts +++ b/packages/agent-bundle/tests/adapter-capability-states.test.ts @@ -9,6 +9,7 @@ import { unavailableCapability, unionCapabilityStates, } from '../src/adapters/capability-state.ts'; +import claudeCapabilityTable from '../src/adapters/capabilities/claude-2.1.250.json' with { type: 'json' }; import { TargetRegistry, createDefaultRegistry } from '../src/adapters/registry.ts'; import { CapabilityStateError, isCapabilityState } from '../src/core/capabilities.ts'; import type { CapabilityEvidence, CapabilityState } from '../src/core/capabilities.ts'; @@ -268,6 +269,81 @@ it('reports Claude dependency support and honest unavailable composite coverage' expect(registry.supports('plugin', 'dependencies')).toBe(false); }); +const claudeDistributionPolicyCapabilities = [ + 'skillsDirectoryPlugins', + 'skillsDirectoryProjectTrust', + 'skillsDirectoryMcpApproval', + 'skillsDirectoryLspTrust', + 'skillsDirectoryMonitors', + 'pluginInstallScopes', + 'pluginReload', + 'pluginTrustGates', + 'syncedPlugins', + 'managedPluginScope', + 'managedStrictKnownMarketplaces', + 'managedBlockedMarketplaces', + 'managedDisableSideloadFlags', + 'managedDisableCommandPluginSources', + 'managedAllowManagedHooksOnly', + 'managedPluginSuggestions', + 'pluginCliLifecycle', + 'marketplaceCliLifecycle', +] as const; + +it('records dated unavailable Claude distribution and policy capability rows', () => { + const registry = createDefaultRegistry(); + const distributionPolicy = ( + claudeCapabilityTable.plugin as unknown as { + readonly distributionPolicy?: Readonly>; + } + ).distributionPolicy; + + expect(distributionPolicy).toBeDefined(); + if (distributionPolicy === undefined) return; + expect(Object.keys(distributionPolicy).sort()).toEqual([...claudeDistributionPolicyCapabilities].sort()); + for (const capability of claudeDistributionPolicyCapabilities) { + const row = distributionPolicy[capability]; + expect(row.state).toBe('unavailable'); + expect(row.reason.length).toBeGreaterThan(0); + expect(row.evidence.length).toBeGreaterThan(0); + expect(row.evidence.every((line) => line.includes('retrieved 2026-09-02'))).toBe(true); + expect(registry.get('claude').capabilities[capability]).toEqual({ + reason: row.reason, + state: 'unavailable', + }); + expect(registry.get('plugin').capabilities[capability]).toMatchObject({ + state: 'unavailable', + }); + } + expect(distributionPolicy.pluginCliLifecycle.commands).toEqual([ + 'init', + 'new', + 'install', + 'uninstall', + 'prune', + 'enable', + 'disable', + 'update', + 'list', + 'details', + 'tag', + ]); + expect(distributionPolicy.marketplaceCliLifecycle.commands).toEqual([ + 'add', + 'list', + 'remove', + 'update', + ]); +}); + it.each([ ['manifestMetadata', 'manifest metadata fields'], ['manifestPaths', 'custom manifest path rules'], diff --git a/packages/agent-bundle/tests/adapter-metadata.test.ts b/packages/agent-bundle/tests/adapter-metadata.test.ts index 3f4b91e84..3f1f61845 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.13.0', + adapterRevision: '1.14.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.12.0'); + expect(registryMetadata(registry, 'plugin').adapterRevision).toBe('1.13.0'); }); it('records observed capability versions and rehashes schema snapshots against pinned provenance', async () => { diff --git a/packages/agent-bundle/tests/host-adapters.native.test.ts b/packages/agent-bundle/tests/host-adapters.native.test.ts index f21dae457..15365fdee 100644 --- a/packages/agent-bundle/tests/host-adapters.native.test.ts +++ b/packages/agent-bundle/tests/host-adapters.native.test.ts @@ -16,10 +16,17 @@ interface ClaudeValidation { readonly output: string; } -const runClaudeValidation = async (cwd: string, target: string): Promise => +const runClaude = async ( + cwd: string, + args: readonly string[], + configDir?: string, +): Promise => new Promise((resolvePromise, reject) => { - const child = spawn('claude', ['plugin', 'validate', '--strict', target], { + const child = spawn('claude', args, { cwd, + ...(configDir === undefined + ? {} + : { env: { ...process.env, CLAUDE_CONFIG_DIR: configDir } }), stdio: ['ignore', 'pipe', 'pipe'], }); let output = ''; @@ -35,6 +42,9 @@ const runClaudeValidation = async (cwd: string, target: string): Promise resolvePromise({ code, output })); }); +const runClaudeValidation = async (cwd: string, target: string): Promise => + runClaude(cwd, ['plugin', 'validate', '--strict', target]); + const model: NormalizedPlugin = { extensions: {}, hooks: [], @@ -161,6 +171,96 @@ const writeClaudeArtifact = async ( return written; }; +nativeIt('pins Claude plugin and marketplace lifecycle command help', async () => { + const root = await mkdtemp(join(tmpdir(), 'agent-bundle-claude-lifecycle-help-')); + + try { + const [version, pluginHelp, marketplaceHelp] = await Promise.all([ + runClaude(root, ['--version'], root), + runClaude(root, ['plugin', '--help'], root), + runClaude(root, ['plugin', 'marketplace', '--help'], root), + ]); + + expect(version.code, version.output).toBe(0); + expect(version.output).toContain('2.1.257'); + expect(pluginHelp.code, pluginHelp.output).toBe(0); + for (const command of [ + 'details', + 'disable', + 'enable', + 'init|new', + 'install|i', + 'list', + 'marketplace', + 'prune|autoremove', + 'tag', + 'uninstall|remove', + 'update', + ]) { + expect(pluginHelp.output).toContain(command); + } + expect(pluginHelp.output).toContain('restart required to apply'); + expect(marketplaceHelp.code, marketplaceHelp.output).toBe(0); + for (const command of ['add', 'list', 'remove|rm', 'update']) { + expect(marketplaceHelp.output).toContain(command); + } + } finally { + await rm(root, { force: true, recursive: true }); + } +}); + +nativeIt('adds, lists, and removes a marketplace only in an isolated config directory', async () => { + const root = await mkdtemp(join(tmpdir(), 'agent-bundle-claude-marketplace-lifecycle-')); + const marketplaceRoot = join(root, 'marketplace'); + const pluginRoot = join(marketplaceRoot, 'plugin'); + const configRoot = join(root, 'config'); + const marketplaceName = 'agent-bundle-native-policy'; + + try { + await Promise.all([ + mkdir(join(marketplaceRoot, '.claude-plugin'), { recursive: true }), + mkdir(join(pluginRoot, '.claude-plugin'), { recursive: true }), + mkdir(configRoot, { recursive: true }), + ]); + await Promise.all([ + writeFile( + join(marketplaceRoot, '.claude-plugin', 'marketplace.json'), + `${JSON.stringify({ + name: marketplaceName, + owner: { name: 'agent-bundle' }, + plugins: [{ + description: 'Native lifecycle proof.', + name: 'native-policy-proof', + source: './plugin', + version: '1.0.0', + }], + })}\n`, + ), + writeFile( + join(pluginRoot, '.claude-plugin', 'plugin.json'), + `${JSON.stringify({ + description: 'Native lifecycle proof.', + name: 'native-policy-proof', + version: '1.0.0', + })}\n`, + ), + ]); + + const added = await runClaude(root, ['plugin', 'marketplace', 'add', marketplaceRoot], configRoot); + expect(added.code, added.output).toBe(0); + const listed = await runClaude(root, ['plugin', 'marketplace', 'list'], configRoot); + expect(listed.code, listed.output).toBe(0); + expect(listed.output).toContain(marketplaceName); + const removed = await runClaude(root, ['plugin', 'marketplace', 'remove', marketplaceName], configRoot); + expect(removed.code, removed.output).toBe(0); + const listedAfterRemoval = await runClaude(root, ['plugin', 'marketplace', 'list'], configRoot); + expect(listedAfterRemoval.code, listedAfterRemoval.output).toBe(0); + expect(listedAfterRemoval.output).not.toContain(marketplaceName); + } finally { + await rm(root, { force: true, recursive: true }); + } +}); + nativeIt('accepts the emitted Claude marketplace under strict native validation', async () => { const root = await mkdtemp(join(tmpdir(), 'agent-bundle-claude-marketplace-'));