remote-codex 0.11.50 → 0.11.52

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +31 -0
  2. package/apps/supervisor-api/dist/index.js +2132 -417
  3. package/apps/supervisor-web/dist/assets/index-BBq6mr8o.js +22 -0
  4. package/apps/supervisor-web/dist/assets/{index-Dy8PgXgw.css → index-Bg8FdhS1.css} +1 -1
  5. package/apps/supervisor-web/dist/assets/thread-ui-C5pxOmDJ.js +3974 -0
  6. package/apps/supervisor-web/dist/index.html +3 -3
  7. package/package.json +7 -1
  8. package/packages/acp/src/agent-catalog.test.ts +25 -0
  9. package/packages/acp/src/agent-catalog.ts +3 -1
  10. package/packages/acp/src/capabilities.ts +139 -0
  11. package/packages/acp/src/capability-parity.test.ts +99 -0
  12. package/packages/acp/src/catalog-runtime.test.ts +165 -6
  13. package/packages/acp/src/catalog-runtime.ts +234 -25
  14. package/packages/acp/src/extension-registry.test.ts +198 -0
  15. package/packages/acp/src/extension-registry.ts +285 -0
  16. package/packages/acp/src/extensions.test.ts +45 -0
  17. package/packages/acp/src/extensions.ts +103 -0
  18. package/packages/acp/src/harness-contract.test.ts +81 -0
  19. package/packages/acp/src/harness-contract.ts +62 -0
  20. package/packages/acp/src/index.ts +7 -0
  21. package/packages/acp/src/item-mapper.ts +28 -2
  22. package/packages/acp/src/prompt-content.test.ts +90 -0
  23. package/packages/acp/src/prompt-content.ts +99 -0
  24. package/packages/acp/src/runtimeAdapter.test.ts +471 -5
  25. package/packages/acp/src/runtimeAdapter.ts +791 -60
  26. package/packages/acp/src/session-hydrator.test.ts +135 -0
  27. package/packages/acp/src/session-hydrator.ts +147 -0
  28. package/packages/acp/src/terminal-service.test.ts +21 -2
  29. package/packages/acp/src/terminal-service.ts +23 -3
  30. package/packages/acp/src/test/fixtures/fake-acp-agent.mjs +514 -0
  31. package/packages/acp/src/workspace-boundary.test.ts +32 -0
  32. package/packages/acp/src/workspace-boundary.ts +47 -0
  33. package/packages/agent-runtime/src/types.ts +61 -1
  34. package/packages/codex/src/appServerManager.test.ts +2 -2
  35. package/packages/db/src/repositories.ts +3 -2
  36. package/packages/shared/src/index.ts +32 -1
  37. package/apps/supervisor-web/dist/assets/index-BXypG9kl.js +0 -22
  38. package/apps/supervisor-web/dist/assets/thread-ui-gcslNXur.js +0 -3968
@@ -10,16 +10,16 @@
10
10
  <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
11
11
  <link rel="manifest" href="/site.webmanifest" />
12
12
  <title>Remote Codex</title>
13
- <script type="module" crossorigin src="/assets/index-BXypG9kl.js"></script>
13
+ <script type="module" crossorigin src="/assets/index-BBq6mr8o.js"></script>
14
14
  <link rel="modulepreload" crossorigin href="/assets/react-vendor-Dfg_6BLf.js">
15
15
  <link rel="modulepreload" crossorigin href="/assets/ui-vendor-CuR8GHb0.js">
16
16
  <link rel="modulepreload" crossorigin href="/assets/graph-vendor-DVQUpZ8C.js">
17
17
  <link rel="modulepreload" crossorigin href="/assets/terminal-vendor-C5bTa-Ka.js">
18
18
  <link rel="modulepreload" crossorigin href="/assets/markdown-vendor-BG6PurxI.js">
19
- <link rel="modulepreload" crossorigin href="/assets/thread-ui-gcslNXur.js">
19
+ <link rel="modulepreload" crossorigin href="/assets/thread-ui-C5pxOmDJ.js">
20
20
  <link rel="stylesheet" crossorigin href="/assets/graph-vendor-C5ap-Sga.css">
21
21
  <link rel="stylesheet" crossorigin href="/assets/terminal-vendor-Beg8tuEN.css">
