@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.
- package/README.md +57 -91
- package/dist/application/fleet-query-service.js +0 -12
- package/dist/application/role-creation-service.js +10 -7
- package/dist/application/types.d.ts +0 -11
- package/dist/briefing.js +6 -15
- package/dist/build-info.json +5 -5
- package/dist/cli.js +12 -37
- package/dist/config.d.ts +8 -6
- package/dist/config.js +46 -30
- package/dist/creation.d.ts +38 -22
- package/dist/creation.js +111 -24
- package/dist/docs.d.ts +1 -1
- package/dist/docs.js +44 -95
- package/dist/doctor.d.ts +5 -1
- package/dist/doctor.js +18 -11
- package/dist/fleet-proxy.d.ts +0 -5
- package/dist/harness/acp-agent.js +6 -11
- package/dist/harness/claude-code.js +11 -200
- package/dist/harness/codex.d.ts +1 -4
- package/dist/harness/codex.js +12 -70
- package/dist/harness/types.d.ts +4 -54
- package/dist/loops/manager.d.ts +1 -30
- package/dist/loops/manager.js +6 -69
- package/dist/loops/state.d.ts +0 -18
- package/dist/loops/state.js +0 -4
- package/dist/monitor.js +1 -1
- package/dist/ops.d.ts +3 -0
- package/dist/ops.js +8 -3
- package/dist/owner-channel/attachments.d.ts +25 -2
- package/dist/owner-channel/attachments.js +61 -5
- package/dist/owner-channel/channel.d.ts +29 -30
- package/dist/owner-channel/channel.js +291 -291
- package/dist/owner-channel/commands.js +89 -0
- package/dist/owner-channel/message-recovery.d.ts +25 -0
- package/dist/owner-channel/message-recovery.js +114 -0
- package/dist/owner-channel/notices.d.ts +0 -7
- package/dist/owner-channel/notices.js +0 -9
- package/dist/owner-channel/ours-client.d.ts +148 -0
- package/dist/owner-channel/ours-client.js +231 -0
- package/dist/rooms-tasks/cli.d.ts +4 -0
- package/dist/rooms-tasks/cli.js +565 -0
- package/dist/rooms-tasks/config.d.ts +15 -0
- package/dist/rooms-tasks/config.js +171 -0
- package/dist/rooms-tasks/cowork-adapter.d.ts +80 -0
- package/dist/rooms-tasks/cowork-adapter.js +48 -0
- package/dist/rooms-tasks/index.d.ts +7 -0
- package/dist/rooms-tasks/index.js +7 -0
- package/dist/rooms-tasks/room-state.d.ts +22 -0
- package/dist/rooms-tasks/room-state.js +117 -0
- package/dist/rooms-tasks/task-state.d.ts +31 -0
- package/dist/rooms-tasks/task-state.js +173 -0
- package/dist/rooms-tasks/templates.d.ts +6 -0
- package/dist/rooms-tasks/templates.js +80 -0
- package/dist/rooms-tasks/types.d.ts +153 -0
- package/dist/rooms-tasks/types.js +20 -0
- package/dist/runner.d.ts +0 -48
- package/dist/runner.js +94 -236
- package/dist/session/acp.d.ts +0 -104
- package/dist/session/acp.js +10 -213
- package/dist/session/conversation-normalizer.d.ts +0 -6
- package/dist/session/conversation-normalizer.js +10 -153
- package/dist/session/conversation-types.d.ts +4 -23
- package/dist/session/types.d.ts +0 -35
- package/dist/spawn.js +26 -33
- package/dist/supervisor/systemd.js +29 -2
- package/dist/watchdog/briefing.js +0 -7
- package/dist/watchdog/run.js +3 -3
- package/dist/web-app/assets/{TerminalView-C_G1ID2P.js → TerminalView-BAVk1Bot.js} +1 -1
- package/dist/web-app/assets/{index-BCBK78hw.js → index-C3S-xFRU.js} +5 -5
- package/dist/web-app/index.html +1 -1
- package/dist/worklog.d.ts +1 -7
- package/dist/worklog.js +39 -191
- package/package.json +3 -1
- package/dist/model-env.d.ts +0 -71
- package/dist/model-env.js +0 -106
- package/dist/owner-channel/mcp.d.ts +0 -24
- package/dist/owner-channel/mcp.js +0 -145
- package/dist/session/activity.d.ts +0 -31
- package/dist/session/activity.js +0 -48
package/dist/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
|
-
|
|
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\`
|
|
412
|
-
|
|
413
|
-
|
|
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
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
\`
|
|
432
|
-
|
|
433
|
-
|
|
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
|
|
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
|
|
525
|
-
|
|
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
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
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
|
|
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
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
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
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const
|
|
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
|
|
124
|
-
detail:
|
|
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
|
|
298
|
+
`(start it: ours daemon start) [${e?.message ?? e}]`;
|
|
292
299
|
}
|
|
293
300
|
checks.push({ name: checkName, ok, detail });
|
|
294
301
|
}
|
package/dist/fleet-proxy.d.ts
CHANGED
|
@@ -12,11 +12,6 @@ export interface ManagedFleetSpawnResult {
|
|
|
12
12
|
session: 'tmux' | 'acp';
|
|
13
13
|
model?: string;
|
|
14
14
|
monitor: Pick<MonitorConfig, 'mode' | 'interrupt'>;
|
|
15
|
-
/** Adapter-resolved portable policy and exact native runtime mode. */
|
|
16
|
-
permissionMode?: {
|
|
17
|
-
fleetMode: 'ask' | 'auto' | 'allow';
|
|
18
|
-
nativeMode: string;
|
|
19
|
-
};
|
|
20
15
|
inherited: string[];
|
|
21
16
|
creationActionId: string;
|
|
22
17
|
}
|
|
@@ -1,23 +1,18 @@
|
|
|
1
1
|
import { createRequire } from 'node:module';
|
|
2
|
-
import {
|
|
3
|
-
import { dirname,
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, resolve } from 'node:path';
|
|
4
4
|
const require = createRequire(import.meta.url);
|
|
5
5
|
export function resolveBundledAcpAgent(packageName, binName, fallbackCommand) {
|
|
6
6
|
try {
|
|
7
7
|
const manifestPath = require.resolve(`${packageName}/package.json`);
|
|
8
8
|
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
9
|
-
const
|
|
9
|
+
const relative = typeof manifest.bin === 'string'
|
|
10
10
|
? manifest.bin
|
|
11
11
|
: manifest.bin?.[binName];
|
|
12
|
-
if (!
|
|
12
|
+
if (!relative)
|
|
13
13
|
return { argv: [fallbackCommand], bundled: false };
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
const entrypointFromRoot = relative(packageRoot, entrypoint);
|
|
17
|
-
if (entrypointFromRoot === '..'
|
|
18
|
-
|| entrypointFromRoot.startsWith(`..${sep}`)
|
|
19
|
-
|| isAbsolute(entrypointFromRoot)
|
|
20
|
-
|| !statSync(entrypoint).isFile())
|
|
14
|
+
const entrypoint = resolve(dirname(manifestPath), relative);
|
|
15
|
+
if (!existsSync(entrypoint))
|
|
21
16
|
return { argv: [fallbackCommand], bundled: false };
|
|
22
17
|
return {
|
|
23
18
|
argv: [process.execPath, entrypoint], bundled: true, manifestPath,
|
|
@@ -6,98 +6,8 @@ import { registerAdapter } from './registry.js';
|
|
|
6
6
|
import { replaceFileAtomically, withFileLock } from '../atomic-file.js';
|
|
7
7
|
import { harnessRuntimeDir } from '../isolation/policy.js';
|
|
8
8
|
import { bundledAcpAgent } from './acp-agent.js';
|
|
9
|
-
const OPTION_KEYS = [
|
|
10
|
-
'plugins', 'mem_palace', 'mem_palace_midsession_autosave', 'permission_mode', 'effort',
|
|
11
|
-
'mcp_servers', 'mcp_servers_only',
|
|
12
|
-
];
|
|
9
|
+
const OPTION_KEYS = ['plugins', 'mem_palace', 'mem_palace_midsession_autosave', 'permission_mode', 'effort'];
|
|
13
10
|
const EFFORT_LEVELS = ['low', 'medium', 'high', 'xhigh', 'max'];
|
|
14
|
-
/** `.mcp.json` server types. Absent means stdio, as the file format has it. */
|
|
15
|
-
const MCP_SERVER_TYPES = ['stdio', 'http', 'sse'];
|
|
16
|
-
/** A role that names its own ACP command runs a process fleet did not choose. */
|
|
17
|
-
const customAcpCommand = (role) => role.session === 'acp' && role.session_options?.acp?.command != null;
|
|
18
|
-
/**
|
|
19
|
-
* Does this server set include the ours connector?
|
|
20
|
-
*
|
|
21
|
-
* Load-bearing, and the reason it is a check rather than a doc line:
|
|
22
|
-
* `mcp_servers_only` maps to `--strict-mcp-config`, which ignores EVERY other MCP
|
|
23
|
-
* configuration — project `.mcp.json`, user settings, and **plugins**. On a
|
|
24
|
-
* normal install the ours connector arrives as a plugin
|
|
25
|
-
* (`~/.claude/plugins/.../plugin.json` declares `ours`), so a role that turns
|
|
26
|
-
* strict mode on without re-declaring it loses `send_message` and `get_messages`
|
|
27
|
-
* and cannot report that it has: a mute agent looks exactly like a quiet one.
|
|
28
|
-
*
|
|
29
|
-
* Matched on the command line rather than the server's NAME, because the name is
|
|
30
|
-
* the operator's to choose and would make this trivially satisfiable by writing
|
|
31
|
-
* `ours:` above the wrong command.
|
|
32
|
-
*/
|
|
33
|
-
const declaresOursConnector = (servers) => Object.values(servers).some(s => [s.command ?? '', ...(s.args ?? [])].some(part => /(^|[/\\])ours-mcp($|\s)|@ours\.network[/\\]mcp/.test(part)));
|
|
34
|
-
/** Shape-check `harness_options.mcp_servers` against `.mcp.json`'s own rules. */
|
|
35
|
-
function validateMcpServers(servers) {
|
|
36
|
-
if (servers == null)
|
|
37
|
-
return [];
|
|
38
|
-
const at = (k = '') => ({ path: `harness_options.mcp_servers${k}` });
|
|
39
|
-
if (typeof servers !== 'object' || Array.isArray(servers))
|
|
40
|
-
return [{ ...at(), message: 'must be a map of server name to server definition' }];
|
|
41
|
-
const entries = Object.entries(servers);
|
|
42
|
-
if (!entries.length)
|
|
43
|
-
return [{ ...at(), message: 'must declare at least one server, or be omitted' }];
|
|
44
|
-
const errors = [];
|
|
45
|
-
for (const [name, raw] of entries) {
|
|
46
|
-
const p = `.${name}`;
|
|
47
|
-
if (!/^[A-Za-z0-9_-]+$/.test(name)) {
|
|
48
|
-
errors.push({ ...at(p), message: 'server name must be [A-Za-z0-9_-]' });
|
|
49
|
-
continue;
|
|
50
|
-
}
|
|
51
|
-
if (raw == null || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
52
|
-
errors.push({ ...at(p), message: 'must be a map' });
|
|
53
|
-
continue;
|
|
54
|
-
}
|
|
55
|
-
const s = raw;
|
|
56
|
-
if (s.type != null && !MCP_SERVER_TYPES.includes(s.type))
|
|
57
|
-
errors.push({ ...at(`${p}.type`), message: `must be one of: ${MCP_SERVER_TYPES.join(', ')}` });
|
|
58
|
-
const remote = s.type === 'http' || s.type === 'sse';
|
|
59
|
-
if (remote) {
|
|
60
|
-
if (typeof s.url !== 'string' || !s.url.trim())
|
|
61
|
-
errors.push({ ...at(`${p}.url`), message: `must be a non-empty URL for a ${s.type} server` });
|
|
62
|
-
if (s.command != null)
|
|
63
|
-
errors.push({ ...at(`${p}.command`), message: `must not be set for a ${s.type} server` });
|
|
64
|
-
}
|
|
65
|
-
else {
|
|
66
|
-
if (typeof s.command !== 'string' || !s.command.trim())
|
|
67
|
-
errors.push({ ...at(`${p}.command`), message: 'must be a non-empty command for a stdio server' });
|
|
68
|
-
if (s.args != null && (!Array.isArray(s.args) || s.args.some(a => typeof a !== 'string')))
|
|
69
|
-
errors.push({ ...at(`${p}.args`), message: 'must be an array of strings' });
|
|
70
|
-
if (s.url != null)
|
|
71
|
-
errors.push({ ...at(`${p}.url`), message: 'must not be set for a stdio server' });
|
|
72
|
-
}
|
|
73
|
-
for (const key of ['env', 'headers']) {
|
|
74
|
-
const v = s[key];
|
|
75
|
-
if (v == null)
|
|
76
|
-
continue;
|
|
77
|
-
if (typeof v !== 'object' || Array.isArray(v)
|
|
78
|
-
|| Object.values(v).some(x => typeof x !== 'string'))
|
|
79
|
-
errors.push({ ...at(`${p}.${key}`), message: 'must be a map of string to string' });
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
return errors;
|
|
83
|
-
}
|
|
84
|
-
/** `harness_options.mcp_servers` in ACP's `session/new` array shape. */
|
|
85
|
-
function acpMcpServersFor(servers) {
|
|
86
|
-
if (!servers)
|
|
87
|
-
return [];
|
|
88
|
-
// `env` and `headers` are REQUIRED arrays in the protocol, so they are always
|
|
89
|
-
// sent — empty when the role declared none.
|
|
90
|
-
const pairs = (r) => Object.entries(r ?? {}).map(([name, value]) => ({ name, value }));
|
|
91
|
-
return Object.entries(servers).map(([name, s]) => {
|
|
92
|
-
if (s.type === 'http' || s.type === 'sse')
|
|
93
|
-
return { name, type: s.type, url: s.url, headers: pairs(s.headers) };
|
|
94
|
-
// Stdio carries NO `type` field: ACP's stdio variant is the one without it,
|
|
95
|
-
// and the bundled agent keys on exactly that (claude-agent-acp
|
|
96
|
-
// acp-agent.js:4058, `!("type" in server)`), so sending `type: 'stdio'`
|
|
97
|
-
// would drop the server on the floor.
|
|
98
|
-
return { name, command: s.command, args: s.args ?? [], env: pairs(s.env) };
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
11
|
/** Claude Code's accepted --permission-mode values. */
|
|
102
12
|
const PERMISSION_MODES = ['default', 'acceptEdits', 'plan', 'dontAsk', 'bypassPermissions'];
|
|
103
13
|
/**
|
|
@@ -217,8 +127,11 @@ export async function pretrust(dir, deps = {}) {
|
|
|
217
127
|
* it must be the Monitor TOOL, not a background Bash task (which never wakes the
|
|
218
128
|
* agent on output → an armed-looking but deaf monitor).
|
|
219
129
|
*/
|
|
130
|
+
const shellQuote = (value) => `'${value.replace(/'/g, `'"'"'`)}'`;
|
|
131
|
+
const watchCommand = (id) => 'ours api watch-notifications --input '
|
|
132
|
+
+ shellQuote(JSON.stringify({ identity: id, since: 'tip' })) + ' --json';
|
|
220
133
|
const armMonitor = (id) => 'arm a **persistent Monitor** (the Monitor TOOL — NOT a background Bash command; a ' +
|
|
221
|
-
`background Bash task never wakes you on output) running
|
|
134
|
+
`background Bash task never wakes you on output) running \`${watchCommand(id)}\` ` +
|
|
222
135
|
'so inbound ours mail wakes you';
|
|
223
136
|
export function makeClaudeCodeAdapter(exec = realExec) {
|
|
224
137
|
return {
|
|
@@ -236,7 +149,7 @@ export function makeClaudeCodeAdapter(exec = realExec) {
|
|
|
236
149
|
}],
|
|
237
150
|
};
|
|
238
151
|
},
|
|
239
|
-
validateOptions(opts
|
|
152
|
+
validateOptions(opts) {
|
|
240
153
|
if (opts == null)
|
|
241
154
|
return [];
|
|
242
155
|
if (typeof opts !== 'object' || Array.isArray(opts))
|
|
@@ -244,44 +157,9 @@ export function makeClaudeCodeAdapter(exec = realExec) {
|
|
|
244
157
|
const errors = Object.keys(opts)
|
|
245
158
|
.filter(k => !OPTION_KEYS.includes(k))
|
|
246
159
|
.map(k => ({ path: `harness_options.${k}`, message: `unknown option; allowed: ${OPTION_KEYS.join(', ')}` }));
|
|
247
|
-
const
|
|
248
|
-
const effort = o.effort;
|
|
160
|
+
const effort = opts.effort;
|
|
249
161
|
if (effort != null && !EFFORT_LEVELS.includes(effort))
|
|
250
162
|
errors.push({ path: 'harness_options.effort', message: `must be one of: ${EFFORT_LEVELS.join(', ')}` });
|
|
251
|
-
if (o.mcp_servers_only != null && typeof o.mcp_servers_only !== 'boolean')
|
|
252
|
-
errors.push({ path: 'harness_options.mcp_servers_only', message: 'must be a boolean' });
|
|
253
|
-
errors.push(...validateMcpServers(o.mcp_servers));
|
|
254
|
-
if (o.mcp_servers_only === true && !o.mcp_servers)
|
|
255
|
-
errors.push({
|
|
256
|
-
path: 'harness_options.mcp_servers_only',
|
|
257
|
-
message: 'requires harness_options.mcp_servers; on its own it would leave the role with no MCP servers at all',
|
|
258
|
-
});
|
|
259
|
-
// The muteness gate. Only when the declared set is otherwise well-formed —
|
|
260
|
-
// a shape error already told the operator to look here.
|
|
261
|
-
if (o.mcp_servers_only === true && o.mcp_servers && errors.length === 0
|
|
262
|
-
&& !declaresOursConnector(o.mcp_servers))
|
|
263
|
-
errors.push({
|
|
264
|
-
path: 'harness_options.mcp_servers',
|
|
265
|
-
message: 'mcp_servers_only ignores every other MCP configuration, INCLUDING plugins — and the ours '
|
|
266
|
-
+ 'connector is normally a plugin, so this role would have no send_message or get_messages and no way '
|
|
267
|
-
+ 'to report that. Declare it explicitly, e.g. ours: { command: ours-mcp, args: [proxy] }',
|
|
268
|
-
});
|
|
269
|
-
// Session-aware refusals. Both options reach an ACP session through the
|
|
270
|
-
// bundled agent's `_meta` vocabulary, so a role that launches a DIFFERENT
|
|
271
|
-
// ACP agent cannot be promised either one. Refuse rather than send it and
|
|
272
|
-
// hope: silently dropping the config is the defect being fixed here.
|
|
273
|
-
if (role && customAcpCommand(role)) {
|
|
274
|
-
for (const key of ['plugins', 'mcp_servers', 'mcp_servers_only']) {
|
|
275
|
-
if (o[key] == null)
|
|
276
|
-
continue;
|
|
277
|
-
errors.push({
|
|
278
|
-
path: `harness_options.${key}`,
|
|
279
|
-
message: 'cannot be honoured with session_options.acp.command: it is delivered through the bundled '
|
|
280
|
-
+ 'Claude ACP agent\'s _meta vocabulary, which another agent has no reason to read. Drop the '
|
|
281
|
-
+ 'custom ACP command, or drop this option',
|
|
282
|
-
});
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
163
|
return errors;
|
|
286
164
|
},
|
|
287
165
|
async prepareSession(role, dirs) {
|
|
@@ -300,15 +178,6 @@ export function makeClaudeCodeAdapter(exec = realExec) {
|
|
|
300
178
|
CLAUDE_AUTOCOMPACT_PCT_OVERRIDE: String(autocompactPct(role)),
|
|
301
179
|
MEMPALACE_HOOKS_AUTO_SAVE: 'false',
|
|
302
180
|
MEMPALACE_MIDSESSION_AUTOSAVE: o.mem_palace_midsession_autosave ? 'true' : 'false',
|
|
303
|
-
// The role's identity, for the ours connector to bind at startup instead of
|
|
304
|
-
// the briefing telling the MODEL to call choose_identity. Both launches
|
|
305
|
-
// return `prep.env`, so this one line covers tmux and ACP alike.
|
|
306
|
-
//
|
|
307
|
-
// The bind the connector performs is PLAIN and fail-closed: it can never
|
|
308
|
-
// evict a live session, and a role whose identity does not exist yet simply
|
|
309
|
-
// boots unbound and falls through to the briefing's create-if-missing step.
|
|
310
|
-
// Nothing here may ever grow a force flag.
|
|
311
|
-
OURS_BIND_IDENTITY: role.identity,
|
|
312
181
|
};
|
|
313
182
|
if (!memPalace)
|
|
314
183
|
env.MEMPALACE_DISABLED = 'true';
|
|
@@ -319,29 +188,12 @@ export function makeClaudeCodeAdapter(exec = realExec) {
|
|
|
319
188
|
if (role.isolation)
|
|
320
189
|
mkdirSync(harnessRuntimeDir(dirs.stateDir, 'claude'), { recursive: true });
|
|
321
190
|
const argv = [];
|
|
322
|
-
let settingsOverlay;
|
|
323
191
|
if (Object.keys(enabledPlugins).length) {
|
|
324
|
-
|
|
325
|
-
writeFileSync(
|
|
326
|
-
argv.push('--settings',
|
|
192
|
+
const overlay = join(dirs.stateDir, '.settings-overlay.json');
|
|
193
|
+
writeFileSync(overlay, JSON.stringify({ enabledPlugins }, null, 2));
|
|
194
|
+
argv.push('--settings', overlay);
|
|
327
195
|
}
|
|
328
|
-
|
|
329
|
-
// file's servers; `--strict-mcp-config` is what makes the set exclusive, and
|
|
330
|
-
// it is opt-in per role because it drops everything else the user has,
|
|
331
|
-
// plugins included (see `declaresOursConnector`).
|
|
332
|
-
let mcpConfigFile;
|
|
333
|
-
if (o.mcp_servers) {
|
|
334
|
-
mcpConfigFile = join(dirs.stateDir, '.mcp-config.json');
|
|
335
|
-
writeFileSync(mcpConfigFile, JSON.stringify({ mcpServers: o.mcp_servers }, null, 2), { mode: 0o600 });
|
|
336
|
-
argv.push('--mcp-config', mcpConfigFile);
|
|
337
|
-
if (o.mcp_servers_only === true)
|
|
338
|
-
argv.push('--strict-mcp-config');
|
|
339
|
-
}
|
|
340
|
-
return {
|
|
341
|
-
argv, env,
|
|
342
|
-
...(settingsOverlay ? { settingsOverlay } : {}),
|
|
343
|
-
...(mcpConfigFile ? { mcpConfigFile } : {}),
|
|
344
|
-
};
|
|
196
|
+
return { argv, env };
|
|
345
197
|
},
|
|
346
198
|
buildLaunch(role, mode, s, prep) {
|
|
347
199
|
const stateDir = roleStateDir(role);
|
|
@@ -371,46 +223,6 @@ export function makeClaudeCodeAdapter(exec = realExec) {
|
|
|
371
223
|
acpPermissionModeId(role) {
|
|
372
224
|
return permissionMode(role);
|
|
373
225
|
},
|
|
374
|
-
/**
|
|
375
|
-
* Deliver, over ACP, the two things the tmux launch delivers as flags.
|
|
376
|
-
*
|
|
377
|
-
* `buildAcpLaunch` builds its own argv and cannot carry `prep.argv`: the
|
|
378
|
-
* process it launches is the ACP agent, not `claude`, and it takes none of
|
|
379
|
-
* claude's flags. That is why `harness_options.plugins` did nothing at all on
|
|
380
|
-
* an ACP role — the overlay was written and then dropped, and the mem-palace
|
|
381
|
-
* toggle rode `prep.env` and survived, so the failure was silent AND
|
|
382
|
-
* selective.
|
|
383
|
-
*
|
|
384
|
-
* `_meta.claudeCode.options` is the bundled agent's own passthrough into the
|
|
385
|
-
* Claude Agent SDK (@agentclientprotocol/claude-agent-acp, acp-agent.js:4092
|
|
386
|
-
* → the `options` object at :4144). `settings` takes the same overlay path
|
|
387
|
-
* `--settings` takes; `strictMcpConfig` is the SDK's spelling of
|
|
388
|
-
* `--strict-mcp-config`. Both are spread BEFORE the fields the agent forces,
|
|
389
|
-
* so neither is overwritten.
|
|
390
|
-
*
|
|
391
|
-
* ⚠ RETURNS NOTHING FOR A ROLE THAT NAMES ITS OWN ACP COMMAND. That process
|
|
392
|
-
* is not the bundled agent and has no reason to read this vocabulary; sending
|
|
393
|
-
* it anyway would be the silent drop again, one level down. `validateOptions`
|
|
394
|
-
* refuses those roles instead.
|
|
395
|
-
*/
|
|
396
|
-
acpSessionMeta(role, prep) {
|
|
397
|
-
if (customAcpCommand(role))
|
|
398
|
-
return undefined;
|
|
399
|
-
const options = {};
|
|
400
|
-
if (prep.settingsOverlay)
|
|
401
|
-
options.settings = prep.settingsOverlay;
|
|
402
|
-
if (role.harness_options?.mcp_servers_only === true)
|
|
403
|
-
options.strictMcpConfig = true;
|
|
404
|
-
return Object.keys(options).length ? { claudeCode: { options } } : undefined;
|
|
405
|
-
},
|
|
406
|
-
/**
|
|
407
|
-
* The declared servers, in ACP's array shape. Sent on `session/new` and on
|
|
408
|
-
* resume/load, because the SDK builds its server set once per session and a
|
|
409
|
-
* resumed session that dropped them would quietly lose its tools.
|
|
410
|
-
*/
|
|
411
|
-
acpMcpServers(role) {
|
|
412
|
-
return acpMcpServersFor(role.harness_options?.mcp_servers);
|
|
413
|
-
},
|
|
414
226
|
isolationPaths(_role, _dirs) {
|
|
415
227
|
const claudeHome = join(home(), '.claude');
|
|
416
228
|
return {
|
|
@@ -473,7 +285,6 @@ export function makeClaudeCodeAdapter(exec = realExec) {
|
|
|
473
285
|
currentIdentityTool: 'current_identity',
|
|
474
286
|
sendTool: 'send_message',
|
|
475
287
|
getMessagesTool: 'get_messages',
|
|
476
|
-
watchCommand: id => `ours-mcp watch "${id}"`,
|
|
477
288
|
monitorInstruction: id => {
|
|
478
289
|
const m = armMonitor(id);
|
|
479
290
|
return `${m.charAt(0).toUpperCase()}${m.slice(1)}.`;
|
package/dist/harness/codex.d.ts
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
import { type Exec } from '../exec.js';
|
|
2
|
-
import type {
|
|
3
|
-
import { type AcpAgentResolution } from './acp-agent.js';
|
|
2
|
+
import type { HarnessAdapter, UnattendedCapability } from './types.js';
|
|
4
3
|
/**
|
|
5
4
|
* What an unattended role can actually do under Codex's native settings.
|
|
6
5
|
* `on-request` and `untrusted` stop to ask, and with no console attached that
|
|
7
6
|
* request is refused rather than answered — so the role can only read.
|
|
8
7
|
*/
|
|
9
8
|
export declare function codexCapabilities(approval: string, sandbox: string): UnattendedCapability[];
|
|
10
|
-
/** Bind launch argv and metadata provenance to one already-completed resolution. */
|
|
11
|
-
export declare function codexAcpLaunchForResolution(resolution: AcpAgentResolution): Pick<AcpLaunch, 'argv' | 'permissionMetadataSource'>;
|
|
12
9
|
export declare function makeCodexAdapter(exec?: Exec): HarnessAdapter;
|
|
13
10
|
export declare const codexAdapter: HarnessAdapter;
|