@ours.network/fleet 0.18.1 → 0.19.0-nightly.10
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 +86 -90
- package/dist/application/fleet-query-service.js +0 -12
- package/dist/application/role-creation-service.js +10 -7
- package/dist/application/types.d.ts +0 -11
- package/dist/briefing.js +6 -15
- package/dist/build-info.json +5 -5
- package/dist/cli.js +12 -37
- package/dist/config.d.ts +14 -6
- package/dist/config.js +58 -31
- package/dist/creation.d.ts +38 -22
- package/dist/creation.js +111 -24
- package/dist/docs.d.ts +1 -1
- package/dist/docs.js +71 -94
- package/dist/doctor.d.ts +5 -1
- package/dist/doctor.js +143 -12
- package/dist/fleet-proxy.d.ts +0 -5
- package/dist/harness/acp-agent.js +6 -11
- package/dist/harness/claude-code.js +11 -200
- package/dist/harness/codex.d.ts +1 -4
- package/dist/harness/codex.js +12 -70
- package/dist/harness/types.d.ts +4 -54
- package/dist/loops/manager.d.ts +1 -30
- package/dist/loops/manager.js +6 -69
- package/dist/loops/state.d.ts +0 -18
- package/dist/loops/state.js +0 -4
- package/dist/monitor.js +1 -1
- package/dist/ops.d.ts +3 -0
- package/dist/ops.js +8 -3
- package/dist/owner-channel/attachments.d.ts +25 -2
- package/dist/owner-channel/attachments.js +61 -5
- package/dist/owner-channel/channel.d.ts +36 -30
- package/dist/owner-channel/channel.js +338 -291
- package/dist/owner-channel/commands.d.ts +9 -0
- package/dist/owner-channel/commands.js +393 -0
- package/dist/owner-channel/message-recovery.d.ts +25 -0
- package/dist/owner-channel/message-recovery.js +114 -0
- package/dist/owner-channel/notices.d.ts +0 -7
- package/dist/owner-channel/notices.js +0 -9
- package/dist/owner-channel/ours-client.d.ts +148 -0
- package/dist/owner-channel/ours-client.js +231 -0
- package/dist/rooms-tasks/cli.d.ts +4 -0
- package/dist/rooms-tasks/cli.js +1102 -0
- package/dist/rooms-tasks/close.d.ts +21 -0
- package/dist/rooms-tasks/close.js +159 -0
- package/dist/rooms-tasks/config.d.ts +15 -0
- package/dist/rooms-tasks/config.js +174 -0
- package/dist/rooms-tasks/cowork-adapter.d.ts +77 -0
- package/dist/rooms-tasks/cowork-adapter.js +272 -0
- package/dist/rooms-tasks/external-worker.d.ts +2 -0
- package/dist/rooms-tasks/external-worker.js +40 -0
- package/dist/rooms-tasks/index.d.ts +10 -0
- package/dist/rooms-tasks/index.js +10 -0
- package/dist/rooms-tasks/provision.d.ts +21 -0
- package/dist/rooms-tasks/provision.js +243 -0
- package/dist/rooms-tasks/room-state.d.ts +28 -0
- package/dist/rooms-tasks/room-state.js +221 -0
- package/dist/rooms-tasks/task-state.d.ts +43 -0
- package/dist/rooms-tasks/task-state.js +277 -0
- package/dist/rooms-tasks/templates.d.ts +6 -0
- package/dist/rooms-tasks/templates.js +94 -0
- package/dist/rooms-tasks/terminal.d.ts +23 -0
- package/dist/rooms-tasks/terminal.js +53 -0
- package/dist/rooms-tasks/types.d.ts +187 -0
- package/dist/rooms-tasks/types.js +20 -0
- package/dist/runner.d.ts +0 -48
- package/dist/runner.js +94 -236
- package/dist/session/acp.d.ts +0 -104
- package/dist/session/acp.js +10 -213
- package/dist/session/conversation-normalizer.d.ts +0 -6
- package/dist/session/conversation-normalizer.js +10 -153
- package/dist/session/conversation-types.d.ts +4 -23
- package/dist/session/types.d.ts +0 -35
- package/dist/spawn.js +26 -33
- package/dist/supervisor/systemd.js +29 -2
- package/dist/temp-lifecycle.d.ts +9 -2
- package/dist/temp-lifecycle.js +61 -0
- package/dist/watchdog/briefing.js +0 -7
- package/dist/watchdog/run.js +3 -3
- package/dist/web-app/assets/{TerminalView-C_G1ID2P.js → TerminalView-BAVk1Bot.js} +1 -1
- package/dist/web-app/assets/{index-BCBK78hw.js → index-C3S-xFRU.js} +5 -5
- package/dist/web-app/index.html +1 -1
- package/dist/worklog.d.ts +1 -7
- package/dist/worklog.js +39 -191
- package/package.json +3 -1
- package/dist/model-env.d.ts +0 -71
- package/dist/model-env.js +0 -106
- package/dist/owner-channel/mcp.d.ts +0 -24
- package/dist/owner-channel/mcp.js +0 -145
- package/dist/session/activity.d.ts +0 -31
- package/dist/session/activity.js +0 -48
package/dist/doctor.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { userInfo } from 'node:os';
|
|
2
2
|
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { attachOursClient, } from '@ours.network/sdk/client';
|
|
3
4
|
import { realExec } from './exec.js';
|
|
4
5
|
import { isolationContextFor, loadConfig } from './config.js';
|
|
5
6
|
import { getAdapter, productionAdapters } from './harness/registry.js';
|
|
@@ -76,7 +77,7 @@ function installChecks(scan) {
|
|
|
76
77
|
];
|
|
77
78
|
}
|
|
78
79
|
/** Host-level + per-harness prerequisite report with actionable messages. */
|
|
79
|
-
export async function doctor(opts = {}, exec = realExec, platform = process.platform, fetchImpl = (u, i) => globalThis.fetch(u, i)) {
|
|
80
|
+
export async function doctor(opts = {}, exec = realExec, platform = process.platform, fetchImpl = (u, i) => globalThis.fetch(u, i), attachDaemon = attachOursClient) {
|
|
80
81
|
const checks = [];
|
|
81
82
|
// First: which artifact is producing this report. Everything below is only as
|
|
82
83
|
// trustworthy as the answer, and a second install with the same semver and
|
|
@@ -105,6 +106,126 @@ export async function doctor(opts = {}, exec = realExec, platform = process.plat
|
|
|
105
106
|
ok: true,
|
|
106
107
|
detail: `warning: ${diagnostic.message}`,
|
|
107
108
|
});
|
|
109
|
+
// ── Rooms/tasks checks (§5.3) ────────────────────────────────────────
|
|
110
|
+
if (loaded.ok && loaded.rooms) {
|
|
111
|
+
const rooms = loaded.rooms;
|
|
112
|
+
// Cowork socket reachability
|
|
113
|
+
const coworkConfig = rooms.cowork?.config;
|
|
114
|
+
try {
|
|
115
|
+
const { createCoworkAdapter } = await import('./rooms-tasks/cowork-adapter.js');
|
|
116
|
+
const adapter = createCoworkAdapter({ configPath: coworkConfig });
|
|
117
|
+
const reachable = await adapter.available();
|
|
118
|
+
checks.push({
|
|
119
|
+
name: 'cowork', ok: reachable,
|
|
120
|
+
detail: reachable
|
|
121
|
+
? 'management socket reachable'
|
|
122
|
+
: `management socket unreachable${coworkConfig ? ` (config: ${coworkConfig})` : ''}`,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
catch (e) {
|
|
126
|
+
checks.push({
|
|
127
|
+
name: 'cowork', ok: false,
|
|
128
|
+
detail: `management socket error${coworkConfig ? ` (config: ${coworkConfig})` : ''}: ${e?.message ?? e}`,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
// Owner CID shape
|
|
132
|
+
const cidOk = /^[0-9a-fA-F]{64}$/.test(rooms.owner.expected_cid);
|
|
133
|
+
checks.push({
|
|
134
|
+
name: 'rooms: owner CID', ok: cidOk,
|
|
135
|
+
detail: cidOk ? 'valid 64-hex CID' : `invalid CID shape: ${rooms.owner.expected_cid.slice(0, 16)}...`,
|
|
136
|
+
});
|
|
137
|
+
// Invite presence (never content)
|
|
138
|
+
checks.push({
|
|
139
|
+
name: 'rooms: owner invite',
|
|
140
|
+
ok: !!loaded.ownerInviteFingerprint,
|
|
141
|
+
detail: loaded.ownerInviteFingerprint
|
|
142
|
+
? `present (fingerprint: ${loaded.ownerInviteFingerprint.slice(0, 12)}...)`
|
|
143
|
+
: 'not configured — room owner attachment will use waiting_owner_invite',
|
|
144
|
+
});
|
|
145
|
+
// Template validity and referenced role validity
|
|
146
|
+
const { listTemplates, resolveTemplate } = await import('./rooms-tasks/templates.js');
|
|
147
|
+
const customs = loaded.roomTemplates ?? {};
|
|
148
|
+
const templates = listTemplates(customs);
|
|
149
|
+
const roleNames = new Set(roles.map(r => r.name));
|
|
150
|
+
for (const t of templates) {
|
|
151
|
+
const badRefs = t.members
|
|
152
|
+
.filter(m => m.role_ref && !roleNames.has(m.role_ref))
|
|
153
|
+
.map(m => m.role_ref);
|
|
154
|
+
if (badRefs.length) {
|
|
155
|
+
checks.push({
|
|
156
|
+
name: `template: ${t.name}@${t.version}`,
|
|
157
|
+
ok: true,
|
|
158
|
+
detail: `warning: role_ref(s) ${badRefs.join(', ')} not found in configured roles`,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
// Default template validity
|
|
163
|
+
const defaultTemplate = loaded.rooms.defaults?.template
|
|
164
|
+
?? loaded.tasks?.default_room_template;
|
|
165
|
+
if (defaultTemplate) {
|
|
166
|
+
const resolved = resolveTemplate(defaultTemplate, customs);
|
|
167
|
+
checks.push({
|
|
168
|
+
name: 'rooms: default template',
|
|
169
|
+
ok: !!resolved,
|
|
170
|
+
detail: resolved
|
|
171
|
+
? `${resolved.name}@${resolved.version}`
|
|
172
|
+
: `template '${defaultTemplate}' not found`,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
// Shared-daemon selection coherence
|
|
176
|
+
if (rooms.cowork?.config) {
|
|
177
|
+
const { existsSync: exists } = await import('node:fs');
|
|
178
|
+
const configOk = exists(rooms.cowork.config);
|
|
179
|
+
checks.push({
|
|
180
|
+
name: 'rooms: cowork config', ok: configOk,
|
|
181
|
+
detail: configOk
|
|
182
|
+
? `cowork config at ${rooms.cowork.config}`
|
|
183
|
+
: `cowork config not found: ${rooms.cowork.config}`,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
// Hard prerelease capability check
|
|
187
|
+
try {
|
|
188
|
+
const { createCoworkAdapter } = await import('./rooms-tasks/cowork-adapter.js');
|
|
189
|
+
const adapter = createCoworkAdapter({ configPath: coworkConfig });
|
|
190
|
+
const roomList = await adapter.listRooms();
|
|
191
|
+
checks.push({
|
|
192
|
+
name: 'rooms: capability', ok: true,
|
|
193
|
+
detail: `cowork room management operational (${roomList.length} room(s))`,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
catch (e) {
|
|
197
|
+
const msg = e?.message ?? String(e);
|
|
198
|
+
const isProto = msg.includes('protocol') || msg.includes('invalid');
|
|
199
|
+
checks.push({
|
|
200
|
+
name: 'rooms: capability', ok: false,
|
|
201
|
+
detail: isProto
|
|
202
|
+
? `cowork does not support room management protocol — upgrade ours-cowork to prerelease`
|
|
203
|
+
: `room management check failed: ${msg}`,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
// Stale task/room warnings
|
|
207
|
+
try {
|
|
208
|
+
const { listTasks } = await import('./rooms-tasks/task-state.js');
|
|
209
|
+
const { listRoomRecords } = await import('./rooms-tasks/room-state.js');
|
|
210
|
+
const tasks = listTasks();
|
|
211
|
+
const roomRecords = listRoomRecords();
|
|
212
|
+
const roomIds = new Set(roomRecords.map(r => r.room_id));
|
|
213
|
+
const taskIds = new Set(tasks.map(t => t.task_id));
|
|
214
|
+
const orphanedTasks = tasks.filter(t => t.room_id && !roomIds.has(t.room_id));
|
|
215
|
+
const orphanedRooms = roomRecords.filter(r => r.task_id && !taskIds.has(r.task_id));
|
|
216
|
+
if (orphanedTasks.length)
|
|
217
|
+
checks.push({
|
|
218
|
+
name: 'rooms: stale tasks', ok: true,
|
|
219
|
+
detail: `warning: ${orphanedTasks.length} task(s) reference missing rooms: ${orphanedTasks.map(t => t.task_id).join(', ')}`,
|
|
220
|
+
});
|
|
221
|
+
if (orphanedRooms.length)
|
|
222
|
+
checks.push({
|
|
223
|
+
name: 'rooms: stale rooms', ok: true,
|
|
224
|
+
detail: `warning: ${orphanedRooms.length} room(s) reference missing tasks: ${orphanedRooms.map(r => r.room_id).join(', ')}`,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
catch { /* state dir may not exist yet */ }
|
|
228
|
+
}
|
|
108
229
|
if (roles.length === 0 || roles.some(role => (role.session ?? 'tmux') === 'tmux')) {
|
|
109
230
|
const tmux = await exec('tmux', ['-V']);
|
|
110
231
|
checks.push({
|
|
@@ -112,16 +233,22 @@ export async function doctor(opts = {}, exec = realExec, platform = process.plat
|
|
|
112
233
|
detail: tmux.code === 0 ? tmux.stdout.trim() : 'not found — apt install tmux / brew install tmux',
|
|
113
234
|
});
|
|
114
235
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const
|
|
236
|
+
try {
|
|
237
|
+
const client = await attachDaemon({
|
|
238
|
+
env: process.env,
|
|
239
|
+
leaseToken: `ours-fleet-doctor-${process.pid}`,
|
|
240
|
+
clientPid: process.pid,
|
|
241
|
+
});
|
|
242
|
+
const info = await client.version();
|
|
243
|
+
if (info.name !== 'ours' || typeof info.version !== 'string')
|
|
244
|
+
throw new Error('the selected endpoint did not return a valid ours daemon identity');
|
|
245
|
+
checks.push({ name: 'ours daemon', ok: true, detail: `running (${info.version})` });
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
122
248
|
checks.push({
|
|
123
|
-
name: 'ours
|
|
124
|
-
detail:
|
|
249
|
+
name: 'ours daemon', ok: false,
|
|
250
|
+
detail: `not reachable through the SDK — start it with: ours daemon start `
|
|
251
|
+
+ `[${error?.message ?? String(error)}]`,
|
|
125
252
|
});
|
|
126
253
|
}
|
|
127
254
|
if (platform === 'linux') {
|
|
@@ -288,7 +415,7 @@ export async function doctor(opts = {}, exec = realExec, platform = process.plat
|
|
|
288
415
|
}
|
|
289
416
|
catch (e) {
|
|
290
417
|
detail = `unreachable on :${endpoint.port} — monitored roles run degraded until it is up ` +
|
|
291
|
-
`(start it: ours
|
|
418
|
+
`(start it: ours daemon start) [${e?.message ?? e}]`;
|
|
292
419
|
}
|
|
293
420
|
checks.push({ name: checkName, ok, detail });
|
|
294
421
|
}
|
|
@@ -398,7 +525,11 @@ export async function doctor(opts = {}, exec = realExec, platform = process.plat
|
|
|
398
525
|
function loadConfigResult(configPath, yamlMode) {
|
|
399
526
|
try {
|
|
400
527
|
const cfg = loadConfig(configPath, { yamlMode });
|
|
401
|
-
return {
|
|
528
|
+
return {
|
|
529
|
+
ok: true, roles: cfg.roles, files: cfg.files, diagnostics: cfg.diagnostics,
|
|
530
|
+
rooms: cfg.rooms, roomTemplates: cfg.roomTemplates, tasks: cfg.tasks,
|
|
531
|
+
ownerInviteFingerprint: cfg.ownerInviteFingerprint,
|
|
532
|
+
};
|
|
402
533
|
}
|
|
403
534
|
catch (e) {
|
|
404
535
|
return { ok: false, error: e instanceof Error ? e.message : String(e) };
|
package/dist/fleet-proxy.d.ts
CHANGED
|
@@ -12,11 +12,6 @@ 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
|
-
};
|
|
20
15
|
inherited: string[];
|
|
21
16
|
creationActionId: string;
|
|
22
17
|
}
|
|
@@ -1,23 +1,18 @@
|
|
|
1
1
|
import { createRequire } from 'node:module';
|
|
2
|
-
import {
|
|
3
|
-
import { dirname,
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, resolve } 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 relative = typeof manifest.bin === 'string'
|
|
10
10
|
? manifest.bin
|
|
11
11
|
: manifest.bin?.[binName];
|
|
12
|
-
if (!
|
|
12
|
+
if (!relative)
|
|
13
13
|
return { argv: [fallbackCommand], bundled: false };
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
const entrypointFromRoot = relative(packageRoot, entrypoint);
|
|
17
|
-
if (entrypointFromRoot === '..'
|
|
18
|
-
|| entrypointFromRoot.startsWith(`..${sep}`)
|
|
19
|
-
|| isAbsolute(entrypointFromRoot)
|
|
20
|
-
|| !statSync(entrypoint).isFile())
|
|
14
|
+
const entrypoint = resolve(dirname(manifestPath), relative);
|
|
15
|
+
if (!existsSync(entrypoint))
|
|
21
16
|
return { argv: [fallbackCommand], bundled: false };
|
|
22
17
|
return {
|
|
23
18
|
argv: [process.execPath, entrypoint], bundled: true, manifestPath,
|
|
@@ -6,98 +6,8 @@ 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
|
-
const OPTION_KEYS = [
|
|
10
|
-
'plugins', 'mem_palace', 'mem_palace_midsession_autosave', 'permission_mode', 'effort',
|
|
11
|
-
'mcp_servers', 'mcp_servers_only',
|
|
12
|
-
];
|
|
9
|
+
const OPTION_KEYS = ['plugins', 'mem_palace', 'mem_palace_midsession_autosave', 'permission_mode', 'effort'];
|
|
13
10
|
const EFFORT_LEVELS = ['low', 'medium', 'high', 'xhigh', 'max'];
|
|
14
|
-
/** `.mcp.json` server types. Absent means stdio, as the file format has it. */
|
|
15
|
-
const MCP_SERVER_TYPES = ['stdio', 'http', 'sse'];
|
|
16
|
-
/** A role that names its own ACP command runs a process fleet did not choose. */
|
|
17
|
-
const customAcpCommand = (role) => role.session === 'acp' && role.session_options?.acp?.command != null;
|
|
18
|
-
/**
|
|
19
|
-
* Does this server set include the ours connector?
|
|
20
|
-
*
|
|
21
|
-
* Load-bearing, and the reason it is a check rather than a doc line:
|
|
22
|
-
* `mcp_servers_only` maps to `--strict-mcp-config`, which ignores EVERY other MCP
|
|
23
|
-
* configuration — project `.mcp.json`, user settings, and **plugins**. On a
|
|
24
|
-
* normal install the ours connector arrives as a plugin
|
|
25
|
-
* (`~/.claude/plugins/.../plugin.json` declares `ours`), so a role that turns
|
|
26
|
-
* strict mode on without re-declaring it loses `send_message` and `get_messages`
|
|
27
|
-
* and cannot report that it has: a mute agent looks exactly like a quiet one.
|
|
28
|
-
*
|
|
29
|
-
* Matched on the command line rather than the server's NAME, because the name is
|
|
30
|
-
* the operator's to choose and would make this trivially satisfiable by writing
|
|
31
|
-
* `ours:` above the wrong command.
|
|
32
|
-
*/
|
|
33
|
-
const declaresOursConnector = (servers) => Object.values(servers).some(s => [s.command ?? '', ...(s.args ?? [])].some(part => /(^|[/\\])ours-mcp($|\s)|@ours\.network[/\\]mcp/.test(part)));
|
|
34
|
-
/** Shape-check `harness_options.mcp_servers` against `.mcp.json`'s own rules. */
|
|
35
|
-
function validateMcpServers(servers) {
|
|
36
|
-
if (servers == null)
|
|
37
|
-
return [];
|
|
38
|
-
const at = (k = '') => ({ path: `harness_options.mcp_servers${k}` });
|
|
39
|
-
if (typeof servers !== 'object' || Array.isArray(servers))
|
|
40
|
-
return [{ ...at(), message: 'must be a map of server name to server definition' }];
|
|
41
|
-
const entries = Object.entries(servers);
|
|
42
|
-
if (!entries.length)
|
|
43
|
-
return [{ ...at(), message: 'must declare at least one server, or be omitted' }];
|
|
44
|
-
const errors = [];
|
|
45
|
-
for (const [name, raw] of entries) {
|
|
46
|
-
const p = `.${name}`;
|
|
47
|
-
if (!/^[A-Za-z0-9_-]+$/.test(name)) {
|
|
48
|
-
errors.push({ ...at(p), message: 'server name must be [A-Za-z0-9_-]' });
|
|
49
|
-
continue;
|
|
50
|
-
}
|
|
51
|
-
if (raw == null || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
52
|
-
errors.push({ ...at(p), message: 'must be a map' });
|
|
53
|
-
continue;
|
|
54
|
-
}
|
|
55
|
-
const s = raw;
|
|
56
|
-
if (s.type != null && !MCP_SERVER_TYPES.includes(s.type))
|
|
57
|
-
errors.push({ ...at(`${p}.type`), message: `must be one of: ${MCP_SERVER_TYPES.join(', ')}` });
|
|
58
|
-
const remote = s.type === 'http' || s.type === 'sse';
|
|
59
|
-
if (remote) {
|
|
60
|
-
if (typeof s.url !== 'string' || !s.url.trim())
|
|
61
|
-
errors.push({ ...at(`${p}.url`), message: `must be a non-empty URL for a ${s.type} server` });
|
|
62
|
-
if (s.command != null)
|
|
63
|
-
errors.push({ ...at(`${p}.command`), message: `must not be set for a ${s.type} server` });
|
|
64
|
-
}
|
|
65
|
-
else {
|
|
66
|
-
if (typeof s.command !== 'string' || !s.command.trim())
|
|
67
|
-
errors.push({ ...at(`${p}.command`), message: 'must be a non-empty command for a stdio server' });
|
|
68
|
-
if (s.args != null && (!Array.isArray(s.args) || s.args.some(a => typeof a !== 'string')))
|
|
69
|
-
errors.push({ ...at(`${p}.args`), message: 'must be an array of strings' });
|
|
70
|
-
if (s.url != null)
|
|
71
|
-
errors.push({ ...at(`${p}.url`), message: 'must not be set for a stdio server' });
|
|
72
|
-
}
|
|
73
|
-
for (const key of ['env', 'headers']) {
|
|
74
|
-
const v = s[key];
|
|
75
|
-
if (v == null)
|
|
76
|
-
continue;
|
|
77
|
-
if (typeof v !== 'object' || Array.isArray(v)
|
|
78
|
-
|| Object.values(v).some(x => typeof x !== 'string'))
|
|
79
|
-
errors.push({ ...at(`${p}.${key}`), message: 'must be a map of string to string' });
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
return errors;
|
|
83
|
-
}
|
|
84
|
-
/** `harness_options.mcp_servers` in ACP's `session/new` array shape. */
|
|
85
|
-
function acpMcpServersFor(servers) {
|
|
86
|
-
if (!servers)
|
|
87
|
-
return [];
|
|
88
|
-
// `env` and `headers` are REQUIRED arrays in the protocol, so they are always
|
|
89
|
-
// sent — empty when the role declared none.
|
|
90
|
-
const pairs = (r) => Object.entries(r ?? {}).map(([name, value]) => ({ name, value }));
|
|
91
|
-
return Object.entries(servers).map(([name, s]) => {
|
|
92
|
-
if (s.type === 'http' || s.type === 'sse')
|
|
93
|
-
return { name, type: s.type, url: s.url, headers: pairs(s.headers) };
|
|
94
|
-
// Stdio carries NO `type` field: ACP's stdio variant is the one without it,
|
|
95
|
-
// and the bundled agent keys on exactly that (claude-agent-acp
|
|
96
|
-
// acp-agent.js:4058, `!("type" in server)`), so sending `type: 'stdio'`
|
|
97
|
-
// would drop the server on the floor.
|
|
98
|
-
return { name, command: s.command, args: s.args ?? [], env: pairs(s.env) };
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
11
|
/** Claude Code's accepted --permission-mode values. */
|
|
102
12
|
const PERMISSION_MODES = ['default', 'acceptEdits', 'plan', 'dontAsk', 'bypassPermissions'];
|
|
103
13
|
/**
|
|
@@ -217,8 +127,11 @@ export async function pretrust(dir, deps = {}) {
|
|
|
217
127
|
* it must be the Monitor TOOL, not a background Bash task (which never wakes the
|
|
218
128
|
* agent on output → an armed-looking but deaf monitor).
|
|
219
129
|
*/
|
|
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';
|
|
220
133
|
const armMonitor = (id) => 'arm a **persistent Monitor** (the Monitor TOOL — NOT a background Bash command; a ' +
|
|
221
|
-
`background Bash task never wakes you on output) running
|
|
134
|
+
`background Bash task never wakes you on output) running \`${watchCommand(id)}\` ` +
|
|
222
135
|
'so inbound ours mail wakes you';
|
|
223
136
|
export function makeClaudeCodeAdapter(exec = realExec) {
|
|
224
137
|
return {
|
|
@@ -236,7 +149,7 @@ export function makeClaudeCodeAdapter(exec = realExec) {
|
|
|
236
149
|
}],
|
|
237
150
|
};
|
|
238
151
|
},
|
|
239
|
-
validateOptions(opts
|
|
152
|
+
validateOptions(opts) {
|
|
240
153
|
if (opts == null)
|
|
241
154
|
return [];
|
|
242
155
|
if (typeof opts !== 'object' || Array.isArray(opts))
|
|
@@ -244,44 +157,9 @@ export function makeClaudeCodeAdapter(exec = realExec) {
|
|
|
244
157
|
const errors = Object.keys(opts)
|
|
245
158
|
.filter(k => !OPTION_KEYS.includes(k))
|
|
246
159
|
.map(k => ({ path: `harness_options.${k}`, message: `unknown option; allowed: ${OPTION_KEYS.join(', ')}` }));
|
|
247
|
-
const
|
|
248
|
-
const effort = o.effort;
|
|
160
|
+
const effort = opts.effort;
|
|
249
161
|
if (effort != null && !EFFORT_LEVELS.includes(effort))
|
|
250
162
|
errors.push({ path: 'harness_options.effort', message: `must be one of: ${EFFORT_LEVELS.join(', ')}` });
|
|
251
|
-
if (o.mcp_servers_only != null && typeof o.mcp_servers_only !== 'boolean')
|
|
252
|
-
errors.push({ path: 'harness_options.mcp_servers_only', message: 'must be a boolean' });
|
|
253
|
-
errors.push(...validateMcpServers(o.mcp_servers));
|
|
254
|
-
if (o.mcp_servers_only === true && !o.mcp_servers)
|
|
255
|
-
errors.push({
|
|
256
|
-
path: 'harness_options.mcp_servers_only',
|
|
257
|
-
message: 'requires harness_options.mcp_servers; on its own it would leave the role with no MCP servers at all',
|
|
258
|
-
});
|
|
259
|
-
// The muteness gate. Only when the declared set is otherwise well-formed —
|
|
260
|
-
// a shape error already told the operator to look here.
|
|
261
|
-
if (o.mcp_servers_only === true && o.mcp_servers && errors.length === 0
|
|
262
|
-
&& !declaresOursConnector(o.mcp_servers))
|
|
263
|
-
errors.push({
|
|
264
|
-
path: 'harness_options.mcp_servers',
|
|
265
|
-
message: 'mcp_servers_only ignores every other MCP configuration, INCLUDING plugins — and the ours '
|
|
266
|
-
+ 'connector is normally a plugin, so this role would have no send_message or get_messages and no way '
|
|
267
|
-
+ 'to report that. Declare it explicitly, e.g. ours: { command: ours-mcp, args: [proxy] }',
|
|
268
|
-
});
|
|
269
|
-
// Session-aware refusals. Both options reach an ACP session through the
|
|
270
|
-
// bundled agent's `_meta` vocabulary, so a role that launches a DIFFERENT
|
|
271
|
-
// ACP agent cannot be promised either one. Refuse rather than send it and
|
|
272
|
-
// hope: silently dropping the config is the defect being fixed here.
|
|
273
|
-
if (role && customAcpCommand(role)) {
|
|
274
|
-
for (const key of ['plugins', 'mcp_servers', 'mcp_servers_only']) {
|
|
275
|
-
if (o[key] == null)
|
|
276
|
-
continue;
|
|
277
|
-
errors.push({
|
|
278
|
-
path: `harness_options.${key}`,
|
|
279
|
-
message: 'cannot be honoured with session_options.acp.command: it is delivered through the bundled '
|
|
280
|
-
+ 'Claude ACP agent\'s _meta vocabulary, which another agent has no reason to read. Drop the '
|
|
281
|
-
+ 'custom ACP command, or drop this option',
|
|
282
|
-
});
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
163
|
return errors;
|
|
286
164
|
},
|
|
287
165
|
async prepareSession(role, dirs) {
|
|
@@ -300,15 +178,6 @@ export function makeClaudeCodeAdapter(exec = realExec) {
|
|
|
300
178
|
CLAUDE_AUTOCOMPACT_PCT_OVERRIDE: String(autocompactPct(role)),
|
|
301
179
|
MEMPALACE_HOOKS_AUTO_SAVE: 'false',
|
|
302
180
|
MEMPALACE_MIDSESSION_AUTOSAVE: o.mem_palace_midsession_autosave ? 'true' : 'false',
|
|
303
|
-
// The role's identity, for the ours connector to bind at startup instead of
|
|
304
|
-
// the briefing telling the MODEL to call choose_identity. Both launches
|
|
305
|
-
// return `prep.env`, so this one line covers tmux and ACP alike.
|
|
306
|
-
//
|
|
307
|
-
// The bind the connector performs is PLAIN and fail-closed: it can never
|
|
308
|
-
// evict a live session, and a role whose identity does not exist yet simply
|
|
309
|
-
// boots unbound and falls through to the briefing's create-if-missing step.
|
|
310
|
-
// Nothing here may ever grow a force flag.
|
|
311
|
-
OURS_BIND_IDENTITY: role.identity,
|
|
312
181
|
};
|
|
313
182
|
if (!memPalace)
|
|
314
183
|
env.MEMPALACE_DISABLED = 'true';
|
|
@@ -319,29 +188,12 @@ export function makeClaudeCodeAdapter(exec = realExec) {
|
|
|
319
188
|
if (role.isolation)
|
|
320
189
|
mkdirSync(harnessRuntimeDir(dirs.stateDir, 'claude'), { recursive: true });
|
|
321
190
|
const argv = [];
|
|
322
|
-
let settingsOverlay;
|
|
323
191
|
if (Object.keys(enabledPlugins).length) {
|
|
324
|
-
|
|
325
|
-
writeFileSync(
|
|
326
|
-
argv.push('--settings',
|
|
192
|
+
const overlay = join(dirs.stateDir, '.settings-overlay.json');
|
|
193
|
+
writeFileSync(overlay, JSON.stringify({ enabledPlugins }, null, 2));
|
|
194
|
+
argv.push('--settings', overlay);
|
|
327
195
|
}
|
|
328
|
-
|
|
329
|
-
// file's servers; `--strict-mcp-config` is what makes the set exclusive, and
|
|
330
|
-
// it is opt-in per role because it drops everything else the user has,
|
|
331
|
-
// plugins included (see `declaresOursConnector`).
|
|
332
|
-
let mcpConfigFile;
|
|
333
|
-
if (o.mcp_servers) {
|
|
334
|
-
mcpConfigFile = join(dirs.stateDir, '.mcp-config.json');
|
|
335
|
-
writeFileSync(mcpConfigFile, JSON.stringify({ mcpServers: o.mcp_servers }, null, 2), { mode: 0o600 });
|
|
336
|
-
argv.push('--mcp-config', mcpConfigFile);
|
|
337
|
-
if (o.mcp_servers_only === true)
|
|
338
|
-
argv.push('--strict-mcp-config');
|
|
339
|
-
}
|
|
340
|
-
return {
|
|
341
|
-
argv, env,
|
|
342
|
-
...(settingsOverlay ? { settingsOverlay } : {}),
|
|
343
|
-
...(mcpConfigFile ? { mcpConfigFile } : {}),
|
|
344
|
-
};
|
|
196
|
+
return { argv, env };
|
|
345
197
|
},
|
|
346
198
|
buildLaunch(role, mode, s, prep) {
|
|
347
199
|
const stateDir = roleStateDir(role);
|
|
@@ -371,46 +223,6 @@ export function makeClaudeCodeAdapter(exec = realExec) {
|
|
|
371
223
|
acpPermissionModeId(role) {
|
|
372
224
|
return permissionMode(role);
|
|
373
225
|
},
|
|
374
|
-
/**
|
|
375
|
-
* Deliver, over ACP, the two things the tmux launch delivers as flags.
|
|
376
|
-
*
|
|
377
|
-
* `buildAcpLaunch` builds its own argv and cannot carry `prep.argv`: the
|
|
378
|
-
* process it launches is the ACP agent, not `claude`, and it takes none of
|
|
379
|
-
* claude's flags. That is why `harness_options.plugins` did nothing at all on
|
|
380
|
-
* an ACP role — the overlay was written and then dropped, and the mem-palace
|
|
381
|
-
* toggle rode `prep.env` and survived, so the failure was silent AND
|
|
382
|
-
* selective.
|
|
383
|
-
*
|
|
384
|
-
* `_meta.claudeCode.options` is the bundled agent's own passthrough into the
|
|
385
|
-
* Claude Agent SDK (@agentclientprotocol/claude-agent-acp, acp-agent.js:4092
|
|
386
|
-
* → the `options` object at :4144). `settings` takes the same overlay path
|
|
387
|
-
* `--settings` takes; `strictMcpConfig` is the SDK's spelling of
|
|
388
|
-
* `--strict-mcp-config`. Both are spread BEFORE the fields the agent forces,
|
|
389
|
-
* so neither is overwritten.
|
|
390
|
-
*
|
|
391
|
-
* ⚠ RETURNS NOTHING FOR A ROLE THAT NAMES ITS OWN ACP COMMAND. That process
|
|
392
|
-
* is not the bundled agent and has no reason to read this vocabulary; sending
|
|
393
|
-
* it anyway would be the silent drop again, one level down. `validateOptions`
|
|
394
|
-
* refuses those roles instead.
|
|
395
|
-
*/
|
|
396
|
-
acpSessionMeta(role, prep) {
|
|
397
|
-
if (customAcpCommand(role))
|
|
398
|
-
return undefined;
|
|
399
|
-
const options = {};
|
|
400
|
-
if (prep.settingsOverlay)
|
|
401
|
-
options.settings = prep.settingsOverlay;
|
|
402
|
-
if (role.harness_options?.mcp_servers_only === true)
|
|
403
|
-
options.strictMcpConfig = true;
|
|
404
|
-
return Object.keys(options).length ? { claudeCode: { options } } : undefined;
|
|
405
|
-
},
|
|
406
|
-
/**
|
|
407
|
-
* The declared servers, in ACP's array shape. Sent on `session/new` and on
|
|
408
|
-
* resume/load, because the SDK builds its server set once per session and a
|
|
409
|
-
* resumed session that dropped them would quietly lose its tools.
|
|
410
|
-
*/
|
|
411
|
-
acpMcpServers(role) {
|
|
412
|
-
return acpMcpServersFor(role.harness_options?.mcp_servers);
|
|
413
|
-
},
|
|
414
226
|
isolationPaths(_role, _dirs) {
|
|
415
227
|
const claudeHome = join(home(), '.claude');
|
|
416
228
|
return {
|
|
@@ -473,7 +285,6 @@ export function makeClaudeCodeAdapter(exec = realExec) {
|
|
|
473
285
|
currentIdentityTool: 'current_identity',
|
|
474
286
|
sendTool: 'send_message',
|
|
475
287
|
getMessagesTool: 'get_messages',
|
|
476
|
-
watchCommand: id => `ours-mcp watch "${id}"`,
|
|
477
288
|
monitorInstruction: id => {
|
|
478
289
|
const m = armMonitor(id);
|
|
479
290
|
return `${m.charAt(0).toUpperCase()}${m.slice(1)}.`;
|
package/dist/harness/codex.d.ts
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
import { type Exec } from '../exec.js';
|
|
2
|
-
import type {
|
|
3
|
-
import { type AcpAgentResolution } from './acp-agent.js';
|
|
2
|
+
import type { HarnessAdapter, UnattendedCapability } from './types.js';
|
|
4
3
|
/**
|
|
5
4
|
* What an unattended role can actually do under Codex's native settings.
|
|
6
5
|
* `on-request` and `untrusted` stop to ask, and with no console attached that
|
|
7
6
|
* request is refused rather than answered — so the role can only read.
|
|
8
7
|
*/
|
|
9
8
|
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'>;
|
|
12
9
|
export declare function makeCodexAdapter(exec?: Exec): HarnessAdapter;
|
|
13
10
|
export declare const codexAdapter: HarnessAdapter;
|