@ours.network/fleet 0.18.0 → 0.19.0-nightly.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +57 -121
  2. package/dist/application/fleet-query-service.js +0 -12
  3. package/dist/application/role-creation-service.js +10 -7
  4. package/dist/application/types.d.ts +0 -11
  5. package/dist/briefing.js +6 -15
  6. package/dist/build-info.json +6 -7
  7. package/dist/capabilities.d.ts +1 -3
  8. package/dist/capabilities.js +0 -3
  9. package/dist/cli.js +7 -83
  10. package/dist/config.d.ts +3 -11
  11. package/dist/config.js +15 -40
  12. package/dist/creation.d.ts +38 -22
  13. package/dist/creation.js +111 -24
  14. package/dist/docs.d.ts +1 -1
  15. package/dist/docs.js +46 -120
  16. package/dist/doctor.d.ts +5 -1
  17. package/dist/doctor.js +18 -11
  18. package/dist/fleet-proxy.d.ts +0 -5
  19. package/dist/harness/acp-agent.js +6 -11
  20. package/dist/harness/claude-code.js +11 -204
  21. package/dist/harness/codex.d.ts +1 -4
  22. package/dist/harness/codex.js +12 -74
  23. package/dist/harness/types.d.ts +4 -54
  24. package/dist/index.d.ts +0 -2
  25. package/dist/index.js +0 -1
  26. package/dist/loops/manager.d.ts +1 -30
  27. package/dist/loops/manager.js +6 -69
  28. package/dist/loops/state.d.ts +0 -18
  29. package/dist/loops/state.js +0 -4
  30. package/dist/monitor.js +1 -1
  31. package/dist/ops.d.ts +3 -0
  32. package/dist/ops.js +8 -3
  33. package/dist/owner-channel/attachments.d.ts +25 -2
  34. package/dist/owner-channel/attachments.js +61 -5
  35. package/dist/owner-channel/channel.d.ts +29 -30
  36. package/dist/owner-channel/channel.js +291 -291
  37. package/dist/owner-channel/message-recovery.d.ts +25 -0
  38. package/dist/owner-channel/message-recovery.js +114 -0
  39. package/dist/owner-channel/notices.d.ts +0 -7
  40. package/dist/owner-channel/notices.js +0 -9
  41. package/dist/owner-channel/ours-client.d.ts +148 -0
  42. package/dist/owner-channel/ours-client.js +231 -0
  43. package/dist/resolved-plan.js +0 -1
  44. package/dist/runner.d.ts +0 -48
  45. package/dist/runner.js +94 -236
  46. package/dist/session/acp.d.ts +0 -104
  47. package/dist/session/acp.js +10 -213
  48. package/dist/session/conversation-normalizer.d.ts +0 -6
  49. package/dist/session/conversation-normalizer.js +10 -153
  50. package/dist/session/conversation-types.d.ts +4 -23
  51. package/dist/session/types.d.ts +0 -35
  52. package/dist/spawn.js +26 -33
  53. package/dist/supervisor/systemd.js +29 -2
  54. package/dist/watchdog/briefing.js +0 -7
  55. package/dist/watchdog/run.js +3 -3
  56. package/dist/web-app/assets/{TerminalView-C_G1ID2P.js → TerminalView-BAVk1Bot.js} +1 -1
  57. package/dist/web-app/assets/{index-BCBK78hw.js → index-C3S-xFRU.js} +5 -5
  58. package/dist/web-app/index.html +1 -1
  59. package/dist/worklog.d.ts +1 -7
  60. package/dist/worklog.js +39 -191
  61. package/package.json +3 -1
  62. package/dist/harness-plugins.d.ts +0 -48
  63. package/dist/harness-plugins.js +0 -309
  64. package/dist/model-env.d.ts +0 -71
  65. package/dist/model-env.js +0 -106
  66. package/dist/owner-channel/mcp.d.ts +0 -24
  67. package/dist/owner-channel/mcp.js +0 -145
  68. package/dist/session/activity.d.ts +0 -31
  69. package/dist/session/activity.js +0 -48
@@ -6,99 +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
- 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
- ];
9
+ const OPTION_KEYS = ['plugins', 'mem_palace', 'mem_palace_midsession_autosave', 'permission_mode', 'effort'];
14
10
  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
