@ours.network/fleet 0.18.1 → 0.19.0-nightly.2

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 (79) hide show
  1. package/README.md +57 -91
  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 +5 -5
  7. package/dist/cli.js +12 -37
  8. package/dist/config.d.ts +8 -6
  9. package/dist/config.js +46 -30
  10. package/dist/creation.d.ts +38 -22
  11. package/dist/creation.js +111 -24
  12. package/dist/docs.d.ts +1 -1
  13. package/dist/docs.js +44 -95
  14. package/dist/doctor.d.ts +5 -1
  15. package/dist/doctor.js +18 -11
  16. package/dist/fleet-proxy.d.ts +0 -5
  17. package/dist/harness/acp-agent.js +6 -11
  18. package/dist/harness/claude-code.js +11 -200
  19. package/dist/harness/codex.d.ts +1 -4
  20. package/dist/harness/codex.js +12 -70
  21. package/dist/harness/types.d.ts +4 -54
  22. package/dist/loops/manager.d.ts +1 -30
  23. package/dist/loops/manager.js +6 -69
  24. package/dist/loops/state.d.ts +0 -18
  25. package/dist/loops/state.js +0 -4
  26. package/dist/monitor.js +1 -1
  27. package/dist/ops.d.ts +3 -0
  28. package/dist/ops.js +8 -3
  29. package/dist/owner-channel/attachments.d.ts +25 -2
  30. package/dist/owner-channel/attachments.js +61 -5
  31. package/dist/owner-channel/channel.d.ts +29 -30
  32. package/dist/owner-channel/channel.js +291 -291
  33. package/dist/owner-channel/commands.js +89 -0
  34. package/dist/owner-channel/message-recovery.d.ts +25 -0
  35. package/dist/owner-channel/message-recovery.js +114 -0
  36. package/dist/owner-channel/notices.d.ts +0 -7
  37. package/dist/owner-channel/notices.js +0 -9
  38. package/dist/owner-channel/ours-client.d.ts +148 -0
  39. package/dist/owner-channel/ours-client.js +231 -0
  40. package/dist/rooms-tasks/cli.d.ts +4 -0
  41. package/dist/rooms-tasks/cli.js +565 -0
  42. package/dist/rooms-tasks/config.d.ts +15 -0
  43. package/dist/rooms-tasks/config.js +171 -0
  44. package/dist/rooms-tasks/cowork-adapter.d.ts +80 -0
  45. package/dist/rooms-tasks/cowork-adapter.js +48 -0
  46. package/dist/rooms-tasks/index.d.ts +7 -0
  47. package/dist/rooms-tasks/index.js +7 -0
  48. package/dist/rooms-tasks/room-state.d.ts +22 -0
  49. package/dist/rooms-tasks/room-state.js +117 -0
  50. package/dist/rooms-tasks/task-state.d.ts +31 -0
  51. package/dist/rooms-tasks/task-state.js +173 -0
  52. package/dist/rooms-tasks/templates.d.ts +6 -0
  53. package/dist/rooms-tasks/templates.js +80 -0
  54. package/dist/rooms-tasks/types.d.ts +153 -0
  55. package/dist/rooms-tasks/types.js +20 -0
  56. package/dist/runner.d.ts +0 -48
  57. package/dist/runner.js +94 -236
  58. package/dist/session/acp.d.ts +0 -104
  59. package/dist/session/acp.js +10 -213
  60. package/dist/session/conversation-normalizer.d.ts +0 -6
  61. package/dist/session/conversation-normalizer.js +10 -153
  62. package/dist/session/conversation-types.d.ts +4 -23
  63. package/dist/session/types.d.ts +0 -35
  64. package/dist/spawn.js +26 -33
  65. package/dist/supervisor/systemd.js +29 -2
  66. package/dist/watchdog/briefing.js +0 -7
  67. package/dist/watchdog/run.js +3 -3
  68. package/dist/web-app/assets/{TerminalView-C_G1ID2P.js → TerminalView-BAVk1Bot.js} +1 -1
  69. package/dist/web-app/assets/{index-BCBK78hw.js → index-C3S-xFRU.js} +5 -5
  70. package/dist/web-app/index.html +1 -1
  71. package/dist/worklog.d.ts +1 -7
  72. package/dist/worklog.js +39 -191
  73. package/package.json +3 -1
  74. package/dist/model-env.d.ts +0 -71
  75. package/dist/model-env.js +0 -106
  76. package/dist/owner-channel/mcp.d.ts +0 -24
  77. package/dist/owner-channel/mcp.js +0 -145
  78. package/dist/session/activity.d.ts +0 -31
  79. package/dist/session/activity.js +0 -48
