diff --git a/packages/daemon/src/mcp/remote-mcp-runtimes.ts b/packages/daemon/src/mcp/remote-mcp-runtimes.ts index ebdeb8d89..e556b7042 100644 --- a/packages/daemon/src/mcp/remote-mcp-runtimes.ts +++ b/packages/daemon/src/mcp/remote-mcp-runtimes.ts @@ -56,6 +56,10 @@ import type { ResolvedRuntimeEntry } from '../runtimes/registry.js' * on rotation → [0] after restart, and 337 bytes of clean diagnostics. * * NOT admitted, evidence unmet: + * - npx -y @agentconnect.md/codex-acp@1.1.8-agentconnect.1 connected its + * descriptor in the 2026-08-02 harness run, but two explicit probe turns + * produced zero `tools/call` requests. It therefore fails the non-vacuous + * tool-execution and retry requirements. * - omp 17.0.5 satisfied transport, tool execution, isolation, rotation, and * leak checks, but its random JSON-RPC ids did not reuse after rotation or * restart. It therefore fails the current §13 restart-id-reuse requirement. @@ -100,8 +104,9 @@ function sameLaunch( * 1. the id is a canonical adapter with validated evidence; * 2. its definition came from the daemon's own resolution of the curated * catalog / public ACP registry document (`source: 'curated' | 'registry'`) - * — a user-configured runtime, including one shadowing a validated id, is - * never admitted; and + * — a user-configured runtime, including one shadowing a validated id, and + * an AgentConnect-managed build without passing evidence are never + * admitted; and * 3. the resolved launch matches a validated launch EXACTLY (catalog version, * command, args, no env), and binary launches whose command does not pin an * artifact additionally match the actual `agentInfo.version` observed by diff --git a/packages/daemon/src/runtimes/managed.ts b/packages/daemon/src/runtimes/managed.ts new file mode 100644 index 000000000..6d80b697d --- /dev/null +++ b/packages/daemon/src/runtimes/managed.ts @@ -0,0 +1,22 @@ +import type { RuntimeDef } from '../config/config-schema.js' + +export interface ManagedRuntimeEntry { + name: string + version: string + runtime: RuntimeDef +} + +/** AgentConnect-maintained runtime builds that intentionally override the + * public ACP registry. Explicit operator config remains the final authority. */ +export const MANAGED_RUNTIME_CATALOG: Readonly> = Object.freeze({ + 'codex-acp': { + name: 'Codex', + // The ACP probe reports the concrete version resolved by this release channel. + version: '', + runtime: { + command: 'npx', + args: ['-y', '@agentconnect.md/codex-acp@agentconnect'], + env: [] + } + } +}) diff --git a/packages/daemon/src/runtimes/registry.ts b/packages/daemon/src/runtimes/registry.ts index 934a1c6ec..b1c228ccb 100644 --- a/packages/daemon/src/runtimes/registry.ts +++ b/packages/daemon/src/runtimes/registry.ts @@ -5,6 +5,7 @@ import type { RuntimeDef, Config } from '../config/config-schema.js' import { registryPath, registryCachePath } from '../paths.js' import { CURATED_RUNTIME_CATALOG } from './curated.js' import { skillsAgentIdForRuntime } from './skills-capability.js' +import { MANAGED_RUNTIME_CATALOG } from './managed.js' const PackageDistSchema = z.object({ package: z.string(), args: z.array(z.string()).default([]) }) const BinaryPlatformSchema = z.object({ @@ -34,7 +35,7 @@ export const RegistryDocSchema = z })) export type RegistryDoc = { agents: Record } -export type RuntimeSource = 'curated' | 'registry' | 'user' +export type RuntimeSource = 'curated' | 'registry' | 'managed' | 'user' export interface ResolvedRuntimeEntry { runtime: RuntimeDef @@ -209,7 +210,8 @@ function runtimeMap(entries: Record): Record [id, entry.runtime])) } -/** Resolve curated, usable registry, and explicit user definitions with source metadata. */ +/** Resolve curated, usable registry, AgentConnect-managed, and explicit user + * definitions with source metadata. Later layers take precedence. */ export async function resolveRuntimeCatalog( cfg: Config, root: string, @@ -228,8 +230,9 @@ export async function resolveRuntimeCatalog( ) const userRuntimes = cfg.runtimes ?? {} const needed = opts.neededRuntimes - const userCoversNeeded = needed && needed.length > 0 && needed.every((id) => userRuntimes[id]) - const registry = userCoversNeeded + const localCoversNeeded = + needed && needed.length > 0 && needed.every((id) => userRuntimes[id] || MANAGED_RUNTIME_CATALOG[id]) + const registry = localCoversNeeded ? (readCachedDoc(root) ?? { agents: {} }) : await registryDocForResolution(root, opts) @@ -238,6 +241,9 @@ export async function resolveRuntimeCatalog( if (!runtime) continue entries[id] = resolvedRuntimeEntry(id, runtime, 'registry', entry.name || id, entry.version) } + for (const [id, entry] of Object.entries(MANAGED_RUNTIME_CATALOG)) { + entries[id] = resolvedRuntimeEntry(id, entry.runtime, 'managed', entry.name, entry.version) + } for (const [id, runtime] of Object.entries(userRuntimes)) { entries[id] = resolvedRuntimeEntry(id, runtime, 'user', id, '') } diff --git a/packages/daemon/test/registry.test.ts b/packages/daemon/test/registry.test.ts index d1012b8ce..a82324389 100644 --- a/packages/daemon/test/registry.test.ts +++ b/packages/daemon/test/registry.test.ts @@ -9,6 +9,7 @@ import { resolveRuntimeCatalog } from '../src/runtimes/registry.js' import { CURATED_RUNTIME_CATALOG } from '../src/runtimes/curated.js' +import { MANAGED_RUNTIME_CATALOG } from '../src/runtimes/managed.js' import { mkdtempSync, readFileSync, writeFileSync, existsSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' @@ -172,6 +173,41 @@ describe('cachedRuntimeNames', () => { }) describe('resolveRuntimes', () => { + it('uses the managed Codex build by default while preserving operator overrides', async () => { + const root = tmpRoot() + const registry = { + agents: { + 'codex-acp': { + id: 'codex-acp', + name: 'Registry Codex', + version: '1.1.8', + distribution: { npx: { package: '@agentclientprotocol/codex-acp@1.1.8' } } + } + } + } + const fetchImpl = (async () => new Response(JSON.stringify(registry), { status: 200 })) as typeof fetch + + const managed = await resolveRuntimeCatalog({} as any, root, { neededRuntimes: ['other'], fetchImpl }) + expect(managed.entries['codex-acp']).toEqual({ + ...MANAGED_RUNTIME_CATALOG['codex-acp'], + source: 'managed', + skillsAgentId: 'codex' + }) + + const configured = await resolveRuntimeCatalog( + { runtimes: { 'codex-acp': { command: '/custom/codex-acp', args: [], env: [] } } } as any, + root, + { neededRuntimes: ['codex-acp'], fetchImpl } + ) + expect(configured.entries['codex-acp']).toEqual({ + runtime: { command: '/custom/codex-acp', args: [], env: [] }, + source: 'user', + name: 'codex-acp', + version: '', + skillsAgentId: null + }) + }) + it('skips the registry fetch entirely when config covers all needed runtimes', async () => { let called = false const fetchImpl = (async () => { diff --git a/packages/daemon/test/remote-mcp-adapter-behavior.it.test.ts b/packages/daemon/test/remote-mcp-adapter-behavior.it.test.ts index 5cb694255..b195b5ff4 100644 --- a/packages/daemon/test/remote-mcp-adapter-behavior.it.test.ts +++ b/packages/daemon/test/remote-mcp-adapter-behavior.it.test.ts @@ -69,7 +69,7 @@ const TARGETS = (process.env.REMOTE_MCP_ADAPTER_IT ?? '') */ const ADAPTERS: Record = { 'claude-acp': { command: 'npx', args: ['-y', '@agentclientprotocol/claude-agent-acp@0.64.0'] }, - 'codex-acp': { command: 'npx', args: ['-y', '@agentclientprotocol/codex-acp@1.1.7'] }, + 'codex-acp': { command: 'npx', args: ['-y', '@agentconnect.md/codex-acp@agentconnect'] }, opencode: { command: './opencode', args: ['acp'] }, 'grok-build': { command: 'npx', args: ['-y', '@xai-official/grok@0.2.118', 'agent', 'stdio'] } } diff --git a/packages/daemon/test/remote-mcp-runtimes.test.ts b/packages/daemon/test/remote-mcp-runtimes.test.ts index e07738ff7..efe7a8d97 100644 --- a/packages/daemon/test/remote-mcp-runtimes.test.ts +++ b/packages/daemon/test/remote-mcp-runtimes.test.ts @@ -17,6 +17,7 @@ const entry = ( const CLAUDE = ['-y', '@agentclientprotocol/claude-agent-acp@0.64.0'] const CODEX = ['-y', '@agentclientprotocol/codex-acp@1.1.7'] +const MANAGED_CODEX = ['-y', '@agentconnect.md/codex-acp@agentconnect'] const OPENCODE = ['acp'] const GROK = ['-y', '@xai-official/grok@0.2.118', 'agent', 'stdio'] @@ -115,6 +116,10 @@ describe('isValidatedRemoteMcpRuntime', () => { expect(isValidatedRemoteMcpRuntime('codex-acp', entry('user', '/opt/leaky-acp'))).toBe(false) }) + it('keeps the managed Codex build fail-closed after its behavioral harness failure', () => { + expect(isValidatedRemoteMcpRuntime('codex-acp', entry('managed', 'npx', MANAGED_CODEX))).toBe(false) + }) + it('never infers admission from claude/codex-looking launch lines (§13)', () => { expect(isValidatedRemoteMcpRuntime('custom', entry('user', '/opt/leaky-acp', ['codex-acp']))).toBe(false) expect(isValidatedRemoteMcpRuntime('custom', entry('user', '/opt/leaky-acp', ['--profile=claude']))).toBe(false)