22
- <link rel="stylesheet" crossorigin href="/assets/index-Dy8PgXgw.css">
22
+ <link rel="stylesheet" crossorigin href="/assets/index-Bg8FdhS1.css">
23
23
  </head>
24
24
  <body class="bg-stone-950">
25
25
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "remote-codex",
3
- "version": "0.11.50",
3
+ "version": "0.11.52",
4
4
  "description": "Local web supervisor for Codex workspaces and threads.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -57,12 +57,17 @@
57
57
  "prepack": "pnpm build:package",
58
58
  "build": "pnpm -r --if-present build",
59
59
  "build:package": "pnpm --filter @remote-codex/shared build && pnpm --filter @remote-codex/plugin-runtime build && pnpm --filter @remote-codex/plugin-terminal build && pnpm --filter @remote-codex/supervisor-api build && pnpm --filter @remote-codex/relay-server build && pnpm --filter @remote-codex/supervisor-web build",
60
+ "build:mobile-web-deps": "pnpm --filter @remote-codex/shared build && pnpm --filter @remote-codex/plugin-runtime build && pnpm --filter @remote-codex/plugin-terminal build",
60
61
  "lint": "pnpm -r --if-present lint",
61
62
  "typecheck": "pnpm -r --if-present typecheck",
62
63
  "test": "cross-env NODE_ENV=test pnpm -r --if-present test",
63
64
  "test:e2e": "playwright test",
64
65
  "verify:package": "node scripts/verify-package-install.mjs",
65
66
  "verify:relay": "node scripts/verify-relay-supervisor-smoke.mjs",
67
+ "verify:mobile:inputs": "node scripts/verify-mobile-build-inputs.mjs",
68
+ "verify:mobile:parity-gate": "node scripts/collect-mobile-verification-evidence.mjs --simulator-only --output .local/mobile-parity/verification.json",
69
+ "verify:mobile:release-gate": "node scripts/collect-mobile-verification-evidence.mjs",
70
+ "build:android:release": "bash scripts/build-android-release-apk.sh",
66
71
  "build:windows-device-manager": "dotnet publish apps/windows-device-manager/RemoteCodex.DeviceManager.csproj -c Release -r win-x64 --self-contained true -o artifacts/windows-device-manager/win-x64",
67
72
  "db:migrate": "pnpm --filter @remote-codex/db db:migrate",
68
73
  "release:mobile": "node scripts/publish-mobile-release-assets.mjs",
@@ -113,6 +118,7 @@
113
118
  "tsx": "^4.19.3",
114
119
  "typescript": "^5.8.3",
115
120
  "typescript-eslint": "^8.29.1",
121
+ "yaml": "^2.8.1",
116
122
  "vite": "^6.3.2",
117
123
  "vitest": "^3.1.1"
118
124
  }
@@ -20,6 +20,7 @@ function definition(
20
20
  serverCommand: input.serverCommand ?? process.execPath,
21
21
  serverProbeCommand: input.serverProbeCommand ?? `${nodeCommand} --version`,
22
22
  installCommand: input.installCommand ?? null,
23
+ modelListCommand: input.modelListCommand ?? null,
23
24
  };
24
25
  }
25
26
 
@@ -54,4 +55,28 @@ describe('AcpAgentCatalog', () => {
54
55
  expect(entries[1]?.statusMessage).toContain('Install the base agent first');
55
56
  expect(entries[2]?.statusMessage).toContain('Install its ACP adapter');
56
57
  });
58
+
59
+ it('parses both plain provider model ids and bulleted model lists', async () => {
60
+ const plain = new AcpAgentCatalog({
61
+ definitions: [definition({
62
+ id: 'plain',
63
+ modelListCommand: `${nodeCommand} -e "process.stdout.write('provider/first\\nprovider/second\\n')"`,
64
+ })],
65
+ });
66
+ const bulleted = new AcpAgentCatalog({
67
+ definitions: [definition({
68
+ id: 'bulleted',
69
+ modelListCommand: `${nodeCommand} -e "process.stdout.write('Default model: model-one\\n\\nAvailable models:\\n * model-one (default)\\n - model-two\\n')"`,
70
+ })],
71
+ });
72
+
73
+ await expect(plain.listCommandModels('plain')).resolves.toMatchObject([
74
+ { model: 'provider/first', isDefault: false },
75
+ { model: 'provider/second', isDefault: false },
76
+ ]);
77
+ await expect(bulleted.listCommandModels('bulleted')).resolves.toMatchObject([
78
+ { model: 'model-one', isDefault: true },
79
+ { model: 'model-two', isDefault: false },
80
+ ]);
81
+ });
57
82
  });