- }
102
11
  /** Claude Code's accepted --permission-mode values. */
103
12
  const PERMISSION_MODES = ['default', 'acceptEdits', 'plan', 'dontAsk', 'bypassPermissions'];
104
13
  /**
@@ -218,8 +127,11 @@ export async function pretrust(dir, deps = {}) {
218
127
  * it must be the Monitor TOOL, not a background Bash task (which never wakes the
219
128
  * agent on output → an armed-looking but deaf monitor).
220
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';
221
133
  const armMonitor = (id) => 'arm a **persistent Monitor** (the Monitor TOOL — NOT a background Bash command; a ' +
222
- `background Bash task never wakes you on output) running \`ours-mcp watch "${id}"\` ` +
134
+ `background Bash task never wakes you on output) running \`${watchCommand(id)}\` ` +
223
135
  'so inbound ours mail wakes you';
224
136
  export function makeClaudeCodeAdapter(exec = realExec) {
225
137
  return {
@@ -237,7 +149,7 @@ export function makeClaudeCodeAdapter(exec = realExec) {
237
149
  }],
238
150
  };
239
151
  },
240
- validateOptions(opts, role) {
152
+ validateOptions(opts) {
241
153
  if (opts == null)
242
154
  return [];
243
155
  if (typeof opts !== 'object' || Array.isArray(opts))
@@ -245,50 +157,12 @@ export function makeClaudeCodeAdapter(exec = realExec) {
245
157
  const errors = Object.keys(opts)
246
158
  .filter(k => !OPTION_KEYS.includes(k))
247
159
  .map(k => ({ path: `harness_options.${k}`, message: `unknown option; allowed: ${OPTION_KEYS.join(', ')}` }));
248
- const o = opts;
249
- const effort = o.effort;
160
+ const effort = opts.effort;
250
161
  if (effort != null && !EFFORT_LEVELS.includes(effort))
251
162
  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
- }
286
163
  return errors;
287
164
  },
288
165
  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);
292
166
  // Pre-trust stays a HOST-side step: inside the sandbox ~/.claude.json is
293
167
  // read-only, and it is the fleet's job to trust the role's dirs, not the
294
168
  // agent's (5.1, 6.1).
@@ -304,15 +178,6 @@ export function makeClaudeCodeAdapter(exec = realExec) {
304
178
  CLAUDE_AUTOCOMPACT_PCT_OVERRIDE: String(autocompactPct(role)),
305
179
  MEMPALACE_HOOKS_AUTO_SAVE: 'false',
306
180
  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,
316
181
  };
317
182
  if (!memPalace)
318
183
  env.MEMPALACE_DISABLED = 'true';
@@ -323,29 +188,12 @@ export function makeClaudeCodeAdapter(exec = realExec) {
323
188
  if (role.isolation)
324
189
  mkdirSync(harnessRuntimeDir(dirs.stateDir, 'claude'), { recursive: true });
325
190
  const argv = [];
326
- let settingsOverlay;
327
191
  if (Object.keys(enabledPlugins).length) {
328
- settingsOverlay = join(dirs.stateDir, '.settings-overlay.json');
329
- writeFileSync(settingsOverlay, JSON.stringify({ enabledPlugins }, null, 2));
330
- argv.push('--settings', settingsOverlay);
192
+ const overlay = join(dirs.stateDir, '.settings-overlay.json');
193
+ writeFileSync(overlay, JSON.stringify({ enabledPlugins }, null, 2));
194
+ argv.push('--settings', overlay);
331
195
  }
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
- };
196
+ return { argv, env };
349
197
  },
350
198
  buildLaunch(role, mode, s, prep) {
351
199
  const stateDir = roleStateDir(role);
@@ -375,46 +223,6 @@ export function makeClaudeCodeAdapter(exec = realExec) {
375
223
  acpPermissionModeId(role) {
376
224
  return permissionMode(role);
377
225
  },
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
- },
418
226
  isolationPaths(_role, _dirs) {
419
227
  const claudeHome = join(home(), '.claude');
420
228
  return {
@@ -477,7 +285,6 @@ export function makeClaudeCodeAdapter(exec = realExec) {
477
285
  currentIdentityTool: 'current_identity',
478
286
  sendTool: 'send_message',
479
287
  getMessagesTool: 'get_messages',
480
- watchCommand: id => `ours-mcp watch "${id}"`,
481
288
  monitorInstruction: id => {
482
289
  const m = armMonitor(id);
483
290
  return `${m.charAt(0).toUpperCase()}${m.slice(1)}.`;
@@ -1,13 +1,10 @@
1
1
  import { type Exec } from '../exec.js';
2
- import type { AcpLaunch, HarnessAdapter, UnattendedCapability } from './types.js';
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;
@@ -5,8 +5,7 @@ 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 { resolveBundledAcpAgent, } from './acp-agent.js';
9
- import { restoreLockedHarnessMarketplace } from '../harness-plugins.js';
8
+ import { bundledAcpAgent, resolveBundledAcpAgent } from './acp-agent.js';
10
9
  const OPTION_KEYS = [
11
10
  'launcher', 'sandbox', 'approval', 'permission_mode', 'search', 'profile', 'config', 'add_dirs',
12
11
  'monitor',
@@ -52,7 +51,9 @@ function sandboxMode(role) {
52
51
  throw new Error(`invalid harness_options.sandbox "${s}"; allowed: ${SANDBOX_MODES.join(', ')}`);
53
52
  return s;
54
53
  }
55
- function modeForSandbox(sandbox) {
54
+ /** codex-acp exposes the same sandbox postures as named ACP agent modes. */
55
+ function acpAgentMode(role) {
56
+ const sandbox = sandboxMode(role);
56
57
  if (sandbox === 'read-only')
57
58
  return 'read-only';
58
59
  if (sandbox === 'workspace-write')
@@ -61,26 +62,6 @@ function modeForSandbox(sandbox) {
61
62
  return 'agent-full-access';
62
63
  return undefined;
63
64
  }
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
- }
84
65
  function acpModePermissions(mode) {
85
66
  if (mode === 'read-only')
86
67
  return { approval: 'on-request', sandbox: 'read-only' };
@@ -88,10 +69,6 @@ function acpModePermissions(mode) {
88
69
  return { approval: 'never', sandbox: 'danger-full-access' };
89
70
  return { approval: 'on-request', sandbox: 'workspace-write' };
90
71
  }
91
- /** The sandbox Codex will actually receive from the selected coupled ACP mode. */
92
- function acpRuntimeSandbox(role) {
93
- return acpModePermissions(acpAgentMode(role)).sandbox;
94
- }
95
72
  function fleetModeForApproval(nativeMode) {
96
73
  if (nativeMode === 'never')
97
74
  return 'allow';
@@ -125,18 +102,6 @@ function launcherMode(role) {
125
102
  function bundledCodexAcp() {
126
103
  return resolveBundledAcpAgent(CODEX_ACP_PACKAGE, 'codex-acp', 'codex-acp');
127
104
  }
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
- }
140
105
  function canOverrideBundledAcpApproval() {
141
106
  const resolution = bundledCodexAcp();
142
107
  return resolution.bundled && resolution.version === BUNDLED_CODEX_ACP_VERSION
@@ -182,7 +147,7 @@ function codexAcpEnvironment(role, dirs) {
182
147
  return {
183
148
  CODEX_PATH: command,
184
149
  [CODEX_PROXY_APPROVAL_ENV]: approvalPolicy(role) ?? 'on-request',
185
- [CODEX_PROXY_SANDBOX_ENV]: acpRuntimeSandbox(role),
150
+ [CODEX_PROXY_SANDBOX_ENV]: sandboxMode(role) ?? 'workspace-write',
186
151
  [CODEX_PROXY_MANIFEST_ENV]: resolution.manifestPath,
187
152
  ...(process.env.CODEX_PATH ? { [CODEX_PROXY_REAL_PATH_ENV]: process.env.CODEX_PATH } : {}),
188
153
  };
@@ -310,9 +275,6 @@ export function makeCodexAdapter(exec = realExec) {
310
275
  return errs;
311
276
  },
312
277
  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);
316
278
  // Per-role harness runtime home (5.1); harmless for un-isolated roles.
317
279
  // Only a role that declares `isolation:` gets a sandbox, and only a
318
280
  // sandbox needs this directory to exist before entry.
@@ -323,16 +285,7 @@ export function makeCodexAdapter(exec = realExec) {
323
285
  if (requested === 'ours-codex' && !hasOursCodex)
324
286
  throw new Error('harness_options.launcher is ours-codex, but ours-codex is not on PATH; install @ours.network/codex or use launcher: auto');
325
287
  const command = requested === 'codex' ? 'codex' : hasOursCodex ? 'ours-codex' : 'codex';
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
- };
288
+ return { argv: [], env: codexAcpEnvironment(role, dirs), command };
336
289
  },
337
290
  buildLaunch(role, mode, _s, prep) {
338
291
  const stateDir = roleStateDir(role);
@@ -346,30 +299,17 @@ export function makeCodexAdapter(exec = realExec) {
346
299
  },
347
300
  buildAcpLaunch(role, prep) {
348
301
  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;
355
302
  const argv = Array.isArray(configured)
356
303
  ? [...configured]
357
304
  : typeof configured === 'string'
358
305
  ? ['sh', '-c', configured]
359
- : resolved.argv;
306
+ : bundledAcpAgent(CODEX_ACP_PACKAGE, 'codex-acp', 'codex-acp');
360
307
  const initialMode = acpAgentMode(role);
361
308
  return {
362
309
  argv,
363
310
  env: initialMode ? { ...prep.env, INITIAL_AGENT_MODE: initialMode } : prep.env,
364
- ...(resolved?.permissionMetadataSource
365
- ? { permissionMetadataSource: resolved.permissionMetadataSource } : {}),
366
311
  };
367
312
  },
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
- },
373
313
  isolationPaths(role, _dirs) {
374
314
  const codexHome = join(home(), '.codex');
375
315
  const profile = role.harness_options?.profile;
@@ -422,9 +362,7 @@ export function makeCodexAdapter(exec = realExec) {
422
362
  const mode = acpAgentMode(role) ?? 'agent';
423
363
  const configured = role.session_options?.acp?.command;
424
364
  const overrideAvailable = configured == null && canOverrideBundledAcpApproval();
425
- const actual = overrideAvailable
426
- ? { approval, sandbox: acpRuntimeSandbox(role) }
427
- : acpModePermissions(mode);
365
+ const actual = overrideAvailable ? { approval, sandbox } : acpModePermissions(mode);
428
366
  const exact = actual.approval === approval && actual.sandbox === sandbox;
429
367
  return {
430
368
  ...translated,
@@ -434,9 +372,10 @@ export function makeCodexAdapter(exec = realExec) {
434
372
  ? `custom ACP command cannot be verified against approval=${approval} sandbox=${sandbox}; `
435
373
  + `its '${mode}' mode is conservatively treated as approval=${actual.approval} `
436
374
  + `sandbox=${actual.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}`],
375
+ : `codex-acp mode '${mode}' actually uses approval=${actual.approval} `
376
+ + `sandbox=${actual.sandbox}, and the bundled ${BUNDLED_CODEX_ACP_VERSION} `
377
+ + `app-server override is unavailable; this does not exactly represent `
378
+ + `approval=${approval} sandbox=${sandbox}`],
440
379
  capabilities: codexCapabilities(actual.approval, actual.sandbox),
441
380
  };
442
381
  }
@@ -472,7 +411,6 @@ export function makeCodexAdapter(exec = realExec) {
472
411
  currentIdentityTool: 'current_identity',
473
412
  sendTool: 'send_message',
474
413
  getMessagesTool: 'get_messages',
475
- watchCommand: id => `ours-mcp watch "${id}"`,
476
414
  monitorInstruction: (id, configuredRole) => {
477
415
  const consented = configuredRole?.harness_options?.monitor === true;
478
416
  const consent = consented
@@ -1,4 +1,3 @@
1
- import type { McpServer } from '@agentclientprotocol/sdk';
2
1
  import type { CommonPermissions, FleetPermissionMode, ResolvedRole } from '../config.js';
3
2
  export interface PrereqCheck {
4
3
  name: string;
@@ -22,39 +21,14 @@ export interface SessionPrep {
22
21
  env: Record<string, string>;
23
22
  /** Optional launcher selected after runtime prerequisite probing. */
24
23
  command?: string;
25
- /**
26
- * The settings overlay prepareSession wrote, if it wrote one.
27
- *
28
- * The tmux launch delivers this as `--settings <path>` in `argv`; an ACP agent
29
- * takes no flags, so it needs the PATH rather than the flag. Recorded here so
30
- * the two deliveries read one value instead of each re-deriving the filename.
31
- */
32
- settingsOverlay?: string;
33
- /**
34
- * The MCP config file prepareSession wrote for `harness_options.mcp_servers`,
35
- * if the role declared any. Same reason as `settingsOverlay`: the tmux launch
36
- * passes the file, the ACP launch has to send the servers themselves.
37
- */
38
- mcpConfigFile?: string;
39
24
  }
40
- /**
41
- * One MCP server as ACP's `session/new` declares it.
42
- *
43
- * ⚠ THE PROTOCOL'S OWN TYPE, DELIBERATELY NOT A LOCAL RESTATEMENT. `mcpServers`
44
- * goes onto the wire unchanged, so a hand-written near-copy would compile while
45
- * being subtly wrong — `env` and `headers` are REQUIRED arrays, and the stdio
46
- * variant is the one with no `type` field at all. Aliasing it also keeps
47
- * `session/new`'s response type inferable, which a structural stand-in silently
48
- * broke (every field of the result degraded to `unknown`).
49
- */
50
- export type AcpMcpServer = McpServer;
51
25
  export interface Launch {
52
26
  argv: string[];
53
27
  env: Record<string, string>;
54
28
  }
55
- export interface AcpLaunch extends Launch {
56
- /** Metadata vocabulary authenticated by the exact ACP artifact in argv. */
57
- permissionMetadataSource?: 'codex-acp';
29
+ export interface AcpLaunch {
30
+ argv: string[];
31
+ env: Record<string, string>;
58
32
  }
59
33
  /**
60
34
  * The result of expressing neutral `permissions:` in a harness's own terms.
@@ -93,7 +67,6 @@ export interface BriefingVocab {
93
67
  currentIdentityTool: string;
94
68
  sendTool: string;
95
69
  getMessagesTool: string;
96
- watchCommand(identity: string): string;
97
70
  monitorInstruction(identity: string, role?: ResolvedRole): string;
98
71
  /** Wake-source wording for a role whose monitor is supervisor-owned (monitor.mode=fleet). */
99
72
  supervisedWakeNote(identity: string, role?: ResolvedRole): string;
@@ -122,13 +95,7 @@ export interface HarnessAdapter {
122
95
  id: string;
123
96
  supportsResume: boolean;
124
97
  checkPrereqs(): Promise<PrereqReport>;
125
- /**
126
- * `role` is the SESSION-AWARE half: some harness options can only be honoured
127
- * on some session types, and an option that is silently dropped is worse than
128
- * one that is refused. Optional so an adapter that has nothing session-specific
129
- * to say keeps its one-argument implementation.
130
- */
131
- validateOptions(opts: unknown, role?: ResolvedRole): ValidationError[];
98
+ validateOptions(opts: unknown): ValidationError[];
132
99
  prepareSession(role: ResolvedRole, dirs: RoleDirs): Promise<SessionPrep>;
133
100
  buildLaunch(role: ResolvedRole, mode: 'fresh' | 'resume', s: SessionState, prep: SessionPrep): Launch;
134
101
  buildAcpLaunch?(role: ResolvedRole, prep: SessionPrep): AcpLaunch;
@@ -138,23 +105,6 @@ export interface HarnessAdapter {
138
105
  * agent's default. Omit for a harness whose ACP agent has no modes.
139
106
  */
140
107
  acpPermissionModeId?(role: ResolvedRole): string | undefined;
141
- /**
142
- * The MCP servers this role declares, for the `mcpServers` array of ACP's
143
- * `session/new` / `resume` / `load`. Empty (or omitted) leaves the agent's own
144
- * configuration alone, which is what fleet has always sent.
145
- */
146
- acpMcpServers?(role: ResolvedRole): AcpMcpServer[];
147
- /**
148
- * Agent-specific `_meta` for `session/new` — how a capability the CLI takes as
149
- * a flag reaches an ACP agent that accepts no flags.
150
- *
151
- * ⚠ THIS IS A PER-AGENT VOCABULARY, NOT PROTOCOL. `_meta` is free-form in ACP,
152
- * so what an adapter puts here is only honoured by the agent it was written
153
- * for. An adapter must therefore return nothing for an ACP command it did not
154
- * choose, and the options that depend on it must be refused at validation for
155
- * such a role rather than sent and silently ignored.
156
- */
157
- acpSessionMeta?(role: ResolvedRole, prep: SessionPrep): Record<string, unknown> | undefined;
158
108
  /** Effective portable policy and harness-native approval mode after native overrides win. */
159
109
  effectivePermissionMode?(role: ResolvedRole): {
160
110
  fleetMode: FleetPermissionMode;
package/dist/index.d.ts CHANGED
@@ -28,5 +28,3 @@ export { doctor } from './doctor.js';
28
28
  export { runOnce, runTemp } from './runner.js';
29
29
  export { Tmux } from './tmux.js';
30
30
  export { VERSION } from './version.js';
31
- export { HARNESS_PLUGIN_IDS, installHarnessPlugin, readHarnessPluginLock, restoreLockedHarnessMarketplace, resolveHarnessPluginConfigs, } from './harness-plugins.js';
32
- export type { HarnessPluginId, HarnessPluginChannel, HarnessPluginConfig, HarnessPluginConfigs, HarnessPluginLock, HarnessPluginInstallResult, } from './harness-plugins.js';
package/dist/index.js CHANGED
@@ -23,4 +23,3 @@ export { doctor } from './doctor.js';
23
23
  export { runOnce, runTemp } from './runner.js';
24
24
  export { Tmux } from './tmux.js';
25
25
  export { VERSION } from './version.js';
26
- export { HARNESS_PLUGIN_IDS, installHarnessPlugin, readHarnessPluginLock, restoreLockedHarnessMarketplace, resolveHarnessPluginConfigs, } from './harness-plugins.js';
@@ -68,32 +68,8 @@ export declare class ScheduledLoopManager implements ScheduledLoopManagerHandle
68
68
  private armAbandon;
69
69
  private finish;
70
70
  private advance;
71
- /**
72
- * Coalesce a backlog into one skip. The counters alone say how many
73
- * occurrences were lost but never when or for how long, so the window is
74
- * recorded too and carried on the state until a run is actually told about it
75
- * — a dropped pass has to stay visible to the next one, not just to whoever
76
- * was reading the log at the time.
77
- */
78
71
  private skipMissed;
79
- /**
80
- * Restart is not, by itself, a reason to lose an occurrence a running manager
81
- * would still have run. `poll` tolerates lateness up to one full interval and
82
- * runs the tick late; this path used to drop anything already due however
83
- * recently, so a role restarted seconds after its own tick came due lost it
84
- * outright. For an oversight role that is precisely the pass which would have
85
- * recorded why it restarted, so the failure erased its own witness.
86
- *
87
- * The tolerance is the only thing shared with `poll`. A backlog at least one
88
- * interval deep is still coalesced into a single skip and never replayed —
89
- * after a long outage exactly one occurrence survives, and `schedule` then
90
- * arms it through the ordinary path rather than firing a burst here.
91
- *
92
- * Running the survivor late cannot outpace the configured cadence: `advance`
93
- * moves the cursor by exactly one `intervalMs` per occurrence from the nominal
94
- * time, so a loop that keeps restarting still runs at most once per interval.
95
- */
96
- private skipRestartBacklog;
72
+ private skipRestartMisses;
97
73
  /**
98
74
  * A run the store could not record is dropped, not retried: the cursor has
99
75
  * already moved, so this can never become a busy loop, and the outage is
@@ -110,10 +86,5 @@ export declare class ScheduledLoopManager implements ScheduledLoopManagerHandle
110
86
  * until the process was restarted.
111
87
  */
112
88
  private recover;
113
- /**
114
- * The envelope is the only channel a scheduled pass has for learning about
115
- * the passes that did not happen. A gap stated here is what lets an oversight
116
- * role report its own outage instead of resuming as if nothing was missed.
117
- */
118
89
  private envelope;
119
90
  }