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
25 changes: 25 additions & 0 deletions .changeset/host-install-proof-level.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
"agent-bundle": minor
---

Add the `host-install` consumer proof level for real public-path installation
into isolated Claude, Codex, and Cursor homes. The source-built proof fixture
exercises Skills, Hooks, and MCP registration without model calls or packed
artifact claims, validates Cursor's emitted documents against the pinned
schemas, and records only path-relative evidence.

The level now also carries the real-host token proofs deferred from the
canonical Skill IR work. The Codex proof asserts the installed cache copy of a
skill's `agents/openai.yaml` sidecar is byte-identical to the built artifact and
valid against the pinned schema, and that the installed `.codex-plugin/plugin.json`
carries its `interface` block. The Cursor proof asserts the installed hooks and
MCP documents keep `${CURSOR_PLUGIN_ROOT}` unresolved, which is the honest
ceiling because Cursor publishes no non-interactive plugin-loading session
surface. An opt-in session-token qualifier
(`AGENT_BUNDLE_HOST_INSTALL_CLAUDE_SESSION=1`) observes `$ARGUMENTS`,
`${CLAUDE_PLUGIN_ROOT}`, and `${CLAUDE_SKILL_DIR}` resolving inside one real
`claude -p` turn with the built bundle loaded inline via `--plugin-dir`.

Generated Claude and Codex installation instructions now use
`plugin marketplace add ./`; Claude Code 2.1.257 rejects the previously emitted
bare `.` source.
10 changes: 10 additions & 0 deletions .github/workflows/native-host-smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,13 @@ jobs:
- name: ${{ matrix.host }} installed-tarball native smoke
if: inputs.host == 'both' || inputs.host == matrix.host
run: ${{ matrix.packed_command }}
- name: ${{ matrix.host }} real host install proof
if: inputs.host == 'both' || inputs.host == matrix.host
run: pnpm test:host-install
# Claude only: the session-token proof needs a signed-in `claude -p` turn,
# and Codex and Cursor have no equivalent non-interactive surface.
- name: ${{ matrix.host }} real session token proof
if: matrix.host == 'claude' && (inputs.host == 'both' || inputs.host == matrix.host)
env:
AGENT_BUNDLE_HOST_INSTALL_CLAUDE_SESSION: '1'
run: pnpm test:host-install:session
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ which real hosts can consume them.
Pinned capability tables record an `install` section beside each host's plugin
contract:

- Claude: `claude plugin marketplace add .`, then
- Claude: `claude plugin marketplace add ./`, then
`claude plugin install <plugin>@<marketplace> --scope <scope>`.
- Codex: `codex plugin marketplace add .`, then
- Codex: `codex plugin marketplace add ./`, then
`codex plugin add <plugin>@<marketplace>`.
- Cursor: no shell install verb; copy a complete plugin to
`~/.cursor/plugins/local/<plugin>`, then reload Cursor.
Expand All @@ -34,7 +34,7 @@ against a built directory.

## Emitted surface

Every target root contains `INSTALL.md`. Commands use `.` and the real compiled
Every target root contains `INSTALL.md`. Commands use `./` and the real compiled
plugin and marketplace names, so a user runs them from that target root without
editing placeholders.

Expand Down
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@
"test:packed:native": "rstest --config rstest.config.ts packages/agent-bundle/tests/packed-native-smoke.test.ts",
"test:packed:native:claude": "pnpm build && AGENT_BUNDLE_PACKED_NATIVE_CLAUDE_SMOKE=1 pnpm test:packed:native",
"test:packed:native:codex": "pnpm build && AGENT_BUNDLE_PACKED_NATIVE_CODEX_SMOKE=1 pnpm test:packed:native",
"test:host-install": "rstest --config rstest.config.ts packages/agent-bundle/tests/host-install-proof.test.ts",
"test:host-install:build": "pnpm build && pnpm test:host-install",
"test:host-install:session": "rstest --config rstest.config.ts packages/agent-bundle/tests/host-install-session.test.ts",
"test:host-install:session:claude": "pnpm build && AGENT_BUNDLE_HOST_INSTALL_CLAUDE_SESSION=1 pnpm test:host-install:session",
"changeset": "changeset",
"version-packages": "changeset version",
"release": "pnpm build && changeset publish",
Expand Down
5 changes: 4 additions & 1 deletion packages/agent-bundle/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,7 @@ is never a receipt for another.
| `cli-dispatch` | `invokeCli`, `cliJson` | an argv vector resolved and run through the routed CLI's own shell, in-process |
| `packed-stdio` | `openPackedMcpServer` | a built artifact's generated entry running as a real process over stdio |
| `packed-deleted-source` | `removeProjectSource`, `openPackedMcpServer({ deletedSource })` | the packed stdio process still runs after project source and configuration are removed and verified absent |
| `host-install` | repository real-host install proof | a built bundle installed into an isolated real host home through the public install path, with registration observed through the host's own CLI |