@@ -107,6 +107,7 @@ const builtinAcpAgents: AcpAgentDefinition[] = [
107
107
  serverCommand: 'opencode acp',
108
108
  serverProbeCommand: 'opencode acp --help',
109
109
  installCommand: null,
110
+ modelListCommand: 'opencode models',
110
111
  },
111
112
  {
112
113
  id: 'deepseek',
@@ -269,7 +270,8 @@ export class AcpAgentCatalog {
269
270
  const output = `${result.stdout}\n${result.stderr}`;
270
271
  const defaultModel = output.match(/^Default model:\s*(\S+)/m)?.[1] ?? null;
271
272
  const models = output.split(/\r?\n/).flatMap((line) => {
272
- const match = line.match(/^\s*[-*]\s+(\S+?)(?:\s+\(default\))?\s*$/);
273
+ const match = line.match(/^\s*[-*]\s+(\S+?)(?:\s+\(default\))?\s*$/)
274
+ ?? line.match(/^\s*([A-Za-z0-9._-]+\/[A-Za-z0-9._:/-]+)\s*$/);
273
275
  if (!match?.[1]) {
274
276
  return [];
275
277
  }
@@ -0,0 +1,139 @@
1
+ import type * as acp from '@agentclientprotocol/sdk';
2
+
3
+ import type { AgentProviderCapabilities } from '../../agent-runtime/src/types';
4
+ import type { AcpAgentOptionMetadataDto } from '../../shared/src/index';
5
+ import {
6
+ REMOTE_CODEX_HARNESS_EXTENSION_META_KEY,
7
+ type HarnessExtensionDescriptor,
8
+ } from './extensions';
9
+
10
+ export interface AcpNegotiatedCapabilitySnapshot {
11
+ protocolVersion: number;
12
+ agentInfo: {
13
+ name: string;
14
+ title: string | null;
15
+ version: string | null;
16
+ } | null;
17
+ agentCapabilities: acp.AgentCapabilities;
18
+ harnessExtensions: HarnessExtensionDescriptor[];
19
+ legacyExtensions: {
20
+ steering: { supported: boolean } | null;
21
+ goal: {
22
+ version: string | number | null;
23
+ controlMethod: string | null;
24
+ actions: string[];
25
+ } | null;
26
+ };
27
+ }
28
+
29
+ export interface AcpAgentCapabilitySnapshot {
30
+ provider: 'acp';
31
+ agentId: string;
32
+ availability: AcpAgentOptionMetadataDto['availability'];
33
+ negotiated: AcpNegotiatedCapabilitySnapshot | null;
34
+ effectiveCapabilities: AgentProviderCapabilities | null;
35
+ }
36
+
37
+ function isRecord(value: unknown): value is Record<string, unknown> {
38
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
39
+ }
40
+
41
+ function stringValue(value: unknown) {
42
+ return typeof value === 'string' && value.trim() ? value.trim() : null;
43
+ }
44
+
45
+ function harnessExtensions(value: unknown): HarnessExtensionDescriptor[] {
46
+ if (!Array.isArray(value)) {
47
+ return [];
48
+ }
49
+ return value.flatMap((candidate) => {
50
+ if (!isRecord(candidate)) {
51
+ return [];
52
+ }
53
+ const id = stringValue(candidate.id);
54
+ const version = candidate.version;
55
+ const stability = candidate.stability;
56
+ const methods = Array.isArray(candidate.methods)
57
+ ? candidate.methods.filter((item): item is string => typeof item === 'string')
58
+ : [];
59
+ const events = Array.isArray(candidate.events)
60
+ ? candidate.events.filter((item): item is string => typeof item === 'string')
61
+ : [];
62
+ if (
63
+ !id ||
64
+ !Number.isInteger(version) ||
65
+ Number(version) < 1 ||
66
+ (stability !== 'experimental' && stability !== 'stable')
67
+ ) {
68
+ return [];
69
+ }
70
+ return [{
71
+ id,
72
+ version: Number(version),
73
+ stability,
74
+ methods,
75
+ events,
76
+ }];
77
+ });
78
+ }
79
+
80
+ function legacyExtensions(meta: Record<string, unknown>) {
81
+ const steering = isRecord(meta.steering)
82
+ ? { supported: meta.steering.supported === true }
83
+ : null;
84
+ const goal = isRecord(meta.goal)
85
+ ? {
86
+ version:
87
+ typeof meta.goal.version === 'string' || typeof meta.goal.version === 'number'
88
+ ? meta.goal.version
89
+ : null,
90
+ controlMethod: stringValue(meta.goal.controlMethod),
91
+ actions: Array.isArray(meta.goal.actions)
92
+ ? meta.goal.actions.filter((item): item is string => typeof item === 'string')
93
+ : [],
94
+ }
95
+ : null;
96
+ return { steering, goal };
97
+ }
98
+
99
+ export function snapshotAcpInitializeResponse(
100
+ response: acp.InitializeResponse | null,
101
+ ): AcpNegotiatedCapabilitySnapshot | null {
102
+ if (!response) {
103
+ return null;
104
+ }
105
+ const meta = isRecord(response._meta) ? response._meta : {};
106
+ return {
107
+ protocolVersion: response.protocolVersion,
108
+ agentInfo: response.agentInfo
109
+ ? {
110
+ name: response.agentInfo.name,
111
+ title: response.agentInfo.title ?? null,
112
+ version: response.agentInfo.version ?? null,
113
+ }
114
+ : null,
115
+ agentCapabilities: structuredClone(response.agentCapabilities ?? {}),
116
+ harnessExtensions: harnessExtensions(
117
+ meta[REMOTE_CODEX_HARNESS_EXTENSION_META_KEY],
118
+ ),
119
+ legacyExtensions: legacyExtensions(meta),
120
+ };
121
+ }
122
+
123
+ export function snapshotAcpAgentCapabilities(input: {
124
+ agentId: string;
125
+ availability: AcpAgentOptionMetadataDto['availability'];
126
+ negotiated?: AcpNegotiatedCapabilitySnapshot | null;
127
+ effectiveCapabilities: AgentProviderCapabilities;
128
+ }): AcpAgentCapabilitySnapshot {
129
+ const ready = input.availability === 'ready';
130
+ return {
131
+ provider: 'acp',
132
+ agentId: input.agentId,
133
+ availability: input.availability,
134
+ negotiated: ready ? input.negotiated ?? null : null,
135
+ effectiveCapabilities: ready
136
+ ? structuredClone(input.effectiveCapabilities)
137
+ : null,
138
+ };
139
+ }
@@ -0,0 +1,99 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { codexCapabilities } from '../../codex/src/runtimeAdapter';
4
+ import type { AgentProviderCapabilities } from '../../agent-runtime/src/types';
5
+ import { snapshotAcpAgentCapabilities } from './capabilities';
6
+ import {
7
+ acpCapabilities,
8
+ applyNegotiatedAcpCapabilities,
9
+ } from './runtimeAdapter';
10
+
11
+ interface CapabilityDifference {
12
+ capability: string;
13
+ nativeCodex: boolean;
14
+ codexAcp: boolean;
15
+ decision: 'approved-baseline-gap';
16
+ }
17
+
18
+ function capabilityFlags(
19
+ value: AgentProviderCapabilities,
20
+ prefix = '',
21
+ ): Map<string, boolean> {
22
+ const flags = new Map<string, boolean>();
23
+ for (const [key, child] of Object.entries(value)) {
24
+ const path = prefix ? `${prefix}.${key}` : key;
25
+ if (typeof child === 'boolean') {
26
+ flags.set(path, child);
27
+ continue;
28
+ }
29
+ for (const [nestedKey, nestedValue] of Object.entries(child)) {
30
+ if (typeof nestedValue === 'boolean') {
31
+ flags.set(`${path}.${nestedKey}`, nestedValue);
32
+ }
33
+ }
34
+ }
35
+ return flags;
36
+ }
37
+
38
+ function capabilityDifferences(): CapabilityDifference[] {
39
+ const nativeFlags = capabilityFlags(codexCapabilities);
40
+ const acpFlags = capabilityFlags(applyNegotiatedAcpCapabilities(
41
+ structuredClone(acpCapabilities),
42
+ {
43
+ loadSession: true,
44
+ promptCapabilities: { image: true, embeddedContext: true },
45
+ sessionCapabilities: {
46
+ list: {},
47
+ resume: {},
48
+ close: {},
49
+ delete: {},
50
+ },
51
+ },
52
+ ));
53
+ return [...nativeFlags.entries()].flatMap(([capability, nativeCodex]) => {
54
+ const codexAcp = acpFlags.get(capability);
55
+ return codexAcp === undefined || codexAcp === nativeCodex
56
+ ? []
57
+ : [{
58
+ capability,
59
+ nativeCodex,
60
+ codexAcp,
61
+ decision: 'approved-baseline-gap' as const,
62
+ }];
63
+ });
64
+ }
65
+
66
+ describe('native Codex and Codex ACP capability baseline', () => {
67
+ it('requires every current capability difference to remain explicit', () => {
68
+ expect(capabilityDifferences()).toEqual([
69
+ { capability: 'sessions.importLocal', nativeCodex: true, codexAcp: false, decision: 'approved-baseline-gap' },
70
+ { capability: 'turns.steer', nativeCodex: true, codexAcp: false, decision: 'approved-baseline-gap' },
71
+ { capability: 'turns.compact', nativeCodex: true, codexAcp: false, decision: 'approved-baseline-gap' },
72
+ { capability: 'branching.fork', nativeCodex: true, codexAcp: false, decision: 'approved-baseline-gap' },
73
+ { capability: 'branching.hardRollback', nativeCodex: true, codexAcp: false, decision: 'approved-baseline-gap' },
74
+ { capability: 'controls.sandboxMode', nativeCodex: false, codexAcp: true, decision: 'approved-baseline-gap' },
75
+ { capability: 'controls.performanceMode', nativeCodex: true, codexAcp: false, decision: 'approved-baseline-gap' },
76
+ { capability: 'controls.goals', nativeCodex: true, codexAcp: false, decision: 'approved-baseline-gap' },
77
+ { capability: 'management.models', nativeCodex: true, codexAcp: false, decision: 'approved-baseline-gap' },
78
+ { capability: 'management.mcpStatus', nativeCodex: true, codexAcp: false, decision: 'approved-baseline-gap' },
79
+ { capability: 'management.skills', nativeCodex: true, codexAcp: false, decision: 'approved-baseline-gap' },
80
+ { capability: 'management.hooks', nativeCodex: true, codexAcp: false, decision: 'approved-baseline-gap' },
81
+ { capability: 'management.hookTrust', nativeCodex: true, codexAcp: false, decision: 'approved-baseline-gap' },
82
+ { capability: 'management.hostConfigFiles', nativeCodex: true, codexAcp: false, decision: 'approved-baseline-gap' },
83
+ ]);
84
+ });
85
+
86
+ it('does not present unavailable ACP agents as effective capability owners', () => {
87
+ expect(snapshotAcpAgentCapabilities({
88
+ agentId: 'codex',
89
+ availability: 'adapter_missing',
90
+ effectiveCapabilities: acpCapabilities,
91
+ })).toEqual({
92
+ provider: 'acp',
93
+ agentId: 'codex',
94
+ availability: 'adapter_missing',
95
+ negotiated: null,
96
+ effectiveCapabilities: null,
97
+ });
98
+ });
99
+ });
@@ -1,6 +1,6 @@
1
1
  import path from 'node:path';
2
2
 
3
- import { afterEach, describe, expect, it } from 'vitest';
3
+ import { afterEach, describe, expect, it, vi } from 'vitest';
4
4
 
5
5
  import type {
6
6
  AgentProviderRequest,
@@ -16,6 +16,7 @@ const runtimes: AcpCatalogRuntimeAdapter[] = [];
16
16
 
17
17
  afterEach(async () => {
18
18
  await Promise.all(runtimes.splice(0).map((runtime) => runtime.stop()));
19
+ vi.unstubAllEnvs();
19
20
  });
20
21
 
21
22
  describe('AcpCatalogRuntimeAdapter', () => {
@@ -52,12 +53,26 @@ describe('AcpCatalogRuntimeAdapter', () => {
52
53
  state: 'degraded',
53
54
  lastError: 'No supported base agent was detected.',
54
55
  });
56
+ expect(await runtime.getAgentCapabilitySnapshot('missing-agent')).toEqual({
57
+ provider: 'acp',
58
+ agentId: 'missing-agent',
59
+ availability: 'base_missing',
60
+ negotiated: null,
61
+ effectiveCapabilities: null,
62
+ });
63
+ await expect(
64
+ runtime.listModelsForAgent('missing-agent', process.cwd()),
65
+ ).rejects.toThrow(/Install the base agent first/);
66
+ expect(runtime.getStatus().operationalMetrics).toEqual({
67
+ sessionStartFailures: 0,
68
+ resumeFailures: 0,
69
+ capabilityProbeFailures: 1,
70
+ });
55
71
  });
56
72
 
57
73
  it('selects a concrete agent and scopes its provider session id', async () => {
58
- const fixture = path.resolve(
59
- 'node_modules/@agentclientprotocol/sdk/dist/examples/agent.js',
60
- );
74
+ const fixture = path.resolve('src/test/fixtures/fake-acp-agent.mjs');
75
+ vi.stubEnv('REMOTE_CODEX_FAKE_ACP_FORK', '1');
61
76
  const runtime = new AcpCatalogRuntimeAdapter({
62
77
  catalog: new AcpAgentCatalog({
63
78
  definitions: [{
@@ -103,7 +118,21 @@ describe('AcpCatalogRuntimeAdapter', () => {
103
118
  agentId: 'fixture-agent',
104
119
  });
105
120
  expect(session.agentId).toBe('fixture-agent');
106
- expect(session.model).toBe('default');
121
+ expect(session.model).toBe('fixture-model');
122
+ expect(await runtime.getAgentCapabilitySnapshot('fixture-agent')).toMatchObject({
123
+ provider: 'acp',
124
+ agentId: 'fixture-agent',
125
+ availability: 'ready',
126
+ effectiveCapabilities: {
127
+ sessions: {
128
+ list: true,
129
+ load: true,
130
+ resume: true,
131
+ close: true,
132
+ delete: true,
133
+ },
134
+ },
135
+ });
107
136
 
108
137
  const completed = new Promise<Extract<AgentRuntimeEvent, { type: 'turn.completed' }>>(
109
138
  (resolve, reject) => {
@@ -127,8 +156,138 @@ describe('AcpCatalogRuntimeAdapter', () => {
127
156
  expect(event.turn.items.map((item) => item.kind)).toEqual(expect.arrayContaining([
128
157
  'userMessage',
129
158
  'agentMessage',
130
- 'fileRead',
131
159
  'fileChange',
160
+ 'commandExecution',
132
161
  ]));
162
+ const forked = await runtime.forkSession({
163
+ providerSessionId: session.providerSessionId,
164
+ });
165
+ expect(acpSessionId.decode(forked.providerSessionId)).toMatchObject({
166
+ agentId: 'fixture-agent',
167
+ });
168
+ expect(forked.agentId).toBe('fixture-agent');
169
+ }, 15_000);
170
+
171
+ it('delegates Codex child controls while preserving scoped goal identity', async () => {
172
+ const fixture = path.resolve('src/test/fixtures/fake-acp-agent.mjs');
173
+ vi.stubEnv('REMOTE_CODEX_FAKE_ACP_AGENT_KIND', 'codex');
174
+ vi.stubEnv('REMOTE_CODEX_FAKE_ACP_SKIP_PERMISSION', '1');
175
+ const runtime = new AcpCatalogRuntimeAdapter({
176
+ catalog: new AcpAgentCatalog({
177
+ definitions: [{
178
+ id: 'codex',
179
+ displayName: 'Codex fixture',
180
+ description: 'Codex ACP fixture',
181
+ transport: 'adapter',
182
+ baseCommand: process.execPath,
183
+ baseProbeCommand: `"${process.execPath}" --version`,
184
+ serverCommand: `"${process.execPath}" "${fixture}"`,
185
+ serverProbeCommand: `"${process.execPath}" --version`,
186
+ installCommand: null,
187
+ }],
188
+ }),
189
+ startupTimeoutMs: 5_000,
190
+ });
191
+ runtimes.push(runtime);
192
+ await runtime.start();
193
+ await expect(runtime.listImportSessions('codex')).resolves.toEqual([]);
194
+ expect(runtime.managementSchema.toolboxItems.map((item) => item.command)).toEqual([
195
+ '/fast',
196
+ '/compact',
197
+ '/goal',
198
+ '/fork',
199
+ ]);
200
+ expect(await runtime.listModelsForAgent('codex', process.cwd())).toEqual(
201
+ expect.arrayContaining([
202
+ expect.objectContaining({ supportsPerformanceMode: true }),
203
+ ]),
204
+ );
205
+ expect(runtime.capabilities).toMatchObject({
206
+ turns: { steer: true, compact: true },
207
+ controls: { goals: true },
208
+ });
209
+ expect(runtime.getScopedCapabilities({ agentId: 'codex' })).toMatchObject({
210
+ turns: { steer: true, compact: true },
211
+ controls: { goals: true },
212
+ });
213
+ expect(runtime.getScopedCapabilities({ agentId: 'unstarted-agent' })).toMatchObject({
214
+ turns: { steer: false, compact: false },
215
+ controls: { goals: false, performanceMode: false },
216
+ });
217
+ const session = await runtime.startSession({
218
+ cwd: process.cwd(),
219
+ agentId: 'codex',
220
+ model: 'fixture-model',
221
+ approvalMode: 'yolo',
222
+ });
223
+ expect(runtime.capabilities).toMatchObject({
224
+ turns: { steer: true, compact: true },
225
+ controls: { goals: true },
226
+ });
227
+ const goal = await runtime.setGoal({
228
+ providerSessionId: session.providerSessionId,
229
+ objective: 'Scoped catalog goal',
230
+ });
231
+ expect(goal).toMatchObject({
232
+ providerSessionId: session.providerSessionId,
233
+ objective: 'Scoped catalog goal',
234
+ });
235
+ await expect(runtime.compactSession(session.providerSessionId)).resolves.toBeUndefined();
236
+ expect(await runtime.clearGoal(session.providerSessionId)).toBe(true);
237
+
238
+ await runtime.stop();
239
+ expect(runtime.capabilities).toMatchObject({
240
+ turns: { steer: false, compact: false },
241
+ controls: { goals: false, performanceMode: false },
242
+ });
243
+ }, 15_000);
244
+
245
+ it('keeps request and session scopes distinct for guarded permissions', async () => {
246
+ const fixture = path.resolve('src/test/fixtures/fake-acp-agent.mjs');
247
+ const runtime = new AcpCatalogRuntimeAdapter({
248
+ catalog: new AcpAgentCatalog({
249
+ definitions: [{
250
+ id: 'guarded-agent',
251
+ displayName: 'Guarded fixture',
252
+ description: 'Guarded ACP fixture',
253
+ transport: 'native',
254
+ baseCommand: process.execPath,
255
+ baseProbeCommand: `"${process.execPath}" --version`,
256
+ serverCommand: `"${process.execPath}" "${fixture}"`,
257
+ serverProbeCommand: `"${process.execPath}" --version`,
258
+ installCommand: null,
259
+ }],
260
+ }),
261
+ startupTimeoutMs: 5_000,
262
+ });
263
+ runtimes.push(runtime);
264
+ const permission = new Promise<void>((resolve, reject) => {
265
+ const timer = setTimeout(() => reject(new Error('Guarded permission timed out.')), 10_000);
266
+ runtime.on('provider-request', (request: AgentProviderRequest) => {
267
+ const mapping = runtime.mapProviderRequest(request, { approvalMode: 'guarded' });
268
+ if (!mapping?.pendingRequest) return;
269
+ clearTimeout(timer);
270
+ expect(acpSessionId.decode(mapping.providerSessionId)).toMatchObject({
271
+ agentId: 'guarded-agent',
272
+ });
273
+ const result = runtime.buildProviderRequestResponse(
274
+ mapping.pendingRequest,
275
+ { answers: { permission: { answers: ['Allow once'] } } },
276
+ );
277
+ runtime.respondToProviderRequest(mapping.providerRequestId, result);
278
+ resolve();
279
+ });
280
+ });
281
+ const session = await runtime.startSession({
282
+ cwd: process.cwd(),
283
+ agentId: 'guarded-agent',
284
+ model: 'fixture-model',
285
+ approvalMode: 'guarded',
286
+ });
287
+ await runtime.startTurn({
288
+ providerSessionId: session.providerSessionId,
289
+ prompt: 'Request guarded fixture permission.',
290
+ });
291
+ await permission;
133
292
  }, 15_000);
134
293
  });