remote-codex 0.11.49 → 0.11.51
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.
- package/README.md +38 -5
- package/apps/supervisor-api/dist/index.js +2137 -422
- package/apps/supervisor-web/dist/assets/index-7LiZQ3aJ.js +22 -0
- package/apps/supervisor-web/dist/assets/{index-Dy8PgXgw.css → index-Bg8FdhS1.css} +1 -1
- package/apps/supervisor-web/dist/assets/{thread-ui-gcslNXur.js → thread-ui-BbtcUwps.js} +25 -19
- package/apps/supervisor-web/dist/index.html +3 -3
- package/bin/remote-codex.mjs +4 -2
- package/package.json +7 -1
- package/packages/acp/src/agent-catalog.test.ts +25 -0
- package/packages/acp/src/agent-catalog.ts +3 -1
- package/packages/acp/src/capabilities.ts +139 -0
- package/packages/acp/src/capability-parity.test.ts +99 -0
- package/packages/acp/src/catalog-runtime.test.ts +200 -6
- package/packages/acp/src/catalog-runtime.ts +239 -31
- package/packages/acp/src/extension-registry.test.ts +198 -0
- package/packages/acp/src/extension-registry.ts +285 -0
- package/packages/acp/src/extensions.test.ts +45 -0
- package/packages/acp/src/extensions.ts +103 -0
- package/packages/acp/src/harness-contract.test.ts +81 -0
- package/packages/acp/src/harness-contract.ts +62 -0
- package/packages/acp/src/index.ts +7 -0
- package/packages/acp/src/item-mapper.ts +28 -2
- package/packages/acp/src/prompt-content.test.ts +90 -0
- package/packages/acp/src/prompt-content.ts +99 -0
- package/packages/acp/src/runtimeAdapter.test.ts +458 -5
- package/packages/acp/src/runtimeAdapter.ts +791 -60
- package/packages/acp/src/session-hydrator.test.ts +135 -0
- package/packages/acp/src/session-hydrator.ts +147 -0
- package/packages/acp/src/terminal-service.test.ts +21 -2
- package/packages/acp/src/terminal-service.ts +23 -3
- package/packages/acp/src/test/fixtures/fake-acp-agent.mjs +514 -0
- package/packages/acp/src/workspace-boundary.test.ts +32 -0
- package/packages/acp/src/workspace-boundary.ts +47 -0
- package/packages/agent-runtime/src/types.ts +61 -1
- package/packages/db/src/repositories.ts +3 -2
- package/packages/shared/src/index.ts +32 -1
- package/scripts/service-manager.mjs +13 -0
- package/apps/supervisor-web/dist/assets/index-DZI1aSXo.js +0 -22
|
@@ -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-
|
|
13
|
+
<script type="module" crossorigin src="/assets/index-7LiZQ3aJ.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-
|
|
19
|
+
<link rel="modulepreload" crossorigin href="/assets/thread-ui-BbtcUwps.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-
|
|
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/bin/remote-codex.mjs
CHANGED
|
@@ -347,6 +347,7 @@ function resolveSupervisorLaunch() {
|
|
|
347
347
|
function relaySupervisorProcessEnv(extra = {}) {
|
|
348
348
|
return {
|
|
349
349
|
...process.env,
|
|
350
|
+
APP_VERSION: process.env.APP_VERSION ?? readPackageVersion(),
|
|
350
351
|
REMOTE_CODEX_MODE: 'relay',
|
|
351
352
|
REMOTE_CODEX_PACKAGE_ROOT: process.env.REMOTE_CODEX_PACKAGE_ROOT ?? packageRoot,
|
|
352
353
|
REMOTE_CODEX_DISABLE_BUILD_RESTART:
|
|
@@ -408,7 +409,7 @@ async function startRelaySupervisorWindowsUnlocked() {
|
|
|
408
409
|
REMOTE_CODEX_LIFECYCLE_CONTROL_TOKEN: controlToken,
|
|
409
410
|
REMOTE_CODEX_LIFECYCLE_INSTANCE_ID: instanceId,
|
|
410
411
|
REMOTE_CODEX_ENABLED_AGENT_PROVIDERS:
|
|
411
|
-
process.env.REMOTE_CODEX_ENABLED_AGENT_PROVIDERS ?? 'codex',
|
|
412
|
+
process.env.REMOTE_CODEX_ENABLED_AGENT_PROVIDERS ?? 'codex,acp',
|
|
412
413
|
}),
|
|
413
414
|
stdio: ['ignore', logFd, logFd],
|
|
414
415
|
});
|
|
@@ -1437,7 +1438,8 @@ Recommended environment:
|
|
|
1437
1438
|
CODEX_COMMAND
|
|
1438
1439
|
Codex executable. Default codex.
|
|
1439
1440
|
REMOTE_CODEX_ENABLED_AGENT_PROVIDERS
|
|
1440
|
-
Comma-separated provider ids
|
|
1441
|
+
Comma-separated provider ids. Defaults to codex,claude,opencode,acp on Unix
|
|
1442
|
+
and codex,acp on Windows.
|
|
1441
1443
|
REMOTE_CODEX_RELAY_SUPERVISOR_TMUX
|
|
1442
1444
|
Set to 0/false/no/off to disable default tmux management.
|
|
1443
1445
|
REMOTE_CODEX_RELAY_SUPERVISOR_TMUX_SESSION
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "remote-codex",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.51",
|
|
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,13 +16,63 @@ 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', () => {
|
|
23
|
+
it('keeps the built-in ACP runtime selectable when every base agent is missing', async () => {
|
|
24
|
+
const runtime = new AcpCatalogRuntimeAdapter({
|
|
25
|
+
catalog: new AcpAgentCatalog({
|
|
26
|
+
definitions: [{
|
|
27
|
+
id: 'missing-agent',
|
|
28
|
+
displayName: 'Missing Agent',
|
|
29
|
+
description: 'Missing ACP fixture',
|
|
30
|
+
transport: 'native',
|
|
31
|
+
baseCommand: 'remote-codex-missing-acp-agent',
|
|
32
|
+
baseProbeCommand: 'remote-codex-missing-acp-agent --version',
|
|
33
|
+
serverCommand: 'remote-codex-missing-acp-agent acp',
|
|
34
|
+
serverProbeCommand: 'remote-codex-missing-acp-agent acp --help',
|
|
35
|
+
installCommand: null,
|
|
36
|
+
}],
|
|
37
|
+
}),
|
|
38
|
+
});
|
|
39
|
+
runtimes.push(runtime);
|
|
40
|
+
|
|
41
|
+
const agents = await runtime.listModels();
|
|
42
|
+
|
|
43
|
+
expect(agents).toMatchObject([{
|
|
44
|
+
id: 'missing-agent',
|
|
45
|
+
acpAgent: { availability: 'base_missing' },
|
|
46
|
+
}]);
|
|
47
|
+
expect(runtime.installation).toMatchObject({
|
|
48
|
+
installed: true,
|
|
49
|
+
installedVersion: 'Built in · 0 ACP agents ready',
|
|
50
|
+
lastError: 'No supported base agent was detected.',
|
|
51
|
+
});
|
|
52
|
+
expect(runtime.getStatus()).toMatchObject({
|
|
53
|
+
state: 'degraded',
|
|
54
|
+
lastError: 'No supported base agent was detected.',
|
|
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
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
22
73
|
it('selects a concrete agent and scopes its provider session id', async () => {
|
|
23
|
-
const fixture = path.resolve(
|
|
24
|
-
|
|
25
|
-
);
|
|
74
|
+
const fixture = path.resolve('src/test/fixtures/fake-acp-agent.mjs');
|
|
75
|
+
vi.stubEnv('REMOTE_CODEX_FAKE_ACP_FORK', '1');
|
|
26
76
|
const runtime = new AcpCatalogRuntimeAdapter({
|
|
27
77
|
catalog: new AcpAgentCatalog({
|
|
28
78
|
definitions: [{
|
|
@@ -68,7 +118,21 @@ describe('AcpCatalogRuntimeAdapter', () => {
|
|
|
68
118
|
agentId: 'fixture-agent',
|
|
69
119
|
});
|
|
70
120
|
expect(session.agentId).toBe('fixture-agent');
|
|
71
|
-
expect(session.model).toBe('
|
|
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
|
+
});
|
|
72
136
|
|
|
73
137
|
const completed = new Promise<Extract<AgentRuntimeEvent, { type: 'turn.completed' }>>(
|
|
74
138
|
(resolve, reject) => {
|
|
@@ -92,8 +156,138 @@ describe('AcpCatalogRuntimeAdapter', () => {
|
|
|
92
156
|
expect(event.turn.items.map((item) => item.kind)).toEqual(expect.arrayContaining([
|
|
93
157
|
'userMessage',
|
|
94
158
|
'agentMessage',
|
|
95
|
-
'fileRead',
|
|
96
159
|
'fileChange',
|
|
160
|
+
'commandExecution',
|
|
97
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;
|
|
98
292
|
}, 15_000);
|
|
99
293
|
});
|