package/dist/docs.js CHANGED
@@ -190,7 +190,8 @@ exists. If missing, it uses ours MCP \`create_temporary_identity\` when that too
190
190
  is exposed, tying a newly-created identity to the connector session lifecycle;
191
191
  older servers fall back to \`create_identity\`. Collisions and creation errors
192
192
  stop safely without force-adopting or deleting identity state. Permanent roles
193
- retain normal \`create_identity\` behavior.
193
+ are provisioned by fleet before launch and never delegate normal identity
194
+ creation to the harness.
194
195
 
195
196
  The temporary supervisor treats its first positive identity observation as the
196
197
  lifecycle readiness gate: a cold harness may take as long as needed to read its
@@ -270,9 +271,6 @@ roles:
270
271
  add_dirs: [/data/shared]
271
272
  config:
272
273
  model_reasoning_effort: high
273
- mcp_servers: # claude-code: per-role MCP servers, additive by default
274
- ours: { command: ours-mcp, args: [proxy] }
275
- mcp_servers_only: false # true = ONLY these; drops user/project/plugin servers
276
274
  bio: Public role card and when peers should engage it.
277
275
  persona: Local operating contract, boundaries, and escalation policy.
278
276
  briefing_file: /absolute/custom-briefing.md
@@ -317,11 +315,6 @@ Role values override defaults. \`\${name}\` substitutes entries from \`vars\`.
317
315
  Other role fields include \`max_tokens\`, \`autocompact_pct\`, and \`isolation\`.
318
316
  Use README.md for the complete isolation policy and resource-cap schema.
319
317
 
320
- Supervised roles connect to the operator-configured ours daemon; they do not own its
321
- lifecycle. Fleet forces \`OURS_AUTOSTART=0\` in tmux and ACP child processes after role
322
- environment overlays. Start the shared daemon only through an explicit operator or
323
- installer/setup flow.
324
-
325
318
  ## Permissions
326
319
 
327
320
  Prefer the harness-neutral \`permissions\` block:
@@ -407,14 +400,10 @@ permissions through its harness and check the result against a fixed floor:
407
400
  deny those requests with nobody to see it; with \`unattended: wait\` it warns,
408
401
  because a human can still attach and answer.
409
402
 
410
- Security meaning: \`ask\` maps to Codex \`untrusted\` and Claude \`default\`.
411
- \`auto\` selects Codex ACP \`agent\` (\`on-request\` + \`workspace-write\`) and
412
- Claude \`acceptEdits\`. \`approval: allow\` selects Codex ACP's fully
413
- non-interactive yolo mode, reported as \`agent-full-access\` (\`never\` +
414
- \`danger-full-access\`), and Claude \`bypassPermissions\`. Codex tmux retains
415
- independent approval and sandbox flags: \`auto\` is \`on-request\`, \`allow\`
416
- is \`never\`, and \`filesystem\` still selects the sandbox. These modes genuinely
417
- permit the actions the role was authorized to take —
403
+ Security meaning: \`ask\` maps to Codex \`untrusted\` and Claude \`default\`;
404
+ \`auto\` maps to Codex \`on-request\` and Claude \`acceptEdits\`; and
405
+ \`approval: allow\` maps to Codex \`never\` and Claude \`bypassPermissions\`,
406
+ which genuinely permits the actions the role was authorized to take —
418
407
  \`dontAsk\` only suppresses the prompt while still refusing the action. Nothing
419
408
  other than an explicit \`allow\` becomes non-interactive. Legacy \`deny\` keeps
420
409
  its conservative Codex \`on-request\` / Claude \`plan\` translation. \`allow\` is therefore a real grant and
@@ -425,45 +414,20 @@ ACP carries agent-advertised session mode IDs and \`session/set_mode\`, but thos
425
414
  IDs are agent-specific and ACP defines no portable permission-policy capability.
426
415
  Fleet therefore uses the ACP primitive where an adapter exposes a matching mode
427
416
  and otherwise performs the harness translation above. The bundled Codex ACP
428
- adapter couples approval and sandboxing in its advertised mode IDs. Neutral
429
- \`allow\` therefore selects \`agent-full-access\` and widens \`filesystem:
430
- workspace\` or \`read-only\` to \`danger-full-access\`; neutral \`auto\` selects
431
- \`agent\` and \`workspace-write\` even when the neutral filesystem value differs.
432
- An explicit \`harness_options.sandbox\` selects its corresponding ACP preset and
433
- still wins, as does an explicit native approval override. \`config\` and
434
- \`doctor\` report a coupled-mode mismatch as approximate. Use per-role
435
- \`isolation:\` as the outer boundary for an \`allow\` ACP role. The live session
436
- reports both its effective normalized mode and the exact native mode selected.
417
+ adapter couples approval and sandboxing in its advertised mode IDs, so fleet
418
+ keeps the selected sandbox preset and enforces the independently translated
419
+ approval policy on the app-server turn request. For example, \`allow\` plus
420
+ \`workspace\` is really \`approval=never sandbox=workspace-write\`; it is never
421
+ widened to \`danger-full-access\`. The live session reports both its effective
422
+ normalized mode and the ACP sandbox-preset ID.
437
423
 
438
424
  See also: \`spawn --approval/--filesystem/--unattended\` set this intent at
439
425
  creation, and \`ours-fleet config\` prints each role's neutral settings, their
440
426
  native translation, and any warning — the same text \`doctor\` reports.
441
427
 
442
428
  Claude \`harness_options\`: \`permission_mode\` (default, acceptEdits, plan,
443
- dontAsk, bypassPermissions), \`plugins\`, \`mem_palace\`,
444
- \`mem_palace_midsession_autosave\`, \`mcp_servers\` and \`mcp_servers_only\`.
445
-
446
- \`mcp_servers\` declares MCP servers for the role, in \`.mcp.json\`'s own shape
447
- (a map of name to \`{ command, args, env }\`, or \`{ type: http|sse, url,
448
- headers }\`). By default they are ADDED to whatever the OS user running the role
449
- already has configured, on both session types: tmux passes \`--mcp-config\`, and
450
- ACP sends them in \`session/new\`.
451
-
452
- \`mcp_servers_only: true\` makes the declared set EXCLUSIVE — \`--strict-mcp-config\`
453
- on tmux, \`strictMcpConfig\` on ACP. It is all-or-nothing and it ignores every
454
- other MCP configuration: project \`.mcp.json\`, user settings, and **plugins**.
455
- The ours connector is normally installed as a plugin, so a strict role that does
456
- not re-declare it has no \`send_message\` and no \`get_messages\` — it cannot even
457
- report that it has gone mute. Fleet therefore refuses a strict role whose
458
- \`mcp_servers\` does not name the connector; declare it explicitly, e.g.
459
- \`ours: { command: ours-mcp, args: [proxy] }\`.
460
-
461
- Both options, and \`plugins\`, reach an ACP session through the bundled Claude ACP
462
- agent's \`_meta\` vocabulary. A role that sets \`session_options.acp.command\` runs
463
- an agent fleet did not choose and cannot be promised them, so that combination is
464
- refused at validation rather than accepted and dropped. This narrows a role's
465
- tool surface; it does not stop the harness deferring tool schemas, which is the
466
- harness's own decision.
429
+ dontAsk, bypassPermissions), \`plugins\`, \`mem_palace\`, and
430
+ \`mem_palace_midsession_autosave\`.
467
431
 
468
432
  Codex \`harness_options\`: \`launcher\` (auto, ours-codex, codex), \`sandbox\`
469
433
  (read-only, workspace-write, danger-full-access), \`approval\` or
@@ -521,8 +485,8 @@ Inspect \`ours-fleet status Name\`, \`peek Name\`, role logs, and
521
485
 
522
486
  ## Trusted owner channel
523
487
 
524
- An ACP role may declare a separate, existing ours identity which fleet — never
525
- the agent binds:
488
+ An ACP role may declare a separate ours identity which fleet — never the agent —
489
+ creates when missing and binds:
526
490
 
527
491
  \`\`\`yaml
528
492
  owner_channel:
@@ -541,6 +505,13 @@ owner_channel:
541
505
  allowed_mime: [application/pdf, text/plain, image/png, audio/ogg]
542
506
  \`\`\`
543
507
 
508
+ Permanent role identities are also reconciled before launch. Fleet creates a
509
+ missing role identity with local exposure and local auto-accept enabled. A
510
+ missing owner-channel identity uses the safer inverse policy: both are disabled.
511
+ The short provisioning lease is released before the agent or channel binds.
512
+ Temporary role identities remain connector-owned because their creating lease
513
+ defines their cleanup lifetime.
514
+
544
515
  This does not replace the role identity. Normal identity mail remains untrusted
545
516
  peer input: the agent reads it through \`get_messages\` and replies through
546
517
  \`send_message\`. Mail arriving on the dedicated channel from a CID in \`owners\`
@@ -551,11 +522,12 @@ owner of that authenticated source wire instead of the latest conversation.
551
522
  Every other CID is rejected and warned about without reflecting its body. Fleet sends
552
523
  accepted/queued/progress/interrupted/failure notices and routes the ACP turn's
553
524
  final assistant text back to the authenticated sender with its source wire ID.
554
- For file replies of every kind — a response artifact, a proactive note, or an
555
- in-turn attachment the agent calls ours \`send_file\` to the channel identity
556
- and may pair it with a reply-linked caption; fleet, not the agent, chooses the
557
- owner. That is the only delivery route an agent is given: a tool call either
558
- delivers or reports an error, where a file written to disk does neither.
525
+ For file replies, fleet injects a request-specific outbox path into the owner
526
+ prompt. The agent copies completed artifacts there; fleet sends every regular
527
+ file from the channel identity with the same source wire ID and removes the
528
+ temporary outbox only after successful delivery. For proactive or in-turn agent
529
+ attachments, the agent calls ours \`send_file\` to the channel identity and may
530
+ pair it with a reply-linked caption; fleet, not the agent, chooses the owner.
559
531
  Owner messages whose trimmed text starts with \`/\` are deterministic
560
532
  supervisor commands and never enter the model: \`/help\` (alias \`/commands\`),
561
533
  \`/status\`, \`/comments [status|on|off]\`, \`/interrupt\`, \`/clear\`,
@@ -588,13 +560,18 @@ their content signature must match the declared MIME, and symlinks or non-regula
588
560
  paths fail closed. Sanitized copies live only in a mode-0700 request directory as
589
561
  mode-0600 files and are removed after completion or bounded stale retention.
590
562
 
591
- Voice prompts include a bounded transcript only when ours-mcp reports success.
563
+ Voice prompts include a bounded transcript only when typed daemon metadata reports success.
592
564
  Failure or unavailability is explicit and preserves the private audio path as the
593
- fallback. Run \`ours-mcp voice-status --json\` to inspect the host configuration.
594
- A mode-0600 crash journal contains only authenticated CID and wire routing data;
595
- it never stores captions, filenames, paths, transcript text, or bytes. Journaled
596
- post-retrieval files resume selectively through \`save_file\`. A deferred agent
597
- caption is replayed with its processed files before the group is admitted. Fleet
565
+ input for direct review. Run \`ours config show --json\` and inspect \`sttConfigured\` without
566
+ revealing provider credentials.
567
+ A mode-0600 message claim journal stores only wire ID, persistent-history
568
+ sequence, and claim time. Fleet journals the exact body-free oldest-first slice
569
+ before calling \`getMessages\` with that slice length, rejects a returned set
570
+ mismatch, and loads a crash-recovered body only through \`getHistoryItem\`.
571
+ The attachment crash journal contains only authenticated CID and wire routing
572
+ data; it never stores captions, filenames, paths, transcript text, or bytes.
573
+ Journaled read files resume through \`getFileInfo\` and \`fetchFile\`. A claimed
574
+ agent caption is loaded from history and rejoined before the group is admitted. Fleet
598
575
  resolves one authenticated owner route before retrieving bytes, admits every file
599
576
  before emitting the caption or any file, and sends every part to that same route.
600
577
  Unknown correlated routes remain queued without retrieval and receive one bounded
@@ -697,38 +674,10 @@ escape hatch.
697
674
 
698
675
  ## Bounded worklogs, auth proxy, and model recovery
699
676
 
700
- WORKLOG rotation is enabled by default with
701
- \`worklog: { max_kb: 1024, keep_tail_kb: 256, max_archives: 12 }\`. Maps may
702
- override individual values; \`worklog: false\` on a role or in defaults opts out.
703
- Fleet rotates only at that role's launch/resume lifecycle boundary. Concurrent
704
- changes defer rotation. The active file keeps a bounded UTF-8 tail and advances
705
- to a line boundary when a complete line fits. If one logical line alone exceeds
706
- the budget, its newest suffix remains and the rotation manifest records the
707
- mid-line start and omitted byte count. The complete prior inode receives a
708
- collision-safe UTC archive name, and
709
- \`.worklog-rotation.json\` records restart provenance. \`max_archives\` bounds
710
- recent archives beside WORKLOG.md; older complete archives move to
711
- \`WORKLOG.archives/\` without deletion. All archives share the role's sensitive
712
- state boundary. Fleet refuses a symlinked/non-regular live log or a symlinked
713
- cold-archive boundary before replacing the live path and best-effort removes a
714
- duplicate publication left by a detected failure while the original inode is
715
- still available. The manifest records SHA-256 digests for the archive and live
716
- bytes observed when it is written. These checks address ordinary path hazards,
717
- not intentional path mutation by a malicious concurrent process with the same
718
- Unix authority; that is outside the threat model and requires OS-level isolation.
719
-
720
- ACP tool diffs are bounded before entering web conversation events. Existing
721
- small before/after diffs are unchanged. Oversized whole-file snapshots are
722
- reduced to the actual changed region plus path, operation, original byte counts,
723
- digest, and omission metadata. Each retained side is a newest-content UTF-8 tail
724
- of at most 64 KiB, advanced to a line boundary when a complete line fits. An
725
- overlong single line keeps its newest suffix and explicitly records a mid-line
726
- start. Paths retain at most a 4 KiB suffix with byte count, digest, and omitted
727
- prefix metadata; the complete normalized update is capped at 320 KiB. A large
728
- append therefore retains current appended content, not the historical prefix.
729
- The live web-console transcript includes only the current runner generation and
730
- excludes adapter session/load replay. Replayed events remain durable with
731
- agent_replay provenance for diagnosis and recovery.
677
+ An optional \`worklog: { max_kb, keep_tail_kb, max_archives }\` policy rotates a
678
+ stable snapshot at fleet-owned lifecycle points. Concurrent changes defer
679
+ rotation. Archives remain beside WORKLOG.md with the same sensitive-state
680
+ boundary; retention deletes only recognized fleet archive names.
732
681
 
733
682
  \`auth_proxy: { kind: anthropic, base_url, required, health_url }\` is Claude-only
734
683
  and loopback-only. Fleet injects only ANTHROPIC_BASE_URL and doctor rejects
package/dist/doctor.d.ts CHANGED
@@ -1,7 +1,10 @@
1
+ import { type AttachOursClientOptions, type OursClient } from '@ours.network/sdk/client';
1
2
  import { type Exec } from './exec.js';
2
3
  import type { YamlMode } from './config-yaml.js';
3
4
  import { type FetchLike } from './monitor.js';
4
5
  import type { PrereqReport } from './harness/types.js';
6
+ type DoctorDaemonClient = Pick<OursClient, 'version'>;
7
+ type AttachDoctorDaemon = (options: AttachOursClientOptions) => Promise<DoctorDaemonClient>;
5
8
  /** Host-level + per-harness prerequisite report with actionable messages. */
6
9
  export declare function doctor(opts?: {
7
10
  harness?: string;
@@ -12,4 +15,5 @@ export declare function doctor(opts?: {
12
15
  path?: string;
13
16
  argv1?: string;
14
17
  };
15
- }, exec?: Exec, platform?: NodeJS.Platform, fetchImpl?: FetchLike): Promise<PrereqReport>;
18
+ }, exec?: Exec, platform?: NodeJS.Platform, fetchImpl?: FetchLike, attachDaemon?: AttachDoctorDaemon): Promise<PrereqReport>;
19
+ export {};
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
@@ -112,16 +113,22 @@ export async function doctor(opts = {}, exec = realExec, platform = process.plat
112
113
  detail: tmux.code === 0 ? tmux.stdout.trim() : 'not found — apt install tmux / brew install tmux',
113
114
  });
114
115
  }
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']);
116
+ try {
117
+ const client = await attachDaemon({
118
+ env: process.env,
119
+ leaseToken: `ours-fleet-doctor-${process.pid}`,
120
+ clientPid: process.pid,
121
+ });
122
+ const info = await client.version();
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) {
122
128
  checks.push({
123
- name: 'ours-mcp daemon', ok: st.code === 0,
124
- detail: st.code === 0 ? 'running' : 'not running — start it with: ours-mcp start',
129
+ name: 'ours daemon', ok: false,
130
+ detail: `not reachable through the SDK — start it with: ours daemon start `
131
+ + `[${error?.message ?? String(error)}]`,
125
132
  });
126
133
  }
127
134
  if (platform === 'linux') {
@@ -288,7 +295,7 @@ export async function doctor(opts = {}, exec = realExec, platform = process.plat
288
295
  }
289
296
  catch (e) {
290
297
  detail = `unreachable on :${endpoint.port} — monitored roles run degraded until it is up ` +
291
- `(start it: ours-mcp start) [${e?.message ?? e}]`;
298
+ `(start it: ours daemon start) [${e?.message ?? e}]`;
292
299
  }
293
300
  checks.push({ name: checkName, ok, detail });
294
301
  }
@@ -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 { readFileSync, realpathSync, statSync } from 'node:fs';
3
- import { dirname, isAbsolute, relative, resolve, sep } from 'node:path';
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 declaredEntrypoint = typeof manifest.bin === 'string'
9
+ const relative = typeof manifest.bin === 'string'
10
10
  ? manifest.bin
11
11
  : manifest.bin?.[binName];
12
- if (!declaredEntrypoint)
12
+ if (!relative)
13
13
  return { argv: [fallbackCommand], bundled: false };
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())
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 \`ours-mcp watch "${id}"\` ` +
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, role) {
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 o = opts;
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
- settingsOverlay = join(dirs.stateDir, '.settings-overlay.json');
325
- writeFileSync(settingsOverlay, JSON.stringify({ enabledPlugins }, null, 2));
326
- 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);
327
195
  }
328
- // `harness_options.mcp_servers` the tmux delivery. `--mcp-config` ADDS the
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)}.`;
@@ -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;