@ours.network/fleet 0.19.0-nightly.8 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +96 -11
- package/dist/application/fleet-query-service.d.ts +1 -1
- package/dist/application/fleet-query-service.js +12 -0
- package/dist/application/role-creation-service.js +3 -1
- package/dist/application/types.d.ts +11 -0
- package/dist/briefing.d.ts +1 -1
- package/dist/briefing.js +103 -10
- package/dist/build-info.json +5 -5
- package/dist/cli.js +41 -11
- package/dist/config.d.ts +21 -7
- package/dist/config.js +32 -18
- package/dist/creation.d.ts +1 -1
- package/dist/docs.d.ts +2 -2
- package/dist/docs.js +145 -24
- package/dist/doctor.js +10 -10
- package/dist/duration.js +1 -1
- package/dist/fleet-proxy.d.ts +5 -0
- package/dist/harness/acp-agent.js +11 -6
- package/dist/harness/claude-code.js +201 -8
- package/dist/harness/codex.d.ts +4 -1
- package/dist/harness/codex.js +72 -13
- package/dist/harness/types.d.ts +57 -6
- package/dist/isolation/bubblewrap.d.ts +4 -4
- package/dist/isolation/bubblewrap.js +4 -4
- package/dist/isolation/policy.d.ts +1 -1
- package/dist/isolation/policy.js +2 -2
- package/dist/isolation/registry.d.ts +3 -3
- package/dist/isolation/registry.js +5 -5
- package/dist/isolation/resources.d.ts +1 -1
- package/dist/isolation/resources.js +1 -1
- package/dist/isolation/types.d.ts +3 -3
- package/dist/loops/manager.d.ts +30 -1
- package/dist/loops/manager.js +69 -6
- package/dist/loops/state.d.ts +18 -0
- package/dist/loops/state.js +4 -0
- package/dist/model-env.d.ts +71 -0
- package/dist/model-env.js +106 -0
- package/dist/monitor.d.ts +2 -2
- package/dist/monitor.js +4 -4
- package/dist/ops.d.ts +2 -2
- package/dist/ops.js +9 -9
- package/dist/owner-channel/channel.d.ts +17 -0
- package/dist/owner-channel/channel.js +104 -23
- package/dist/owner-channel/commands.d.ts +9 -0
- package/dist/owner-channel/commands.js +226 -99
- package/dist/owner-channel/notices.d.ts +7 -0
- package/dist/owner-channel/notices.js +9 -0
- package/dist/rooms-tasks/cli.js +733 -221
- package/dist/rooms-tasks/close.d.ts +35 -0
- package/dist/rooms-tasks/close.js +182 -0
- package/dist/rooms-tasks/config.js +8 -5
- package/dist/rooms-tasks/cowork-adapter.d.ts +22 -0
- package/dist/rooms-tasks/cowork-adapter.js +116 -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 +2 -0
- package/dist/rooms-tasks/index.js +2 -0
- package/dist/rooms-tasks/markdown.d.ts +49 -0
- package/dist/rooms-tasks/markdown.js +206 -0
- package/dist/rooms-tasks/member-startup.d.ts +51 -0
- package/dist/rooms-tasks/member-startup.js +151 -0
- package/dist/rooms-tasks/provision.d.ts +8 -0
- package/dist/rooms-tasks/provision.js +363 -97
- package/dist/rooms-tasks/room-state.d.ts +13 -1
- package/dist/rooms-tasks/room-state.js +149 -1
- package/dist/rooms-tasks/task-state.d.ts +13 -2
- package/dist/rooms-tasks/task-state.js +131 -3
- package/dist/rooms-tasks/terminal.d.ts +23 -0
- package/dist/rooms-tasks/terminal.js +56 -0
- package/dist/rooms-tasks/types.d.ts +109 -4
- package/dist/rooms-tasks/types.js +1 -1
- package/dist/runner.d.ts +48 -0
- package/dist/runner.js +252 -100
- package/dist/session/acp.d.ts +106 -2
- package/dist/session/acp.js +217 -14
- package/dist/session/activity.d.ts +31 -0
- package/dist/session/activity.js +48 -0
- package/dist/session/control.d.ts +1 -1
- package/dist/session/conversation-normalizer.d.ts +7 -1
- package/dist/session/conversation-normalizer.js +154 -11
- package/dist/session/conversation-store.js +1 -1
- package/dist/session/conversation-types.d.ts +25 -6
- package/dist/session/types.d.ts +35 -0
- package/dist/spawn.d.ts +4 -2
- package/dist/spawn.js +39 -26
- package/dist/supervisor/launchd.d.ts +2 -2
- package/dist/supervisor/launchd.js +4 -4
- package/dist/supervisor/systemd.js +4 -4
- package/dist/supervisor/types.d.ts +1 -1
- package/dist/temp-lifecycle.d.ts +14 -2
- package/dist/temp-lifecycle.js +90 -0
- package/dist/watchdog/alerts.d.ts +2 -2
- package/dist/watchdog/alerts.js +2 -2
- package/dist/watchdog/briefing.d.ts +5 -5
- package/dist/watchdog/briefing.js +13 -6
- package/dist/watchdog/config.js +1 -1
- package/dist/watchdog/query.d.ts +2 -2
- package/dist/watchdog/query.js +2 -2
- package/dist/watchdog/report.js +2 -2
- package/dist/watchdog/run.d.ts +2 -2
- package/dist/watchdog/run.js +6 -6
- package/dist/watchdog/scheduler.d.ts +7 -7
- package/dist/watchdog/scheduler.js +10 -10
- package/dist/watchdog/service.d.ts +4 -4
- package/dist/watchdog/service.js +5 -6
- package/dist/watchdog/store.d.ts +4 -4
- package/dist/watchdog/store.js +4 -4
- package/dist/web/runtime.js +1 -1
- package/dist/web/server.js +1 -1
- package/dist/web/topology-promote.js +1 -1
- package/dist/web-app/assets/{TerminalView-BAVk1Bot.js → TerminalView-C_G1ID2P.js} +1 -1
- package/dist/web-app/assets/{index-C3S-xFRU.js → index-BCBK78hw.js} +5 -5
- package/dist/web-app/index.html +1 -1
- package/dist/worklog.d.ts +7 -1
- package/dist/worklog.js +191 -39
- package/package.json +1 -1
package/dist/docs.js
CHANGED
|
@@ -271,6 +271,9 @@ roles:
|
|
|
271
271
|
add_dirs: [/data/shared]
|
|
272
272
|
config:
|
|
273
273
|
model_reasoning_effort: high
|
|
274
|
+
mcp_servers: # claude-code: per-role MCP servers, additive by default
|
|
275
|
+
ours: { command: ours-mcp, args: [proxy] }
|
|
276
|
+
mcp_servers_only: false # true = ONLY these; drops user/project/plugin servers
|
|
274
277
|
bio: Public role card and when peers should engage it.
|
|
275
278
|
persona: Local operating contract, boundaries, and escalation policy.
|
|
276
279
|
briefing_file: /absolute/custom-briefing.md
|
|
@@ -315,6 +318,68 @@ Role values override defaults. \`\${name}\` substitutes entries from \`vars\`.
|
|
|
315
318
|
Other role fields include \`max_tokens\`, \`autocompact_pct\`, and \`isolation\`.
|
|
316
319
|
Use README.md for the complete isolation policy and resource-cap schema.
|
|
317
320
|
|
|
321
|
+
Supervised roles connect to the operator-configured ours daemon; they do not own its
|
|
322
|
+
lifecycle. Fleet forces \`OURS_AUTOSTART=0\` in tmux and ACP child processes after role
|
|
323
|
+
environment overlays. Start the shared daemon only through an explicit operator or
|
|
324
|
+
installer/setup flow.
|
|
325
|
+
|
|
326
|
+
## Rooms and tasks
|
|
327
|
+
|
|
328
|
+
Rooms always use \`ours-cowork\`; there is no room-provider selector. Configure
|
|
329
|
+
the cowork daemon connection and room owner directly:
|
|
330
|
+
|
|
331
|
+
\`\`\`yaml
|
|
332
|
+
rooms:
|
|
333
|
+
cowork:
|
|
334
|
+
config: /home/me/.ours-cowork/config.json
|
|
335
|
+
owner:
|
|
336
|
+
expected_cid: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
|
|
337
|
+
public_invite_file: /home/me/.ours-fleet/owner-room-invite.txt
|
|
338
|
+
defaults:
|
|
339
|
+
template: team
|
|
340
|
+
attach_owner: true
|
|
341
|
+
close_when_task_done: true
|
|
342
|
+
tasks:
|
|
343
|
+
default_room_template: team
|
|
344
|
+
create_mode: start
|
|
345
|
+
close_room_on_done: true
|
|
346
|
+
\`\`\`
|
|
347
|
+
|
|
348
|
+
Human task and room results use the same compact Markdown presentation in the
|
|
349
|
+
CLI and authenticated owner channel: a short heading, icon-plus-word status,
|
|
350
|
+
code-formatted identifiers, bounded summaries, and actionable recovery or error
|
|
351
|
+
steps. Untrusted prose is context-escaped and control characters are neutralized;
|
|
352
|
+
Messenger-bound results are capped at 3,500 Unicode code points and 12,000 UTF-8
|
|
353
|
+
bytes with structural omission notices. \`--json\` bypasses this presentation layer
|
|
354
|
+
and retains the versioned machine schema and serialization order.
|
|
355
|
+
|
|
356
|
+
Older prerelease files with the exact legacy \`provider: cowork\` key under
|
|
357
|
+
\`rooms:\` still load, but the key is ignored and omitted from resolved
|
|
358
|
+
configuration. Remove it when editing the file. Any other legacy value is an
|
|
359
|
+
error. The optional \`rooms.owner.provider\` setting is separate and defaults to
|
|
360
|
+
\`messenger-server\`.
|
|
361
|
+
|
|
362
|
+
Finish and Delete are distinct terminal task actions:
|
|
363
|
+
|
|
364
|
+
\`ours-fleet task finish <id>\` moves an active or review task to \`done\` and
|
|
365
|
+
deletes its associated Cowork room after retiring its members. The room then
|
|
366
|
+
disappears from normal Fleet and Cowork views; its brief, messages, repository
|
|
367
|
+
references, and attachments are not retained as an inspectable archive.
|
|
368
|
+
The prerelease configuration names \`tasks.close_room_on_done\` and
|
|
369
|
+
\`rooms.defaults.close_when_task_done\` are retained for compatibility, but
|
|
370
|
+
\`true\` now means this close-then-delete behavior.
|
|
371
|
+
|
|
372
|
+
\`ours-fleet room delete <id> <id>\` is the canonical destructive room command.
|
|
373
|
+
\`room close <id> <id>\` remains a deprecated alias with identical deletion
|
|
374
|
+
semantics. Older prerelease \`closed\` room records are deleted directly the next
|
|
375
|
+
time \`room list\` reconciles Fleet with Cowork.
|
|
376
|
+
|
|
377
|
+
\`ours-fleet task delete <id> <id>\` removes only a \`done\` task's Fleet backlog
|
|
378
|
+
record. The exact task ID is required twice for confirmation. Delete rejects every
|
|
379
|
+
other task state, never changes any room state, and reports an already-missing
|
|
380
|
+
task as an idempotent no-op. The Owner-channel equivalent is
|
|
381
|
+
\`/task delete <id> <id>\`.
|
|
382
|
+
|
|
318
383
|
## Permissions
|
|
319
384
|
|
|
320
385
|
Prefer the harness-neutral \`permissions\` block:
|
|
@@ -400,10 +465,14 @@ permissions through its harness and check the result against a fixed floor:
|
|
|
400
465
|
deny those requests with nobody to see it; with \`unattended: wait\` it warns,
|
|
401
466
|
because a human can still attach and answer.
|
|
402
467
|
|
|
403
|
-
Security meaning: \`ask\` maps to Codex \`untrusted\` and Claude \`default
|
|
404
|
-
\`auto\`
|
|
405
|
-
\`approval: allow\`
|
|
406
|
-
|
|
468
|
+
Security meaning: \`ask\` maps to Codex \`untrusted\` and Claude \`default\`.
|
|
469
|
+
\`auto\` selects Codex ACP \`agent\` (\`on-request\` + \`workspace-write\`) and
|
|
470
|
+
Claude \`acceptEdits\`. \`approval: allow\` selects Codex ACP's fully
|
|
471
|
+
non-interactive yolo mode, reported as \`agent-full-access\` (\`never\` +
|
|
472
|
+
\`danger-full-access\`), and Claude \`bypassPermissions\`. Codex tmux retains
|
|
473
|
+
independent approval and sandbox flags: \`auto\` is \`on-request\`, \`allow\`
|
|
474
|
+
is \`never\`, and \`filesystem\` still selects the sandbox. These modes genuinely
|
|
475
|
+
permit the actions the role was authorized to take —
|
|
407
476
|
\`dontAsk\` only suppresses the prompt while still refusing the action. Nothing
|
|
408
477
|
other than an explicit \`allow\` becomes non-interactive. Legacy \`deny\` keeps
|
|
409
478
|
its conservative Codex \`on-request\` / Claude \`plan\` translation. \`allow\` is therefore a real grant and
|
|
@@ -414,20 +483,45 @@ ACP carries agent-advertised session mode IDs and \`session/set_mode\`, but thos
|
|
|
414
483
|
IDs are agent-specific and ACP defines no portable permission-policy capability.
|
|
415
484
|
Fleet therefore uses the ACP primitive where an adapter exposes a matching mode
|
|
416
485
|
and otherwise performs the harness translation above. The bundled Codex ACP
|
|
417
|
-
adapter couples approval and sandboxing in its advertised mode IDs
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
\`
|
|
421
|
-
|
|
422
|
-
|
|
486
|
+
adapter couples approval and sandboxing in its advertised mode IDs. Neutral
|
|
487
|
+
\`allow\` therefore selects \`agent-full-access\` and widens \`filesystem:
|
|
488
|
+
workspace\` or \`read-only\` to \`danger-full-access\`; neutral \`auto\` selects
|
|
489
|
+
\`agent\` and \`workspace-write\` even when the neutral filesystem value differs.
|
|
490
|
+
An explicit \`harness_options.sandbox\` selects its corresponding ACP preset and
|
|
491
|
+
still wins, as does an explicit native approval override. \`config\` and
|
|
492
|
+
\`doctor\` report a coupled-mode mismatch as approximate. Use per-role
|
|
493
|
+
\`isolation:\` as the outer boundary for an \`allow\` ACP role. The live session
|
|
494
|
+
reports both its effective normalized mode and the exact native mode selected.
|
|
423
495
|
|
|
424
496
|
See also: \`spawn --approval/--filesystem/--unattended\` set this intent at
|
|
425
497
|
creation, and \`ours-fleet config\` prints each role's neutral settings, their
|
|
426
498
|
native translation, and any warning — the same text \`doctor\` reports.
|
|
427
499
|
|
|
428
500
|
Claude \`harness_options\`: \`permission_mode\` (default, acceptEdits, plan,
|
|
429
|
-
dontAsk, bypassPermissions), \`plugins\`, \`mem_palace\`,
|
|
430
|
-
\`mem_palace_midsession_autosave\`.
|
|
501
|
+
dontAsk, bypassPermissions), \`plugins\`, \`mem_palace\`,
|
|
502
|
+
\`mem_palace_midsession_autosave\`, \`mcp_servers\` and \`mcp_servers_only\`.
|
|
503
|
+
|
|
504
|
+
\`mcp_servers\` declares MCP servers for the role, in \`.mcp.json\`'s own shape
|
|
505
|
+
(a map of name to \`{ command, args, env }\`, or \`{ type: http|sse, url,
|
|
506
|
+
headers }\`). By default they are ADDED to whatever the OS user running the role
|
|
507
|
+
already has configured, on both session types: tmux passes \`--mcp-config\`, and
|
|
508
|
+
ACP sends them in \`session/new\`.
|
|
509
|
+
|
|
510
|
+
\`mcp_servers_only: true\` makes the declared set EXCLUSIVE — \`--strict-mcp-config\`
|
|
511
|
+
on tmux, \`strictMcpConfig\` on ACP. It is all-or-nothing and it ignores every
|
|
512
|
+
other MCP configuration: project \`.mcp.json\`, user settings, and **plugins**.
|
|
513
|
+
The ours connector is normally installed as a plugin, so a strict role that does
|
|
514
|
+
not re-declare it has no \`send_message\` and no \`get_messages\` — it cannot even
|
|
515
|
+
report that it has gone mute. Fleet therefore refuses a strict role whose
|
|
516
|
+
\`mcp_servers\` does not name the connector; declare it explicitly, e.g.
|
|
517
|
+
\`ours: { command: ours-mcp, args: [proxy] }\`.
|
|
518
|
+
|
|
519
|
+
Both options, and \`plugins\`, reach an ACP session through the bundled Claude ACP
|
|
520
|
+
agent's \`_meta\` vocabulary. A role that sets \`session_options.acp.command\` runs
|
|
521
|
+
an agent fleet did not choose and cannot be promised them, so that combination is
|
|
522
|
+
refused at validation rather than accepted and dropped. This narrows a role's
|
|
523
|
+
tool surface; it does not stop the harness deferring tool schemas, which is the
|
|
524
|
+
harness's own decision.
|
|
431
525
|
|
|
432
526
|
Codex \`harness_options\`: \`launcher\` (auto, ours-codex, codex), \`sandbox\`
|
|
433
527
|
(read-only, workspace-write, danger-full-access), \`approval\` or
|
|
@@ -522,12 +616,11 @@ owner of that authenticated source wire instead of the latest conversation.
|
|
|
522
616
|
Every other CID is rejected and warned about without reflecting its body. Fleet sends
|
|
523
617
|
accepted/queued/progress/interrupted/failure notices and routes the ACP turn's
|
|
524
618
|
final assistant text back to the authenticated sender with its source wire ID.
|
|
525
|
-
For file replies
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
pair it with a reply-linked caption; fleet, not the agent, chooses the owner.
|
|
619
|
+
For file replies of every kind — a response artifact, a proactive note, or an
|
|
620
|
+
in-turn attachment — the agent calls ours \`send_file\` to the channel identity
|
|
621
|
+
and may pair it with a reply-linked caption; fleet, not the agent, chooses the
|
|
622
|
+
owner. That is the only delivery route an agent is given: a tool call either
|
|
623
|
+
delivers or reports an error, where a file written to disk does neither.
|
|
531
624
|
Owner messages whose trimmed text starts with \`/\` are deterministic
|
|
532
625
|
supervisor commands and never enter the model: \`/help\` (alias \`/commands\`),
|
|
533
626
|
\`/status\`, \`/comments [status|on|off]\`, \`/interrupt\`, \`/clear\`,
|
|
@@ -674,10 +767,38 @@ escape hatch.
|
|
|
674
767
|
|
|
675
768
|
## Bounded worklogs, auth proxy, and model recovery
|
|
676
769
|
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
770
|
+
WORKLOG rotation is enabled by default with
|
|
771
|
+
\`worklog: { max_kb: 1024, keep_tail_kb: 256, max_archives: 12 }\`. Maps may
|
|
772
|
+
override individual values; \`worklog: false\` on a role or in defaults opts out.
|
|
773
|
+
Fleet rotates only at that role's launch/resume lifecycle boundary. Concurrent
|
|
774
|
+
changes defer rotation. The active file keeps a bounded UTF-8 tail and advances
|
|
775
|
+
to a line boundary when a complete line fits. If one logical line alone exceeds
|
|
776
|
+
the budget, its newest suffix remains and the rotation manifest records the
|
|
777
|
+
mid-line start and omitted byte count. The complete prior inode receives a
|
|
778
|
+
collision-safe UTC archive name, and
|
|
779
|
+
\`.worklog-rotation.json\` records restart provenance. \`max_archives\` bounds
|
|
780
|
+
recent archives beside WORKLOG.md; older complete archives move to
|
|
781
|
+
\`WORKLOG.archives/\` without deletion. All archives share the role's sensitive
|
|
782
|
+
state boundary. Fleet refuses a symlinked/non-regular live log or a symlinked
|
|
783
|
+
cold-archive boundary before replacing the live path and best-effort removes a
|
|
784
|
+
duplicate publication left by a detected failure while the original inode is
|
|
785
|
+
still available. The manifest records SHA-256 digests for the archive and live
|
|
786
|
+
bytes observed when it is written. These checks address ordinary path hazards,
|
|
787
|
+
not intentional path mutation by a malicious concurrent process with the same
|
|
788
|
+
Unix authority; that is outside the threat model and requires OS-level isolation.
|
|
789
|
+
|
|
790
|
+
ACP tool diffs are bounded before entering web conversation events. Existing
|
|
791
|
+
small before/after diffs are unchanged. Oversized whole-file snapshots are
|
|
792
|
+
reduced to the actual changed region plus path, operation, original byte counts,
|
|
793
|
+
digest, and omission metadata. Each retained side is a newest-content UTF-8 tail
|
|
794
|
+
of at most 64 KiB, advanced to a line boundary when a complete line fits. An
|
|
795
|
+
overlong single line keeps its newest suffix and explicitly records a mid-line
|
|
796
|
+
start. Paths retain at most a 4 KiB suffix with byte count, digest, and omitted
|
|
797
|
+
prefix metadata; the complete normalized update is capped at 320 KiB. A large
|
|
798
|
+
append therefore retains current appended content, not the historical prefix.
|
|
799
|
+
The live web-console transcript includes only the current runner generation and
|
|
800
|
+
excludes adapter session/load replay. Replayed events remain durable with
|
|
801
|
+
agent_replay provenance for diagnosis and recovery.
|
|
681
802
|
|
|
682
803
|
\`auth_proxy: { kind: anthropic, base_url, required, health_url }\` is Claude-only
|
|
683
804
|
and loopback-only. Fleet injects only ANTHROPIC_BASE_URL and doctor rejects
|
|
@@ -694,7 +815,7 @@ fail-closed and held down. Change the declared chain/model and restart to
|
|
|
694
815
|
reconcile explicitly; no chain preserves detection-only behavior.
|
|
695
816
|
`;
|
|
696
817
|
/**
|
|
697
|
-
* What every shipped spawn-skill variant must say, and must not say
|
|
818
|
+
* What every shipped spawn-skill variant must say, and must not say.
|
|
698
819
|
*
|
|
699
820
|
* The skills are separate markdown files in two published plugins, written for
|
|
700
821
|
* two different harnesses, so they cannot literally be one file. This is the
|
|
@@ -724,7 +845,7 @@ export const SPAWN_SKILL_CONTRACT = {
|
|
|
724
845
|
// The only intent that clears the floor, and the honest alternative.
|
|
725
846
|
'--approval allow',
|
|
726
847
|
'--unattended wait',
|
|
727
|
-
// Creation-time isolation
|
|
848
|
+
// Creation-time isolation — the one new operator input this release adds.
|
|
728
849
|
'--isolation-file',
|
|
729
850
|
],
|
|
730
851
|
/**
|
package/dist/doctor.js
CHANGED
|
@@ -106,7 +106,7 @@ export async function doctor(opts = {}, exec = realExec, platform = process.plat
|
|
|
106
106
|
ok: true,
|
|
107
107
|
detail: `warning: ${diagnostic.message}`,
|
|
108
108
|
});
|
|
109
|
-
// ── Rooms/tasks checks
|
|
109
|
+
// ── Rooms/tasks checks ────────────────────────────────────────
|
|
110
110
|
if (loaded.ok && loaded.rooms) {
|
|
111
111
|
const rooms = loaded.rooms;
|
|
112
112
|
// Cowork socket reachability
|
|
@@ -183,7 +183,7 @@ export async function doctor(opts = {}, exec = realExec, platform = process.plat
|
|
|
183
183
|
: `cowork config not found: ${rooms.cowork.config}`,
|
|
184
184
|
});
|
|
185
185
|
}
|
|
186
|
-
// Hard
|
|
186
|
+
// Hard room-management capability check
|
|
187
187
|
try {
|
|
188
188
|
const { createCoworkAdapter } = await import('./rooms-tasks/cowork-adapter.js');
|
|
189
189
|
const adapter = createCoworkAdapter({ configPath: coworkConfig });
|
|
@@ -199,7 +199,7 @@ export async function doctor(opts = {}, exec = realExec, platform = process.plat
|
|
|
199
199
|
checks.push({
|
|
200
200
|
name: 'rooms: capability', ok: false,
|
|
201
201
|
detail: isProto
|
|
202
|
-
? `cowork does not support room management protocol — upgrade ours-cowork to
|
|
202
|
+
? `cowork does not support room management protocol — upgrade ours-cowork to a version that supports the room management protocol`
|
|
203
203
|
: `room management check failed: ${msg}`,
|
|
204
204
|
});
|
|
205
205
|
}
|
|
@@ -271,7 +271,7 @@ export async function doctor(opts = {}, exec = realExec, platform = process.plat
|
|
|
271
271
|
: `no XDG_RUNTIME_DIR and /run/user/<uid> missing — systemctl --user cannot reach the user manager; enable linger: sudo loginctl enable-linger ${user}`,
|
|
272
272
|
});
|
|
273
273
|
}
|
|
274
|
-
// Per-role permission translation
|
|
274
|
+
// Per-role permission translation. Rendered from the same analysis the
|
|
275
275
|
// `config` command prints, so the two commands cannot disagree.
|
|
276
276
|
for (const analysis of analyzeFleetPermissions(roles)) {
|
|
277
277
|
if (!analysis.supported) {
|
|
@@ -290,13 +290,13 @@ export async function doctor(opts = {}, exec = realExec, platform = process.plat
|
|
|
290
290
|
? `${summary} — ${analysis.warnings.join('; ')}`
|
|
291
291
|
: `${summary} (exact)`,
|
|
292
292
|
});
|
|
293
|
-
//
|
|
294
|
-
//
|
|
293
|
+
// Report a role that states its permission intent twice in disagreeing places.
|
|
294
|
+
// Stay quiet when there is a single source of intent.
|
|
295
295
|
for (const conflict of analysis.conflicts ?? [])
|
|
296
296
|
checks.push({
|
|
297
297
|
name: `permission conflict: ${analysis.role}`, ok: true, detail: conflict.warning,
|
|
298
298
|
});
|
|
299
|
-
// The floor is checked BEFORE start
|
|
299
|
+
// The floor is checked BEFORE start: an under-permissioned unattended
|
|
300
300
|
// role never reports its own failure, because the denial happens inside the
|
|
301
301
|
// harness with nobody attached to see it.
|
|
302
302
|
const floor = analysis.floor;
|
|
@@ -342,7 +342,7 @@ export async function doctor(opts = {}, exec = realExec, platform = process.plat
|
|
|
342
342
|
});
|
|
343
343
|
}
|
|
344
344
|
// Isolation reporting (AC-9). Backend availability is advisory — isolation is
|
|
345
|
-
// opt-in per role
|
|
345
|
+
// opt-in per role, so a missing bwrap must not fail doctor for fleets that
|
|
346
346
|
// don't use it. Only a role that DECLARES isolation and cannot get it under
|
|
347
347
|
// `strict` is a hard failure.
|
|
348
348
|
const bw = await makeBubblewrapBackend(exec).available();
|
|
@@ -355,7 +355,7 @@ export async function doctor(opts = {}, exec = realExec, platform = process.plat
|
|
|
355
355
|
if (platform === 'linux')
|
|
356
356
|
checks.push({ name: 'isolation: cgroup delegation', ok: true, detail: cgroupDelegationDetail() });
|
|
357
357
|
for (const r of roles.filter(r => r.isolation)) {
|
|
358
|
-
// A refused mount is a launch-blocking policy error
|
|
358
|
+
// A refused mount is a launch-blocking policy error, not a warning.
|
|
359
359
|
let policy;
|
|
360
360
|
try {
|
|
361
361
|
policy = resolveIsolation(r.isolation, isolationContextFor(r));
|
|
@@ -385,7 +385,7 @@ export async function doctor(opts = {}, exec = realExec, platform = process.plat
|
|
|
385
385
|
detail = `backend=${policy.backend} (not yet implemented)`;
|
|
386
386
|
checks.push({ name: `isolation: ${r.name}`, ok, detail });
|
|
387
387
|
}
|
|
388
|
-
//
|
|
388
|
+
// Probe monitor daemon-API reachability only when a role is supervised.
|
|
389
389
|
// /state-dir is unauthenticated (liveness); /identities exercises the token so a
|
|
390
390
|
// shared-mode misconfig (401) surfaces here rather than as a silent deaf monitor.
|
|
391
391
|
const monitorProfiles = resolveMonitorProfiles(roles);
|
package/dist/duration.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** Parse `30s | 10m | 2h` duration strings to milliseconds
|
|
1
|
+
/** Parse `30s | 10m | 2h` duration strings to milliseconds. */
|
|
2
2
|
const UNIT_MS = { s: 1_000, m: 60_000, h: 3_600_000, d: 86_400_000 };
|
|
3
3
|
const RE = /^(\d+)([smhd])$/;
|
|
4
4
|
export function parseDuration(text, opts = {}) {
|
package/dist/fleet-proxy.d.ts
CHANGED
|
@@ -12,6 +12,11 @@ export interface ManagedFleetSpawnResult {
|
|
|
12
12
|
session: 'tmux' | 'acp';
|
|
13
13
|
model?: string;
|
|
14
14
|
monitor: Pick<MonitorConfig, 'mode' | 'interrupt'>;
|
|
15
|
+
/** Adapter-resolved portable policy and exact native runtime mode. */
|
|
16
|
+
permissionMode?: {
|
|
17
|
+
fleetMode: 'ask' | 'auto' | 'allow';
|
|
18
|
+
nativeMode: string;
|
|
19
|
+
};
|
|
15
20
|
inherited: string[];
|
|
16
21
|
creationActionId: string;
|
|
17
22
|
}
|
|
@@ -1,18 +1,23 @@
|
|
|
1
1
|
import { createRequire } from 'node:module';
|
|
2
|
-
import {
|
|
3
|
-
import { dirname, resolve } from 'node:path';
|
|
2
|
+
import { readFileSync, realpathSync, statSync } from 'node:fs';
|
|
3
|
+
import { dirname, isAbsolute, relative, resolve, sep } from 'node:path';
|
|
4
4
|
const require = createRequire(import.meta.url);
|
|
5
5
|
export function resolveBundledAcpAgent(packageName, binName, fallbackCommand) {
|
|
6
6
|
try {
|
|
7
7
|
const manifestPath = require.resolve(`${packageName}/package.json`);
|
|
8
8
|
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
9
|
-
const
|
|
9
|
+
const declaredEntrypoint = typeof manifest.bin === 'string'
|
|
10
10
|
? manifest.bin
|
|
11
11
|
: manifest.bin?.[binName];
|
|
12
|
-
if (!
|
|
12
|
+
if (!declaredEntrypoint)
|
|
13
13
|
return { argv: [fallbackCommand], bundled: false };
|
|
14
|
-
const
|
|
15
|
-
|
|
14
|
+
const packageRoot = realpathSync(dirname(manifestPath));
|
|
15
|
+
const entrypoint = realpathSync(resolve(packageRoot, declaredEntrypoint));
|
|
16
|
+
const entrypointFromRoot = relative(packageRoot, entrypoint);
|
|
17
|
+
if (entrypointFromRoot === '..'
|
|
18
|
+
|| entrypointFromRoot.startsWith(`..${sep}`)
|
|
19
|
+
|| isAbsolute(entrypointFromRoot)
|
|
20
|
+
|| !statSync(entrypoint).isFile())
|
|
16
21
|
return { argv: [fallbackCommand], bundled: false };
|
|
17
22
|
return {
|
|
18
23
|
argv: [process.execPath, entrypoint], bundled: true, manifestPath,
|
|
@@ -6,8 +6,98 @@ 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 = [
|
|
9
|
+
const OPTION_KEYS = [
|
|
10
|
+
'plugins', 'mem_palace', 'mem_palace_midsession_autosave', 'permission_mode', 'effort',
|
|
11
|
+
'mcp_servers', 'mcp_servers_only',
|
|
12
|
+
];
|
|
10
13
|
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
|
+
}
|
|
11
101
|
/** Claude Code's accepted --permission-mode values. */
|
|
12
102
|
const PERMISSION_MODES = ['default', 'acceptEdits', 'plan', 'dontAsk', 'bypassPermissions'];
|
|
13
103
|
/**
|
|
@@ -149,7 +239,7 @@ export function makeClaudeCodeAdapter(exec = realExec) {
|
|
|
149
239
|
}],
|
|
150
240
|
};
|
|
151
241
|
},
|
|
152
|
-
validateOptions(opts) {
|
|
242
|
+
validateOptions(opts, role) {
|
|
153
243
|
if (opts == null)
|
|
154
244
|
return [];
|
|
155
245
|
if (typeof opts !== 'object' || Array.isArray(opts))
|
|
@@ -157,9 +247,44 @@ export function makeClaudeCodeAdapter(exec = realExec) {
|
|
|
157
247
|
const errors = Object.keys(opts)
|
|
158
248
|
.filter(k => !OPTION_KEYS.includes(k))
|
|
159
249
|
.map(k => ({ path: `harness_options.${k}`, message: `unknown option; allowed: ${OPTION_KEYS.join(', ')}` }));
|
|
160
|
-
const
|
|
250
|
+
const o = opts;
|
|
251
|
+
const effort = o.effort;
|
|
161
252
|
if (effort != null && !EFFORT_LEVELS.includes(effort))
|
|
162
253
|
errors.push({ path: 'harness_options.effort', message: `must be one of: ${EFFORT_LEVELS.join(', ')}` });
|
|
254
|
+
if (o.mcp_servers_only != null && typeof o.mcp_servers_only !== 'boolean')
|
|
255
|
+
errors.push({ path: 'harness_options.mcp_servers_only', message: 'must be a boolean' });
|
|
256
|
+
errors.push(...validateMcpServers(o.mcp_servers));
|
|
257
|
+
if (o.mcp_servers_only === true && !o.mcp_servers)
|
|
258
|
+
errors.push({
|
|
259
|
+
path: 'harness_options.mcp_servers_only',
|
|
260
|
+
message: 'requires harness_options.mcp_servers; on its own it would leave the role with no MCP servers at all',
|
|
261
|
+
});
|
|
262
|
+
// The muteness gate. Only when the declared set is otherwise well-formed —
|
|
263
|
+
// a shape error already told the operator to look here.
|
|
264
|
+
if (o.mcp_servers_only === true && o.mcp_servers && errors.length === 0
|
|
265
|
+
&& !declaresOursConnector(o.mcp_servers))
|
|
266
|
+
errors.push({
|
|
267
|
+
path: 'harness_options.mcp_servers',
|
|
268
|
+
message: 'mcp_servers_only ignores every other MCP configuration, INCLUDING plugins — and the ours '
|
|
269
|
+
+ 'connector is normally a plugin, so this role would have no send_message or get_messages and no way '
|
|
270
|
+
+ 'to report that. Declare it explicitly, e.g. ours: { command: ours-mcp, args: [proxy] }',
|
|
271
|
+
});
|
|
272
|
+
// Session-aware refusals. Both options reach an ACP session through the
|
|
273
|
+
// bundled agent's `_meta` vocabulary, so a role that launches a DIFFERENT
|
|
274
|
+
// ACP agent cannot be promised either one. Refuse rather than send it and
|
|
275
|
+
// hope: silently dropping the config is the defect being fixed here.
|
|
276
|
+
if (role && customAcpCommand(role)) {
|
|
277
|
+
for (const key of ['plugins', 'mcp_servers', 'mcp_servers_only']) {
|
|
278
|
+
if (o[key] == null)
|
|
279
|
+
continue;
|
|
280
|
+
errors.push({
|
|
281
|
+
path: `harness_options.${key}`,
|
|
282
|
+
message: 'cannot be honoured with session_options.acp.command: it is delivered through the bundled '
|
|
283
|
+
+ 'Claude ACP agent\'s _meta vocabulary, which another agent has no reason to read. Drop the '
|
|
284
|
+
+ 'custom ACP command, or drop this option',
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
}
|
|
163
288
|
return errors;
|
|
164
289
|
},
|
|
165
290
|
async prepareSession(role, dirs) {
|
|
@@ -178,22 +303,48 @@ export function makeClaudeCodeAdapter(exec = realExec) {
|
|
|
178
303
|
CLAUDE_AUTOCOMPACT_PCT_OVERRIDE: String(autocompactPct(role)),
|
|
179
304
|
MEMPALACE_HOOKS_AUTO_SAVE: 'false',
|
|
180
305
|
MEMPALACE_MIDSESSION_AUTOSAVE: o.mem_palace_midsession_autosave ? 'true' : 'false',
|
|
306
|
+
// The role's identity, for the ours connector to bind at startup instead of
|
|
307
|
+
// the briefing telling the MODEL to call choose_identity. Both launches
|
|
308
|
+
// return `prep.env`, so this one line covers tmux and ACP alike.
|
|
309
|
+
//
|
|
310
|
+
// The bind the connector performs is PLAIN and fail-closed: it can never
|
|
311
|
+
// evict a live session, and a role whose identity does not exist yet simply
|
|
312
|
+
// boots unbound and falls through to the briefing's create-if-missing step.
|
|
313
|
+
// Nothing here may ever grow a force flag.
|
|
314
|
+
OURS_BIND_IDENTITY: role.identity,
|
|
181
315
|
};
|
|
182
316
|
if (!memPalace)
|
|
183
317
|
env.MEMPALACE_DISABLED = 'true';
|
|
184
|
-
// Per-role harness runtime home
|
|
318
|
+
// Per-role harness runtime home. Created before sandbox entry so the
|
|
185
319
|
// bind has something to mount; harmless for un-isolated roles.
|
|
186
320
|
// Only a role that declares `isolation:` gets a sandbox, and only a
|
|
187
321
|
// sandbox needs this directory to exist before entry.
|
|
188
322
|
if (role.isolation)
|
|
189
323
|
mkdirSync(harnessRuntimeDir(dirs.stateDir, 'claude'), { recursive: true });
|
|
190
324
|
const argv = [];
|
|
325
|
+
let settingsOverlay;
|
|
191
326
|
if (Object.keys(enabledPlugins).length) {
|
|
192
|
-
|
|
193
|
-
writeFileSync(
|
|
194
|
-
argv.push('--settings',
|
|
327
|
+
settingsOverlay = join(dirs.stateDir, '.settings-overlay.json');
|
|
328
|
+
writeFileSync(settingsOverlay, JSON.stringify({ enabledPlugins }, null, 2));
|
|
329
|
+
argv.push('--settings', settingsOverlay);
|
|
195
330
|
}
|
|
196
|
-
|
|
331
|
+
// `harness_options.mcp_servers` — the tmux delivery. `--mcp-config` ADDS the
|
|
332
|
+
// file's servers; `--strict-mcp-config` is what makes the set exclusive, and
|
|
333
|
+
// it is opt-in per role because it drops everything else the user has,
|
|
334
|
+
// plugins included (see `declaresOursConnector`).
|
|
335
|
+
let mcpConfigFile;
|
|
336
|
+
if (o.mcp_servers) {
|
|
337
|
+
mcpConfigFile = join(dirs.stateDir, '.mcp-config.json');
|
|
338
|
+
writeFileSync(mcpConfigFile, JSON.stringify({ mcpServers: o.mcp_servers }, null, 2), { mode: 0o600 });
|
|
339
|
+
argv.push('--mcp-config', mcpConfigFile);
|
|
340
|
+
if (o.mcp_servers_only === true)
|
|
341
|
+
argv.push('--strict-mcp-config');
|
|
342
|
+
}
|
|
343
|
+
return {
|
|
344
|
+
argv, env,
|
|
345
|
+
...(settingsOverlay ? { settingsOverlay } : {}),
|
|
346
|
+
...(mcpConfigFile ? { mcpConfigFile } : {}),
|
|
347
|
+
};
|
|
197
348
|
},
|
|
198
349
|
buildLaunch(role, mode, s, prep) {
|
|
199
350
|
const stateDir = roleStateDir(role);
|
|
@@ -223,6 +374,46 @@ export function makeClaudeCodeAdapter(exec = realExec) {
|
|
|
223
374
|
acpPermissionModeId(role) {
|
|
224
375
|
return permissionMode(role);
|
|
225
376
|
},
|
|
377
|
+
/**
|
|
378
|
+
* Deliver, over ACP, the two things the tmux launch delivers as flags.
|
|
379
|
+
*
|
|
380
|
+
* `buildAcpLaunch` builds its own argv and cannot carry `prep.argv`: the
|
|
381
|
+
* process it launches is the ACP agent, not `claude`, and it takes none of
|
|
382
|
+
* claude's flags. That is why `harness_options.plugins` did nothing at all on
|
|
383
|
+
* an ACP role — the overlay was written and then dropped, and the mem-palace
|
|
384
|
+
* toggle rode `prep.env` and survived, so the failure was silent AND
|
|
385
|
+
* selective.
|
|
386
|
+
*
|
|
387
|
+
* `_meta.claudeCode.options` is the bundled agent's own passthrough into the
|
|
388
|
+
* Claude Agent SDK (@agentclientprotocol/claude-agent-acp, acp-agent.js:4092
|
|
389
|
+
* → the `options` object at :4144). `settings` takes the same overlay path
|
|
390
|
+
* `--settings` takes; `strictMcpConfig` is the SDK's spelling of
|
|
391
|
+
* `--strict-mcp-config`. Both are spread BEFORE the fields the agent forces,
|
|
392
|
+
* so neither is overwritten.
|
|
393
|
+
*
|
|
394
|
+
* ⚠ RETURNS NOTHING FOR A ROLE THAT NAMES ITS OWN ACP COMMAND. That process
|
|
395
|
+
* is not the bundled agent and has no reason to read this vocabulary; sending
|
|
396
|
+
* it anyway would be the silent drop again, one level down. `validateOptions`
|
|
397
|
+
* refuses those roles instead.
|
|
398
|
+
*/
|
|
399
|
+
acpSessionMeta(role, prep) {
|
|
400
|
+
if (customAcpCommand(role))
|
|
401
|
+
return undefined;
|
|
402
|
+
const options = {};
|
|
403
|
+
if (prep.settingsOverlay)
|
|
404
|
+
options.settings = prep.settingsOverlay;
|
|
405
|
+
if (role.harness_options?.mcp_servers_only === true)
|
|
406
|
+
options.strictMcpConfig = true;
|
|
407
|
+
return Object.keys(options).length ? { claudeCode: { options } } : undefined;
|
|
408
|
+
},
|
|
409
|
+
/**
|
|
410
|
+
* The declared servers, in ACP's array shape. Sent on `session/new` and on
|
|
411
|
+
* resume/load, because the SDK builds its server set once per session and a
|
|
412
|
+
* resumed session that dropped them would quietly lose its tools.
|
|
413
|
+
*/
|
|
414
|
+
acpMcpServers(role) {
|
|
415
|
+
return acpMcpServersFor(role.harness_options?.mcp_servers);
|
|
416
|
+
},
|
|
226
417
|
isolationPaths(_role, _dirs) {
|
|
227
418
|
const claudeHome = join(home(), '.claude');
|
|
228
419
|
return {
|
|
@@ -285,6 +476,8 @@ export function makeClaudeCodeAdapter(exec = realExec) {
|
|
|
285
476
|
currentIdentityTool: 'current_identity',
|
|
286
477
|
sendTool: 'send_message',
|
|
287
478
|
getMessagesTool: 'get_messages',
|
|
479
|
+
listHistoryTool: 'list_history',
|
|
480
|
+
getHistoryItemTool: 'get_history_item',
|
|
288
481
|
monitorInstruction: id => {
|
|
289
482
|
const m = armMonitor(id);
|
|
290
483
|
return `${m.charAt(0).toUpperCase()}${m.slice(1)}.`;
|
package/dist/harness/codex.d.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { type Exec } from '../exec.js';
|
|
2
|
-
import type { HarnessAdapter, UnattendedCapability } from './types.js';
|
|
2
|
+
import type { AcpLaunch, HarnessAdapter, UnattendedCapability } from './types.js';
|
|
3
|
+
import { type AcpAgentResolution } from './acp-agent.js';
|
|
3
4
|
/**
|
|
4
5
|
* What an unattended role can actually do under Codex's native settings.
|
|
5
6
|
* `on-request` and `untrusted` stop to ask, and with no console attached that
|
|
6
7
|
* request is refused rather than answered — so the role can only read.
|
|
7
8
|
*/
|
|
8
9
|
export declare function codexCapabilities(approval: string, sandbox: string): UnattendedCapability[];
|
|
10
|
+
/** Bind launch argv and metadata provenance to one already-completed resolution. */
|
|
11
|
+
export declare function codexAcpLaunchForResolution(resolution: AcpAgentResolution): Pick<AcpLaunch, 'argv' | 'permissionMetadataSource'>;
|
|
9
12
|
export declare function makeCodexAdapter(exec?: Exec): HarnessAdapter;
|
|
10
13
|
export declare const codexAdapter: HarnessAdapter;
|