@ours.network/fleet 0.18.0-nightly.5 → 0.18.0
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 +101 -25
- package/dist/application/fleet-query-service.js +12 -0
- package/dist/application/role-creation-service.js +3 -1
- package/dist/application/types.d.ts +11 -0
- package/dist/briefing.js +9 -2
- package/dist/build-info.json +7 -6
- package/dist/capabilities.d.ts +3 -1
- package/dist/capabilities.js +3 -0
- package/dist/cli.js +83 -7
- package/dist/config.d.ts +11 -3
- package/dist/config.js +40 -15
- package/dist/creation.d.ts +14 -15
- package/dist/creation.js +19 -13
- package/dist/docs.d.ts +1 -1
- package/dist/docs.js +113 -27
- package/dist/doctor.d.ts +1 -5
- package/dist/doctor.js +11 -18
- package/dist/fleet-proxy.d.ts +5 -0
- package/dist/harness/acp-agent.js +11 -6
- package/dist/harness/claude-code.js +204 -11
- package/dist/harness/codex.d.ts +4 -1
- package/dist/harness/codex.js +74 -12
- package/dist/harness/types.d.ts +54 -4
- package/dist/harness-plugins.d.ts +48 -0
- package/dist/harness-plugins.js +309 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/loops/manager.d.ts +30 -1
- package/dist/loops/manager.js +69 -6
- package/dist/loops/state.d.ts +18 -0
- package/dist/loops/state.js +4 -0
- package/dist/model-env.d.ts +71 -0
- package/dist/model-env.js +106 -0
- package/dist/monitor.js +1 -1
- package/dist/ops.js +1 -1
- package/dist/owner-channel/attachments.d.ts +2 -25
- package/dist/owner-channel/attachments.js +5 -61
- package/dist/owner-channel/channel.d.ts +28 -17
- package/dist/owner-channel/channel.js +249 -158
- package/dist/owner-channel/mcp.d.ts +24 -0
- package/dist/owner-channel/mcp.js +145 -0
- package/dist/owner-channel/notices.d.ts +7 -0
- package/dist/owner-channel/notices.js +9 -0
- package/dist/resolved-plan.js +1 -0
- package/dist/runner.d.ts +48 -0
- package/dist/runner.js +237 -85
- package/dist/session/acp.d.ts +104 -0
- package/dist/session/acp.js +213 -10
- package/dist/session/activity.d.ts +31 -0
- package/dist/session/activity.js +48 -0
- package/dist/session/conversation-normalizer.d.ts +6 -0
- package/dist/session/conversation-normalizer.js +153 -10
- package/dist/session/conversation-types.d.ts +23 -4
- package/dist/session/types.d.ts +35 -0
- package/dist/spawn.js +29 -17
- package/dist/supervisor/systemd.js +2 -29
- package/dist/watchdog/briefing.js +7 -0
- package/dist/web-app/assets/{TerminalView-BAVk1Bot.js → TerminalView-C_G1ID2P.js} +1 -1
- package/dist/web-app/assets/{index-C3S-xFRU.js → index-BCBK78hw.js} +5 -5
- package/dist/web-app/index.html +1 -1
- package/dist/worklog.d.ts +7 -1
- package/dist/worklog.js +191 -39
- package/package.json +1 -3
- package/dist/owner-channel/ours-client.d.ts +0 -141
- package/dist/owner-channel/ours-client.js +0 -225
package/dist/doctor.d.ts
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
|
-
import { type AttachOursClientOptions, type OursClient } from '@ours.network/sdk/client';
|
|
2
1
|
import { type Exec } from './exec.js';
|
|
3
2
|
import type { YamlMode } from './config-yaml.js';
|
|
4
3
|
import { type FetchLike } from './monitor.js';
|
|
5
4
|
import type { PrereqReport } from './harness/types.js';
|
|
6
|
-
type DoctorDaemonClient = Pick<OursClient, 'version'>;
|
|
7
|
-
type AttachDoctorDaemon = (options: AttachOursClientOptions) => Promise<DoctorDaemonClient>;
|
|
8
5
|
/** Host-level + per-harness prerequisite report with actionable messages. */
|
|
9
6
|
export declare function doctor(opts?: {
|
|
10
7
|
harness?: string;
|
|
@@ -15,5 +12,4 @@ export declare function doctor(opts?: {
|
|
|
15
12
|
path?: string;
|
|
16
13
|
argv1?: string;
|
|
17
14
|
};
|
|
18
|
-
}, exec?: Exec, platform?: NodeJS.Platform, fetchImpl?: FetchLike
|
|
19
|
-
export {};
|
|
15
|
+
}, exec?: Exec, platform?: NodeJS.Platform, fetchImpl?: FetchLike): Promise<PrereqReport>;
|
package/dist/doctor.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { userInfo } from 'node:os';
|
|
2
2
|
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
-
import { attachOursClient, } from '@ours.network/sdk/client';
|
|
4
3
|
import { realExec } from './exec.js';
|
|
5
4
|
import { isolationContextFor, loadConfig } from './config.js';
|
|
6
5
|
import { getAdapter, productionAdapters } from './harness/registry.js';
|
|
@@ -77,7 +76,7 @@ function installChecks(scan) {
|
|
|
77
76
|
];
|
|
78
77
|
}
|
|
79
78
|
/** Host-level + per-harness prerequisite report with actionable messages. */
|
|
80
|
-
export async function doctor(opts = {}, exec = realExec, platform = process.platform, fetchImpl = (u, i) => globalThis.fetch(u, i)
|
|
79
|
+
export async function doctor(opts = {}, exec = realExec, platform = process.platform, fetchImpl = (u, i) => globalThis.fetch(u, i)) {
|
|
81
80
|
const checks = [];
|
|
82
81
|
// First: which artifact is producing this report. Everything below is only as
|
|
83
82
|
// trustworthy as the answer, and a second install with the same semver and
|
|
@@ -113,22 +112,16 @@ export async function doctor(opts = {}, exec = realExec, platform = process.plat
|
|
|
113
112
|
detail: tmux.code === 0 ? tmux.stdout.trim() : 'not found — apt install tmux / brew install tmux',
|
|
114
113
|
});
|
|
115
114
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const
|
|
123
|
-
if (info.name !== 'ours' || typeof info.version !== 'string')
|
|
124
|
-
throw new Error('the selected endpoint did not return a valid ours daemon identity');
|
|
125
|
-
checks.push({ name: 'ours daemon', ok: true, detail: `running (${info.version})` });
|
|
126
|
-
}
|
|
127
|
-
catch (error) {
|
|
115
|
+
const mcp = await exec('ours-mcp', ['--version']);
|
|
116
|
+
checks.push({
|
|
117
|
+
name: 'ours-mcp', ok: mcp.code === 0,
|
|
118
|
+
detail: mcp.code === 0 ? mcp.stdout.trim() : 'not found — npm i -g @ours.network/mcp',
|
|
119
|
+
});
|
|
120
|
+
if (mcp.code === 0) {
|
|
121
|
+
const st = await exec('ours-mcp', ['status']);
|
|
128
122
|
checks.push({
|
|
129
|
-
name: 'ours daemon', ok:
|
|
130
|
-
detail:
|
|
131
|
-
+ `[${error?.message ?? String(error)}]`,
|
|
123
|
+
name: 'ours-mcp daemon', ok: st.code === 0,
|
|
124
|
+
detail: st.code === 0 ? 'running' : 'not running — start it with: ours-mcp start',
|
|
132
125
|
});
|
|
133
126
|
}
|
|
134
127
|
if (platform === 'linux') {
|
|
@@ -295,7 +288,7 @@ export async function doctor(opts = {}, exec = realExec, platform = process.plat
|
|
|
295
288
|
}
|
|
296
289
|
catch (e) {
|
|
297
290
|
detail = `unreachable on :${endpoint.port} — monitored roles run degraded until it is up ` +
|
|
298
|
-
`(start it: ours
|
|
291
|
+
`(start it: ours-mcp start) [${e?.message ?? e}]`;
|
|
299
292
|
}
|
|
300
293
|
checks.push({ name: checkName, ok, detail });
|
|
301
294
|
}
|
package/dist/fleet-proxy.d.ts
CHANGED
|
@@ -12,6 +12,11 @@ export interface ManagedFleetSpawnResult {
|
|
|
12
12
|
session: 'tmux' | 'acp';
|
|
13
13
|
model?: string;
|
|
14
14
|
monitor: Pick<MonitorConfig, 'mode' | 'interrupt'>;
|
|
15
|
+
/** Adapter-resolved portable policy and exact native runtime mode. */
|
|
16
|
+
permissionMode?: {
|
|
17
|
+
fleetMode: 'ask' | 'auto' | 'allow';
|
|
18
|
+
nativeMode: string;
|
|
19
|
+
};
|
|
15
20
|
inherited: string[];
|
|
16
21
|
creationActionId: string;
|
|
17
22
|
}
|
|
@@ -1,18 +1,23 @@
|
|
|
1
1
|
import { createRequire } from 'node:module';
|
|
2
|
-
import {
|
|
3
|
-
import { dirname, resolve } from 'node:path';
|
|
2
|
+
import { readFileSync, realpathSync, statSync } from 'node:fs';
|
|
3
|
+
import { dirname, isAbsolute, relative, resolve, sep } from 'node:path';
|
|
4
4
|
const require = createRequire(import.meta.url);
|
|
5
5
|
export function resolveBundledAcpAgent(packageName, binName, fallbackCommand) {
|
|
6
6
|
try {
|
|
7
7
|
const manifestPath = require.resolve(`${packageName}/package.json`);
|
|
8
8
|
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
9
|
-
const
|
|
9
|
+
const declaredEntrypoint = typeof manifest.bin === 'string'
|
|
10
10
|
? manifest.bin
|
|
11
11
|
: manifest.bin?.[binName];
|
|
12
|
-
if (!
|
|
12
|
+
if (!declaredEntrypoint)
|
|
13
13
|
return { argv: [fallbackCommand], bundled: false };
|
|
14
|
-
const
|
|
15
|
-
|
|
14
|
+
const packageRoot = realpathSync(dirname(manifestPath));
|
|
15
|
+
const entrypoint = realpathSync(resolve(packageRoot, declaredEntrypoint));
|
|
16
|
+
const entrypointFromRoot = relative(packageRoot, entrypoint);
|
|
17
|
+
if (entrypointFromRoot === '..'
|
|
18
|
+
|| entrypointFromRoot.startsWith(`..${sep}`)
|
|
19
|
+
|| isAbsolute(entrypointFromRoot)
|
|
20
|
+
|| !statSync(entrypoint).isFile())
|
|
16
21
|
return { argv: [fallbackCommand], bundled: false };
|
|
17
22
|
return {
|
|
18
23
|
argv: [process.execPath, entrypoint], bundled: true, manifestPath,
|
|
@@ -6,8 +6,99 @@ import { registerAdapter } from './registry.js';
|
|
|
6
6
|
import { replaceFileAtomically, withFileLock } from '../atomic-file.js';
|
|
7
7
|
import { harnessRuntimeDir } from '../isolation/policy.js';
|
|
8
8
|
import { bundledAcpAgent } from './acp-agent.js';
|
|
9
|
-
|
|
9
|
+
import { restoreLockedHarnessMarketplace } from '../harness-plugins.js';
|
|
10
|
+
const OPTION_KEYS = [
|
|
11
|
+
'plugins', 'mem_palace', 'mem_palace_midsession_autosave', 'permission_mode', 'effort',
|
|
12
|
+
'mcp_servers', 'mcp_servers_only',
|
|
13
|
+
];
|
|
10
14
|
const EFFORT_LEVELS = ['low', 'medium', 'high', 'xhigh', 'max'];
|
|
15
|
+
/** `.mcp.json` server types. Absent means stdio, as the file format has it. */
|
|
16
|
+
const MCP_SERVER_TYPES = ['stdio', 'http', 'sse'];
|
|
17
|
+
/** A role that names its own ACP command runs a process fleet did not choose. */
|
|
18
|
+
const customAcpCommand = (role) => role.session === 'acp' && role.session_options?.acp?.command != null;
|
|
19
|
+
/**
|
|
20
|
+
* Does this server set include the ours connector?
|
|
21
|
+
*
|
|
22
|
+
* Load-bearing, and the reason it is a check rather than a doc line:
|
|
23
|
+
* `mcp_servers_only` maps to `--strict-mcp-config`, which ignores EVERY other MCP
|
|
24
|
+
* configuration — project `.mcp.json`, user settings, and **plugins**. On a
|
|
25
|
+
* normal install the ours connector arrives as a plugin
|
|
26
|
+
* (`~/.claude/plugins/.../plugin.json` declares `ours`), so a role that turns
|
|
27
|
+
* strict mode on without re-declaring it loses `send_message` and `get_messages`
|
|
28
|
+
* and cannot report that it has: a mute agent looks exactly like a quiet one.
|
|
29
|
+
*
|
|
30
|
+
* Matched on the command line rather than the server's NAME, because the name is
|
|
31
|
+
* the operator's to choose and would make this trivially satisfiable by writing
|
|
32
|
+
* `ours:` above the wrong command.
|
|
33
|
+
*/
|
|
34
|
+
const declaresOursConnector = (servers) => Object.values(servers).some(s => [s.command ?? '', ...(s.args ?? [])].some(part => /(^|[/\\])ours-mcp($|\s)|@ours\.network[/\\]mcp/.test(part)));
|
|
35
|
+
/** Shape-check `harness_options.mcp_servers` against `.mcp.json`'s own rules. */
|
|
36
|
+
function validateMcpServers(servers) {
|
|
37
|
+
if (servers == null)
|
|
38
|
+
return [];
|
|
39
|
+
const at = (k = '') => ({ path: `harness_options.mcp_servers${k}` });
|
|
40
|
+
if (typeof servers !== 'object' || Array.isArray(servers))
|
|
41
|
+
return [{ ...at(), message: 'must be a map of server name to server definition' }];
|
|
42
|
+
const entries = Object.entries(servers);
|
|
43
|
+
if (!entries.length)
|
|
44
|
+
return [{ ...at(), message: 'must declare at least one server, or be omitted' }];
|
|
45
|
+
const errors = [];
|
|
46
|
+
for (const [name, raw] of entries) {
|
|
47
|
+
const p = `.${name}`;
|
|
48
|
+
if (!/^[A-Za-z0-9_-]+$/.test(name)) {
|
|
49
|
+
errors.push({ ...at(p), message: 'server name must be [A-Za-z0-9_-]' });
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (raw == null || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
53
|
+
errors.push({ ...at(p), message: 'must be a map' });
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const s = raw;
|
|
57
|
+
if (s.type != null && !MCP_SERVER_TYPES.includes(s.type))
|
|
58
|
+
errors.push({ ...at(`${p}.type`), message: `must be one of: ${MCP_SERVER_TYPES.join(', ')}` });
|
|
59
|
+
const remote = s.type === 'http' || s.type === 'sse';
|
|
60
|
+
if (remote) {
|
|
61
|
+
if (typeof s.url !== 'string' || !s.url.trim())
|
|
62
|
+
errors.push({ ...at(`${p}.url`), message: `must be a non-empty URL for a ${s.type} server` });
|
|
63
|
+
if (s.command != null)
|
|
64
|
+
errors.push({ ...at(`${p}.command`), message: `must not be set for a ${s.type} server` });
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
if (typeof s.command !== 'string' || !s.command.trim())
|
|
68
|
+
errors.push({ ...at(`${p}.command`), message: 'must be a non-empty command for a stdio server' });
|
|
69
|
+
if (s.args != null && (!Array.isArray(s.args) || s.args.some(a => typeof a !== 'string')))
|
|
70
|
+
errors.push({ ...at(`${p}.args`), message: 'must be an array of strings' });
|
|
71
|
+
if (s.url != null)
|
|
72
|
+
errors.push({ ...at(`${p}.url`), message: 'must not be set for a stdio server' });
|
|
73
|
+
}
|
|
74
|
+
for (const key of ['env', 'headers']) {
|
|
75
|
+
const v = s[key];
|
|
76
|
+
if (v == null)
|
|
77
|
+
continue;
|
|
78
|
+
if (typeof v !== 'object' || Array.isArray(v)
|
|
79
|
+
|| Object.values(v).some(x => typeof x !== 'string'))
|
|
80
|
+
errors.push({ ...at(`${p}.${key}`), message: 'must be a map of string to string' });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return errors;
|
|
84
|
+
}
|
|
85
|
+
/** `harness_options.mcp_servers` in ACP's `session/new` array shape. */
|
|
86
|
+
function acpMcpServersFor(servers) {
|
|
87
|
+
if (!servers)
|
|
88
|
+
return [];
|
|
89
|
+
// `env` and `headers` are REQUIRED arrays in the protocol, so they are always
|
|
90
|
+
// sent — empty when the role declared none.
|
|
91
|
+
const pairs = (r) => Object.entries(r ?? {}).map(([name, value]) => ({ name, value }));
|
|
92
|
+
return Object.entries(servers).map(([name, s]) => {
|
|
93
|
+
if (s.type === 'http' || s.type === 'sse')
|
|
94
|
+
return { name, type: s.type, url: s.url, headers: pairs(s.headers) };
|
|
95
|
+
// Stdio carries NO `type` field: ACP's stdio variant is the one without it,
|
|
96
|
+
// and the bundled agent keys on exactly that (claude-agent-acp
|
|
97
|
+
// acp-agent.js:4058, `!("type" in server)`), so sending `type: 'stdio'`
|
|
98
|
+
// would drop the server on the floor.
|
|
99
|
+
return { name, command: s.command, args: s.args ?? [], env: pairs(s.env) };
|
|
100
|
+
});
|
|
101
|
+
}
|
|
11
102
|
/** Claude Code's accepted --permission-mode values. */
|
|
12
103
|
const PERMISSION_MODES = ['default', 'acceptEdits', 'plan', 'dontAsk', 'bypassPermissions'];
|
|
13
104
|
/**
|
|
@@ -127,11 +218,8 @@ export async function pretrust(dir, deps = {}) {
|
|
|
127
218
|
* it must be the Monitor TOOL, not a background Bash task (which never wakes the
|
|
128
219
|
* agent on output → an armed-looking but deaf monitor).
|
|
129
220
|
*/
|
|
130
|
-
const shellQuote = (value) => `'${value.replace(/'/g, `'"'"'`)}'`;
|
|
131
|
-
const watchCommand = (id) => 'ours api watch-notifications --input '
|
|
132
|
-
+ shellQuote(JSON.stringify({ identity: id, since: 'tip' })) + ' --json';
|
|
133
221
|
const armMonitor = (id) => 'arm a **persistent Monitor** (the Monitor TOOL — NOT a background Bash command; a ' +
|
|
134
|
-
`background Bash task never wakes you on output) running
|
|
222
|
+
`background Bash task never wakes you on output) running \`ours-mcp watch "${id}"\` ` +
|
|
135
223
|
'so inbound ours mail wakes you';
|
|
136
224
|
export function makeClaudeCodeAdapter(exec = realExec) {
|
|
137
225
|
return {
|
|
@@ -149,7 +237,7 @@ export function makeClaudeCodeAdapter(exec = realExec) {
|
|
|
149
237
|
}],
|
|
150
238
|
};
|
|
151
239
|
},
|
|
152
|
-
validateOptions(opts) {
|
|
240
|
+
validateOptions(opts, role) {
|
|
153
241
|
if (opts == null)
|
|
154
242
|
return [];
|
|
155
243
|
if (typeof opts !== 'object' || Array.isArray(opts))
|
|
@@ -157,12 +245,50 @@ export function makeClaudeCodeAdapter(exec = realExec) {
|
|
|
157
245
|
const errors = Object.keys(opts)
|
|
158
246
|
.filter(k => !OPTION_KEYS.includes(k))
|
|
159
247
|
.map(k => ({ path: `harness_options.${k}`, message: `unknown option; allowed: ${OPTION_KEYS.join(', ')}` }));
|
|
160
|
-
const
|
|
248
|
+
const o = opts;
|
|
249
|
+
const effort = o.effort;
|
|
161
250
|
if (effort != null && !EFFORT_LEVELS.includes(effort))
|
|
162
251
|
errors.push({ path: 'harness_options.effort', message: `must be one of: ${EFFORT_LEVELS.join(', ')}` });
|
|
252
|
+
if (o.mcp_servers_only != null && typeof o.mcp_servers_only !== 'boolean')
|
|
253
|
+
errors.push({ path: 'harness_options.mcp_servers_only', message: 'must be a boolean' });
|
|
254
|
+
errors.push(...validateMcpServers(o.mcp_servers));
|
|
255
|
+
if (o.mcp_servers_only === true && !o.mcp_servers)
|
|
256
|
+
errors.push({
|
|
257
|
+
path: 'harness_options.mcp_servers_only',
|
|
258
|
+
message: 'requires harness_options.mcp_servers; on its own it would leave the role with no MCP servers at all',
|
|
259
|
+
});
|
|
260
|
+
// The muteness gate. Only when the declared set is otherwise well-formed —
|
|
261
|
+
// a shape error already told the operator to look here.
|
|
262
|
+
if (o.mcp_servers_only === true && o.mcp_servers && errors.length === 0
|
|
263
|
+
&& !declaresOursConnector(o.mcp_servers))
|
|
264
|
+
errors.push({
|
|
265
|
+
path: 'harness_options.mcp_servers',
|
|
266
|
+
message: 'mcp_servers_only ignores every other MCP configuration, INCLUDING plugins — and the ours '
|
|
267
|
+
+ 'connector is normally a plugin, so this role would have no send_message or get_messages and no way '
|
|
268
|
+
+ 'to report that. Declare it explicitly, e.g. ours: { command: ours-mcp, args: [proxy] }',
|
|
269
|
+
});
|
|
270
|
+
// Session-aware refusals. Both options reach an ACP session through the
|
|
271
|
+
// bundled agent's `_meta` vocabulary, so a role that launches a DIFFERENT
|
|
272
|
+
// ACP agent cannot be promised either one. Refuse rather than send it and
|
|
273
|
+
// hope: silently dropping the config is the defect being fixed here.
|
|
274
|
+
if (role && customAcpCommand(role)) {
|
|
275
|
+
for (const key of ['plugins', 'mcp_servers', 'mcp_servers_only']) {
|
|
276
|
+
if (o[key] == null)
|
|
277
|
+
continue;
|
|
278
|
+
errors.push({
|
|
279
|
+
path: `harness_options.${key}`,
|
|
280
|
+
message: 'cannot be honoured with session_options.acp.command: it is delivered through the bundled '
|
|
281
|
+
+ 'Claude ACP agent\'s _meta vocabulary, which another agent has no reason to read. Drop the '
|
|
282
|
+
+ 'custom ACP command, or drop this option',
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
}
|
|
163
286
|
return errors;
|
|
164
287
|
},
|
|
165
288
|
async prepareSession(role, dirs) {
|
|
289
|
+
// Re-materialize only from the persisted exact lock. Ordinary launches
|
|
290
|
+
// never resolve npm tags or run an installer.
|
|
291
|
+
restoreLockedHarnessMarketplace('claude-code', role.harnessPluginChannel);
|
|
166
292
|
// Pre-trust stays a HOST-side step: inside the sandbox ~/.claude.json is
|
|
167
293
|
// read-only, and it is the fleet's job to trust the role's dirs, not the
|
|
168
294
|
// agent's (5.1, 6.1).
|
|
@@ -178,6 +304,15 @@ export function makeClaudeCodeAdapter(exec = realExec) {
|
|
|
178
304
|
CLAUDE_AUTOCOMPACT_PCT_OVERRIDE: String(autocompactPct(role)),
|
|
179
305
|
MEMPALACE_HOOKS_AUTO_SAVE: 'false',
|
|
180
306
|
MEMPALACE_MIDSESSION_AUTOSAVE: o.mem_palace_midsession_autosave ? 'true' : 'false',
|
|
307
|
+
// The role's identity, for the ours connector to bind at startup instead of
|
|
308
|
+
// the briefing telling the MODEL to call choose_identity. Both launches
|
|
309
|
+
// return `prep.env`, so this one line covers tmux and ACP alike.
|
|
310
|
+
//
|
|
311
|
+
// The bind the connector performs is PLAIN and fail-closed: it can never
|
|
312
|
+
// evict a live session, and a role whose identity does not exist yet simply
|
|
313
|
+
// boots unbound and falls through to the briefing's create-if-missing step.
|
|
314
|
+
// Nothing here may ever grow a force flag.
|
|
315
|
+
OURS_BIND_IDENTITY: role.identity,
|
|
181
316
|
};
|
|
182
317
|
if (!memPalace)
|
|
183
318
|
env.MEMPALACE_DISABLED = 'true';
|
|
@@ -188,12 +323,29 @@ export function makeClaudeCodeAdapter(exec = realExec) {
|
|
|
188
323
|
if (role.isolation)
|
|
189
324
|
mkdirSync(harnessRuntimeDir(dirs.stateDir, 'claude'), { recursive: true });
|
|
190
325
|
const argv = [];
|
|
326
|
+
let settingsOverlay;
|
|
191
327
|
if (Object.keys(enabledPlugins).length) {
|
|
192
|
-
|
|
193
|
-
writeFileSync(
|
|
194
|
-
argv.push('--settings',
|
|
328
|
+
settingsOverlay = join(dirs.stateDir, '.settings-overlay.json');
|
|
329
|
+
writeFileSync(settingsOverlay, JSON.stringify({ enabledPlugins }, null, 2));
|
|
330
|
+
argv.push('--settings', settingsOverlay);
|
|
195
331
|
}
|
|
196
|
-
|
|
332
|
+
// `harness_options.mcp_servers` — the tmux delivery. `--mcp-config` ADDS the
|
|
333
|
+
// file's servers; `--strict-mcp-config` is what makes the set exclusive, and
|
|
334
|
+
// it is opt-in per role because it drops everything else the user has,
|
|
335
|
+
// plugins included (see `declaresOursConnector`).
|
|
336
|
+
let mcpConfigFile;
|
|
337
|
+
if (o.mcp_servers) {
|
|
338
|
+
mcpConfigFile = join(dirs.stateDir, '.mcp-config.json');
|
|
339
|
+
writeFileSync(mcpConfigFile, JSON.stringify({ mcpServers: o.mcp_servers }, null, 2), { mode: 0o600 });
|
|
340
|
+
argv.push('--mcp-config', mcpConfigFile);
|
|
341
|
+
if (o.mcp_servers_only === true)
|
|
342
|
+
argv.push('--strict-mcp-config');
|
|
343
|
+
}
|
|
344
|
+
return {
|
|
345
|
+
argv, env,
|
|
346
|
+
...(settingsOverlay ? { settingsOverlay } : {}),
|
|
347
|
+
...(mcpConfigFile ? { mcpConfigFile } : {}),
|
|
348
|
+
};
|
|
197
349
|
},
|
|
198
350
|
buildLaunch(role, mode, s, prep) {
|
|
199
351
|
const stateDir = roleStateDir(role);
|
|
@@ -223,6 +375,46 @@ export function makeClaudeCodeAdapter(exec = realExec) {
|
|
|
223
375
|
acpPermissionModeId(role) {
|
|
224
376
|
return permissionMode(role);
|
|
225
377
|
},
|
|
378
|
+
/**
|
|
379
|
+
* Deliver, over ACP, the two things the tmux launch delivers as flags.
|
|
380
|
+
*
|
|
381
|
+
* `buildAcpLaunch` builds its own argv and cannot carry `prep.argv`: the
|
|
382
|
+
* process it launches is the ACP agent, not `claude`, and it takes none of
|
|
383
|
+
* claude's flags. That is why `harness_options.plugins` did nothing at all on
|
|
384
|
+
* an ACP role — the overlay was written and then dropped, and the mem-palace
|
|
385
|
+
* toggle rode `prep.env` and survived, so the failure was silent AND
|
|
386
|
+
* selective.
|
|
387
|
+
*
|
|
388
|
+
* `_meta.claudeCode.options` is the bundled agent's own passthrough into the
|
|
389
|
+
* Claude Agent SDK (@agentclientprotocol/claude-agent-acp, acp-agent.js:4092
|
|
390
|
+
* → the `options` object at :4144). `settings` takes the same overlay path
|
|
391
|
+
* `--settings` takes; `strictMcpConfig` is the SDK's spelling of
|
|
392
|
+
* `--strict-mcp-config`. Both are spread BEFORE the fields the agent forces,
|
|
393
|
+
* so neither is overwritten.
|
|
394
|
+
*
|
|
395
|
+
* ⚠ RETURNS NOTHING FOR A ROLE THAT NAMES ITS OWN ACP COMMAND. That process
|
|
396
|
+
* is not the bundled agent and has no reason to read this vocabulary; sending
|
|
397
|
+
* it anyway would be the silent drop again, one level down. `validateOptions`
|
|
398
|
+
* refuses those roles instead.
|
|
399
|
+
*/
|
|
400
|
+
acpSessionMeta(role, prep) {
|
|
401
|
+
if (customAcpCommand(role))
|
|
402
|
+
return undefined;
|
|
403
|
+
const options = {};
|
|
404
|
+
if (prep.settingsOverlay)
|
|
405
|
+
options.settings = prep.settingsOverlay;
|
|
406
|
+
if (role.harness_options?.mcp_servers_only === true)
|
|
407
|
+
options.strictMcpConfig = true;
|
|
408
|
+
return Object.keys(options).length ? { claudeCode: { options } } : undefined;
|
|
409
|
+
},
|
|
410
|
+
/**
|
|
411
|
+
* The declared servers, in ACP's array shape. Sent on `session/new` and on
|
|
412
|
+
* resume/load, because the SDK builds its server set once per session and a
|
|
413
|
+
* resumed session that dropped them would quietly lose its tools.
|
|
414
|
+
*/
|
|
415
|
+
acpMcpServers(role) {
|
|
416
|
+
return acpMcpServersFor(role.harness_options?.mcp_servers);
|
|
417
|
+
},
|
|
226
418
|
isolationPaths(_role, _dirs) {
|
|
227
419
|
const claudeHome = join(home(), '.claude');
|
|
228
420
|
return {
|
|
@@ -285,6 +477,7 @@ export function makeClaudeCodeAdapter(exec = realExec) {
|
|
|
285
477
|
currentIdentityTool: 'current_identity',
|
|
286
478
|
sendTool: 'send_message',
|
|
287
479
|
getMessagesTool: 'get_messages',
|
|
480
|
+
watchCommand: id => `ours-mcp watch "${id}"`,
|
|
288
481
|
monitorInstruction: id => {
|
|
289
482
|
const m = armMonitor(id);
|
|
290
483
|
return `${m.charAt(0).toUpperCase()}${m.slice(1)}.`;
|
package/dist/harness/codex.d.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { type Exec } from '../exec.js';
|
|
2
|
-
import type { HarnessAdapter, UnattendedCapability } from './types.js';
|
|
2
|
+
import type { AcpLaunch, HarnessAdapter, UnattendedCapability } from './types.js';
|
|
3
|
+
import { type AcpAgentResolution } from './acp-agent.js';
|
|
3
4
|
/**
|
|
4
5
|
* What an unattended role can actually do under Codex's native settings.
|
|
5
6
|
* `on-request` and `untrusted` stop to ask, and with no console attached that
|
|
6
7
|
* request is refused rather than answered — so the role can only read.
|
|
7
8
|
*/
|
|
8
9
|
export declare function codexCapabilities(approval: string, sandbox: string): UnattendedCapability[];
|
|
10
|
+
/** Bind launch argv and metadata provenance to one already-completed resolution. */
|
|
11
|
+
export declare function codexAcpLaunchForResolution(resolution: AcpAgentResolution): Pick<AcpLaunch, 'argv' | 'permissionMetadataSource'>;
|
|
9
12
|
export declare function makeCodexAdapter(exec?: Exec): HarnessAdapter;
|
|
10
13
|
export declare const codexAdapter: HarnessAdapter;
|
package/dist/harness/codex.js
CHANGED
|
@@ -5,7 +5,8 @@ import { agentDir, home } from '../paths.js';
|
|
|
5
5
|
import { realExec } from '../exec.js';
|
|
6
6
|
import { registerAdapter } from './registry.js';
|
|
7
7
|
import { harnessRuntimeDir } from '../isolation/policy.js';
|
|
8
|
-
import {
|
|
8
|
+
import { resolveBundledAcpAgent, } from './acp-agent.js';
|
|
9
|
+
import { restoreLockedHarnessMarketplace } from '../harness-plugins.js';
|
|
9
10
|
const OPTION_KEYS = [
|
|
10
11
|
'launcher', 'sandbox', 'approval', 'permission_mode', 'search', 'profile', 'config', 'add_dirs',
|
|
11
12
|
'monitor',
|
|
@@ -51,9 +52,7 @@ function sandboxMode(role) {
|
|
|
51
52
|
throw new Error(`invalid harness_options.sandbox "${s}"; allowed: ${SANDBOX_MODES.join(', ')}`);
|
|
52
53
|
return s;
|
|
53
54
|
}
|
|
54
|
-
|
|
55
|
-
function acpAgentMode(role) {
|
|
56
|
-
const sandbox = sandboxMode(role);
|
|
55
|
+
function modeForSandbox(sandbox) {
|
|
57
56
|
if (sandbox === 'read-only')
|
|
58
57
|
return 'read-only';
|
|
59
58
|
if (sandbox === 'workspace-write')
|
|
@@ -62,6 +61,26 @@ function acpAgentMode(role) {
|
|
|
62
61
|
return 'agent-full-access';
|
|
63
62
|
return undefined;
|
|
64
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* Resolve the coupled Codex ACP mode.
|
|
66
|
+
*
|
|
67
|
+
* The portable approval contract owns the default mode selection: `allow`
|
|
68
|
+
* means the adapter's fully non-interactive yolo preset and `auto` means its
|
|
69
|
+
* ordinary agent preset. This intentionally means that Codex ACP cannot retain
|
|
70
|
+
* an independent neutral filesystem posture for those two modes. An explicit
|
|
71
|
+
* native sandbox remains authoritative and selects its corresponding preset.
|
|
72
|
+
*/
|
|
73
|
+
function acpAgentMode(role) {
|
|
74
|
+
const explicitSandbox = role.harness_options?.sandbox;
|
|
75
|
+
if (explicitSandbox != null)
|
|
76
|
+
return modeForSandbox(sandboxMode(role));
|
|
77
|
+
if (role.permissions?.approval === 'allow')
|
|
78
|
+
return 'agent-full-access';
|
|
79
|
+
if (role.permissions?.approval === 'auto')
|
|
80
|
+
return 'agent';
|
|
81
|
+
const sandbox = sandboxMode(role);
|
|
82
|
+
return modeForSandbox(sandbox);
|
|
83
|
+
}
|
|
65
84
|
function acpModePermissions(mode) {
|
|
66
85
|
if (mode === 'read-only')
|
|
67
86
|
return { approval: 'on-request', sandbox: 'read-only' };
|
|
@@ -69,6 +88,10 @@ function acpModePermissions(mode) {
|
|
|
69
88
|
return { approval: 'never', sandbox: 'danger-full-access' };
|
|
70
89
|
return { approval: 'on-request', sandbox: 'workspace-write' };
|
|
71
90
|
}
|
|
91
|
+
/** The sandbox Codex will actually receive from the selected coupled ACP mode. */
|
|
92
|
+
function acpRuntimeSandbox(role) {
|
|
93
|
+
return acpModePermissions(acpAgentMode(role)).sandbox;
|
|
94
|
+
}
|
|
72
95
|
function fleetModeForApproval(nativeMode) {
|
|
73
96
|
if (nativeMode === 'never')
|
|
74
97
|
return 'allow';
|
|
@@ -102,6 +125,18 @@ function launcherMode(role) {
|
|
|
102
125
|
function bundledCodexAcp() {
|
|
103
126
|
return resolveBundledAcpAgent(CODEX_ACP_PACKAGE, 'codex-acp', 'codex-acp');
|
|
104
127
|
}
|
|
128
|
+
/** Bind launch argv and metadata provenance to one already-completed resolution. */
|
|
129
|
+
export function codexAcpLaunchForResolution(resolution) {
|
|
130
|
+
const permissionMetadataSource = resolution.bundled
|
|
131
|
+
&& resolution.version === BUNDLED_CODEX_ACP_VERSION
|
|
132
|
+
&& resolution.manifestPath !== undefined
|
|
133
|
+
? 'codex-acp'
|
|
134
|
+
: undefined;
|
|
135
|
+
return {
|
|
136
|
+
argv: [...resolution.argv],
|
|
137
|
+
...(permissionMetadataSource ? { permissionMetadataSource } : {}),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
105
140
|
function canOverrideBundledAcpApproval() {
|
|
106
141
|
const resolution = bundledCodexAcp();
|
|
107
142
|
return resolution.bundled && resolution.version === BUNDLED_CODEX_ACP_VERSION
|
|
@@ -147,7 +182,7 @@ function codexAcpEnvironment(role, dirs) {
|
|
|
147
182
|
return {
|
|
148
183
|
CODEX_PATH: command,
|
|
149
184
|
[CODEX_PROXY_APPROVAL_ENV]: approvalPolicy(role) ?? 'on-request',
|
|
150
|
-
[CODEX_PROXY_SANDBOX_ENV]:
|
|
185
|
+
[CODEX_PROXY_SANDBOX_ENV]: acpRuntimeSandbox(role),
|
|
151
186
|
[CODEX_PROXY_MANIFEST_ENV]: resolution.manifestPath,
|
|
152
187
|
...(process.env.CODEX_PATH ? { [CODEX_PROXY_REAL_PATH_ENV]: process.env.CODEX_PATH } : {}),
|
|
153
188
|
};
|
|
@@ -275,6 +310,9 @@ export function makeCodexAdapter(exec = realExec) {
|
|
|
275
310
|
return errs;
|
|
276
311
|
},
|
|
277
312
|
async prepareSession(role, dirs) {
|
|
313
|
+
// Re-materialize only from the persisted exact lock. Ordinary launches
|
|
314
|
+
// never resolve npm tags or run an installer.
|
|
315
|
+
restoreLockedHarnessMarketplace('codex', role.harnessPluginChannel);
|
|
278
316
|
// Per-role harness runtime home (5.1); harmless for un-isolated roles.
|
|
279
317
|
// Only a role that declares `isolation:` gets a sandbox, and only a
|
|
280
318
|
// sandbox needs this directory to exist before entry.
|
|
@@ -285,7 +323,16 @@ export function makeCodexAdapter(exec = realExec) {
|
|
|
285
323
|
if (requested === 'ours-codex' && !hasOursCodex)
|
|
286
324
|
throw new Error('harness_options.launcher is ours-codex, but ours-codex is not on PATH; install @ours.network/codex or use launcher: auto');
|
|
287
325
|
const command = requested === 'codex' ? 'codex' : hasOursCodex ? 'ours-codex' : 'codex';
|
|
288
|
-
return {
|
|
326
|
+
return {
|
|
327
|
+
argv: [],
|
|
328
|
+
// OURS_BIND_IDENTITY is the connector's startup bind seed — see the note in
|
|
329
|
+
// claude-code.ts's prepareSession. It belongs on EVERY harness that runs a
|
|
330
|
+
// role with an ours identity, not just claude-code: a seed that works on one
|
|
331
|
+
// harness and silently does nothing on the other is the same class of defect
|
|
332
|
+
// as a config key that only works on one session type.
|
|
333
|
+
env: { OURS_BIND_IDENTITY: role.identity, ...codexAcpEnvironment(role, dirs) },
|
|
334
|
+
command,
|
|
335
|
+
};
|
|
289
336
|
},
|
|
290
337
|
buildLaunch(role, mode, _s, prep) {
|
|
291
338
|
const stateDir = roleStateDir(role);
|
|
@@ -299,17 +346,30 @@ export function makeCodexAdapter(exec = realExec) {
|
|
|
299
346
|
},
|
|
300
347
|
buildAcpLaunch(role, prep) {
|
|
301
348
|
const configured = role.session_options?.acp?.command;
|
|
349
|
+
// Resolve once: both argv and permission-metadata provenance must describe
|
|
350
|
+
// the same artifact. A bare PATH fallback is launchable for compatibility,
|
|
351
|
+
// but is never authenticated for protected-MCP auto-approval.
|
|
352
|
+
const resolved = configured == null
|
|
353
|
+
? codexAcpLaunchForResolution(bundledCodexAcp())
|
|
354
|
+
: undefined;
|
|
302
355
|
const argv = Array.isArray(configured)
|
|
303
356
|
? [...configured]
|
|
304
357
|
: typeof configured === 'string'
|
|
305
358
|
? ['sh', '-c', configured]
|
|
306
|
-
:
|
|
359
|
+
: resolved.argv;
|
|
307
360
|
const initialMode = acpAgentMode(role);
|
|
308
361
|
return {
|
|
309
362
|
argv,
|
|
310
363
|
env: initialMode ? { ...prep.env, INITIAL_AGENT_MODE: initialMode } : prep.env,
|
|
364
|
+
...(resolved?.permissionMetadataSource
|
|
365
|
+
? { permissionMetadataSource: resolved.permissionMetadataSource } : {}),
|
|
311
366
|
};
|
|
312
367
|
},
|
|
368
|
+
// INITIAL_AGENT_MODE covers session/new in codex-acp; session/set_mode
|
|
369
|
+
// keeps resumed/loaded sessions and live status on the identical mode.
|
|
370
|
+
acpPermissionModeId(role) {
|
|
371
|
+
return acpAgentMode(role);
|
|
372
|
+
},
|
|
313
373
|
isolationPaths(role, _dirs) {
|
|
314
374
|
const codexHome = join(home(), '.codex');
|
|
315
375
|
const profile = role.harness_options?.profile;
|
|
@@ -362,7 +422,9 @@ export function makeCodexAdapter(exec = realExec) {
|
|
|
362
422
|
const mode = acpAgentMode(role) ?? 'agent';
|
|
363
423
|
const configured = role.session_options?.acp?.command;
|
|
364
424
|
const overrideAvailable = configured == null && canOverrideBundledAcpApproval();
|
|
365
|
-
const actual = overrideAvailable
|
|
425
|
+
const actual = overrideAvailable
|
|
426
|
+
? { approval, sandbox: acpRuntimeSandbox(role) }
|
|
427
|
+
: acpModePermissions(mode);
|
|
366
428
|
const exact = actual.approval === approval && actual.sandbox === sandbox;
|
|
367
429
|
return {
|
|
368
430
|
...translated,
|
|
@@ -372,10 +434,9 @@ export function makeCodexAdapter(exec = realExec) {
|
|
|
372
434
|
? `custom ACP command cannot be verified against approval=${approval} sandbox=${sandbox}; `
|
|
373
435
|
+ `its '${mode}' mode is conservatively treated as approval=${actual.approval} `
|
|
374
436
|
+ `sandbox=${actual.sandbox}`
|
|
375
|
-
: `
|
|
376
|
-
+ `sandbox=${actual.sandbox}
|
|
377
|
-
+ `
|
|
378
|
-
+ `approval=${approval} sandbox=${sandbox}`],
|
|
437
|
+
: `Codex ACP mode '${mode}' couples approval and filesystem as `
|
|
438
|
+
+ `approval=${actual.approval} sandbox=${actual.sandbox}; this does not exactly `
|
|
439
|
+
+ `represent approval=${approval} sandbox=${sandbox}`],
|
|
379
440
|
capabilities: codexCapabilities(actual.approval, actual.sandbox),
|
|
380
441
|
};
|
|
381
442
|
}
|
|
@@ -411,6 +472,7 @@ export function makeCodexAdapter(exec = realExec) {
|
|
|
411
472
|
currentIdentityTool: 'current_identity',
|
|
412
473
|
sendTool: 'send_message',
|
|
413
474
|
getMessagesTool: 'get_messages',
|
|
475
|
+
watchCommand: id => `ours-mcp watch "${id}"`,
|
|
414
476
|
monitorInstruction: (id, configuredRole) => {
|
|
415
477
|
const consented = configuredRole?.harness_options?.monitor === true;
|
|
416
478
|
const consent = consented
|