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
9 changes: 7 additions & 2 deletions packages/daemon/src/mcp/remote-mcp-runtimes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
22 changes: 22 additions & 0 deletions packages/daemon/src/runtimes/managed.ts
Original file line number Diff line number Diff line change
@@ -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<Record<string, ManagedRuntimeEntry>> = 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: []
}
}
})
14 changes: 10 additions & 4 deletions packages/daemon/src/runtimes/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -34,7 +35,7 @@ export const RegistryDocSchema = z
}))
export type RegistryDoc = { agents: Record<string, RegistryEntry> }

export type RuntimeSource = 'curated' | 'registry' | 'user'
export type RuntimeSource = 'curated' | 'registry' | 'managed' | 'user'

export interface ResolvedRuntimeEntry {
runtime: RuntimeDef
Expand Down Expand Up @@ -209,7 +210,8 @@ function runtimeMap(entries: Record<string, ResolvedRuntimeEntry>): Record<strin
return Object.fromEntries(Object.entries(entries).map(([id, entry]) => [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,
Expand All @@ -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)

Expand All @@ -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, '')
}
Expand Down
36 changes: 36 additions & 0 deletions packages/daemon/test/registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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 () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ const TARGETS = (process.env.REMOTE_MCP_ADAPTER_IT ?? '')
*/
const ADAPTERS: Record<string, { command: string; args: string[] }> = {
'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'] }
}
Expand Down
5 changes: 5 additions & 0 deletions packages/daemon/test/remote-mcp-runtimes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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']

Expand Down Expand Up @@ -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)
Expand Down