```ts
import { cliJson, expectEvents, invokeCli, invokeMcpTool } from 'agent-bundle/test';
Expand All @@ -303,7 +304,9 @@ once, build once, remove and verify source once, spawn once, and iterate every
per-route assertion inside that one session. The deleted-source journey also
reads the embedded MCP App resource from the generated server; it does not
prove native-host install or dispatch, or an install mode that copies the
artifact elsewhere.
artifact elsewhere. `host-install` is separate real-host process evidence for
built-bundle acceptance and registration, not packed provenance or session
behavior.

## Evaluation

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
"install": {
"evidence": [
"Local marketplaces accept a directory containing .claude-plugin/marketplace.json.",
"claude plugin install accepts plugin@marketplace and user, project, or local scope."
"claude plugin install accepts plugin@marketplace and user, project, or local scope.",
"2026-09-01: Claude Code 2.1.257 rejects a bare `.` marketplace source but accepts `./` and absolute directory paths."
],
"marketplaceAdd": "claude plugin marketplace add .",
"marketplaceAdd": "claude plugin marketplace add ./",
"pluginInstall": "claude plugin install <plugin>@<marketplace> --scope <scope>",
"scopes": ["user", "project", "local"],
"source": "https://code.claude.com/docs/en/discover-plugins",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
"install": {
"evidence": [
"codex plugin marketplace add accepts a local marketplace root.",
"codex plugin add installs plugin@marketplace from a configured snapshot."
"codex plugin add installs plugin@marketplace from a configured snapshot.",
"2026-09-01: Codex 0.147.0 accepts both `.` and `./`; emitted instructions use `./` consistently with the Claude Code 2.1.257 requirement."
],
"marketplaceAdd": "codex plugin marketplace add .",
"marketplaceAdd": "codex plugin marketplace add ./",
"pluginInstall": "codex plugin add <plugin>@<marketplace>",
"scopes": ["user"],
"source": "https://developers.openai.com/codex/cli/reference",
Expand Down
2 changes: 1 addition & 1 deletion packages/agent-bundle/src/adapters/claude.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ const hookContract = Object.freeze({
const metadata = Object.freeze({
adapterRevision: '1.5.0',
capabilityRevision: capabilityTable.observedCliVersion,
capabilitySha256: '553ebbec4bb16b6e075489fab6c31b11ce84870466710da98e2389659842090e',
capabilitySha256: 'd78b76bda7020f7ea64d332c50d73f7ba3213ef69731835d474383ea6ef46612',
observedVersion: capabilityTable.observedCliVersion,
schemas: schemaDescriptorsFrom(schemaProvenance, schemaProvenance.observedCliVersion),
});
Expand Down
2 changes: 1 addition & 1 deletion packages/agent-bundle/src/adapters/codex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ const hookContract = Object.freeze({
const metadata = Object.freeze({
adapterRevision: '1.2.0',
capabilityRevision: capabilityTable.observedCliVersion,
capabilitySha256: 'd944e508941a0660272a253601019957ae94e9140501f0624f85d111e66d9f28',
capabilitySha256: 'bb0a685d680ffd95c468acfcf2fc20dc8fec672ea718f3dd1934fccad3b726c5',
observedVersion: capabilityTable.observedCliVersion,
schemas: schemaDescriptorsFrom(schemaProvenance, schemaProvenance.observedCliVersion),
});
Expand Down
4 changes: 2 additions & 2 deletions packages/agent-bundle/src/install/surface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const claudeInstructions = (model: NormalizedPlugin): string[] => [
'Claude Code installs this bundle through its local marketplace contract:',
'',
'```sh',
'claude plugin marketplace add .',
'claude plugin marketplace add ./',
`claude plugin install ${model.metadata.name}@${marketplaceName(model)} --scope user`,
'```',
'',
Expand All @@ -41,7 +41,7 @@ const codexInstructions = (model: NormalizedPlugin): string[] => [
'Codex installs this bundle from its local marketplace snapshot:',
'',
'```sh',
'codex plugin marketplace add .',
'codex plugin marketplace add ./',
`codex plugin add ${model.metadata.name}@${marketplaceName(model)}`,
'```',
'',
Expand Down
7 changes: 5 additions & 2 deletions packages/agent-bundle/src/test/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
/**
* `agent-bundle/test` — the consumer test harness helpers.
*
* Five Node proof levels ship here, and the browser-safe sixth level ships
* from `agent-bundle/test/browser`. Each helper names the level it supplies,
* Six Node proof levels ship here, and the browser-safe seventh level ships
* from `agent-bundle/test/browser`. The repository's real-host install proof
* uses the same level convention. Each helper names the level it supplies,
* stamps it into its provenance, and prints it in every failure:
*
* | level | helper | what it proves |
Expand All @@ -13,6 +14,7 @@
* | `packed-stdio` | `openPackedMcpServer` | a built artifact's generated entry running as a real process over stdio |
* | `packed-deleted-source` | `removeProjectSource`, `openPackedMcpServer` | the packed stdio process still runs after project source and configuration are removed and verified absent |
* | `browser-app` | `mountBrowserApp` (`agent-bundle/test/browser`) | production-compiled MCP App HTML mounted over the product bridge in a real browser page |
* | `host-install` | repository real-host install proof | a built bundle accepted through a real host's public install path in an isolated home, with registration observed by that host |
*
* A pass at one level is never a receipt for another. The `deletedSource`
* option upgrades `openPackedMcpServer` provenance only after every path in a
Expand All @@ -21,6 +23,7 @@
export {
BROWSER_APP_PROOF_LEVEL,
CLI_DISPATCH_PROOF_LEVEL,
HOST_INSTALL_PROOF_LEVEL,
MCP_IN_MEMORY_PROOF_LEVEL,
PACKED_DELETED_SOURCE_PROOF_LEVEL,
PACKED_STDIO_PROOF_LEVEL,
Expand Down
12 changes: 9 additions & 3 deletions packages/agent-bundle/src/test/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,30 +28,34 @@ import type {
* resolution, argv projection, and exit codes, not a spawned binary.
* - `packed-stdio` installs the packed release tarball into a clean consumer,
* spawns the generated stdio entry as a real process, and drives it with a
* real MCP client. This is the only level here that is process evidence.
* real MCP client. This is the packed process-and-protocol evidence level.
* - `packed-deleted-source` carries the `packed-stdio` proof after project
* source and configuration have been removed and verified absent. It proves
* that the generated entry is self-contained; it does not prove native-host
* install or dispatch, or an install mode that copies the artifact elsewhere.
*
* - `browser-app` compiles MCP App HTML through the production Rsbuild
* profile and mounts it over the product bridge in a real browser page. It
* does not prove host embedding, a packed artifact, or Workbench behavior.
* - `host-install` installs a built bundle into an isolated real host home
* through the public install path and observes registration through the
* host's own CLI. It does not prove session behavior or packed provenance.
*/
export type AgentTestProofLevel =
| 'route-unit'
| 'mcp-in-memory'
| 'cli-dispatch'
| 'packed-stdio'
| 'packed-deleted-source'
| 'browser-app';
| 'browser-app'
| 'host-install';

export const ROUTE_UNIT_PROOF_LEVEL = 'route-unit' as const;
export const MCP_IN_MEMORY_PROOF_LEVEL = 'mcp-in-memory' as const;
export const CLI_DISPATCH_PROOF_LEVEL = 'cli-dispatch' as const;
export const PACKED_STDIO_PROOF_LEVEL = 'packed-stdio' as const;
export const PACKED_DELETED_SOURCE_PROOF_LEVEL = 'packed-deleted-source' as const;
export const BROWSER_APP_PROOF_LEVEL = 'browser-app' as const;
export const HOST_INSTALL_PROOF_LEVEL = 'host-install' as const;

/**
* One line per level, printed in every harness failure. A red test has to
Expand All @@ -72,6 +76,8 @@ export const proofLevelLabel = (level: AgentTestProofLevel): string => {
return 'packed-deleted-source (packed tarball installed into a clean consumer, artifact built, project source removed and verified absent, generated stdio entry spawned as a real process; self-contained-artifact evidence)';
case 'browser-app':
return 'browser-app (MCP App HTML compiled through the production Rsbuild profile, mounted in a real browser page over the product bridge; NOT host embedding, packed-artifact, or Workbench evidence)';
case 'host-install':
return 'host-install (built bundle installed into an isolated real host home through the public install path, registration observed via the host\'s own CLI; NOT session-behavior or packed-artifact evidence)';
default: {
const exhaustive: never = level;
throw new TypeError(`Unknown proof level ${String(exhaustive)}.`);
Expand Down
4 changes: 2 additions & 2 deletions packages/agent-bundle/tests/adapter-metadata.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ it('records exact immutable metadata for every built-in target', () => {
expect(registryMetadata(registry, 'codex')).toEqual({
adapterRevision: '1.2.0',
capabilityRevision: '0.147.0',
capabilitySha256: 'd944e508941a0660272a253601019957ae94e9140501f0624f85d111e66d9f28',
capabilitySha256: 'bb0a685d680ffd95c468acfcf2fc20dc8fec672ea718f3dd1934fccad3b726c5',
observedVersion: '0.147.0',
schemas: [
{
Expand Down Expand Up @@ -103,7 +103,7 @@ it('records exact immutable metadata for every built-in target', () => {
expect(registryMetadata(registry, 'claude')).toEqual({
adapterRevision: '1.5.0',
capabilityRevision: '2.1.250',
capabilitySha256: '553ebbec4bb16b6e075489fab6c31b11ce84870466710da98e2389659842090e',
capabilitySha256: 'd78b76bda7020f7ea64d332c50d73f7ba3213ef69731835d474383ea6ef46612',
observedVersion: '2.1.250',
schemas: [
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Claude-only on purpose: skill targeting is project-wide, and the arguments
// and skill-root tokens have no Codex or Cursor Skill Markdown equivalent, so
// selecting either host makes this project fail the build with AB3008.
export default {
plugin: {
description: 'Proves canonical Skill tokens resolving in a real Claude session.',
name: 'host-install-token-proof',
version: '1.0.0',
},
skills: ['skills/token-probe'],
targets: ['claude'],
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "host-install-token-proof-fixture",
"private": true,
"type": "module",
"version": "1.0.0"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
name: token-probe
description: Print the host-resolved token markers. Use when the user asks to run the token probe.
---

# Token probe

Print the following three lines as your entire reply, one per line, verbatim.
The host substitutes the marker values before you see them; never edit, quote,
shorten, or re-derive them, and never add a code fence.

ARGS_MARKER=agent-bundle:token:arguments
PLUGIN_ROOT_MARKER=agent-bundle:path:plugin-root
SKILL_DIR_MARKER=agent-bundle:token:skill-root
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export default {
hooks: {
sessionStart: { handler: './src/hooks/session-start.ts' },
},
marketplace: true,
mcp: {
servers: {
probe: {},
},
},
plugin: {
description: 'Proves real host installation of Skills, Hooks, and MCP metadata.',
name: 'host-install-proof',
version: '1.0.0',
},
skills: ['skills/probe'],
targets: ['claude', 'codex', 'cursor'],
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "host-install-proof-fixture",
"private": true,
"type": "module",
"version": "1.0.0"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
name: probe
description: Confirm that the host-install proof fixture was discovered.
targets:
codex:
interface:
display_name: Host install probe
short_description: Confirm the host-install proof fixture was discovered.
policy:
allow_implicit_invocation: true
dependencies:
tools:
- type: mcp
value: probe
---

# Probe

Report that the host-install proof fixture is available.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export default () => ({
additionalContext: 'The host-install proof fixture is available.',
outcome: 'continue' as const,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { McpServer } from '@modelcontextprotocol/server';

export default () => new McpServer({
name: 'host-install-proof',
version: '1.0.0',
});
Loading
Loading