@ours.network/fleet 1.0.4 → 1.1.0-nightly.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -50
- package/dist/application/capabilities.d.ts +0 -1
- package/dist/application/capabilities.js +3 -13
- package/dist/application/errors.d.ts +1 -1
- package/dist/application/fleet-query-service.d.ts +0 -3
- package/dist/application/fleet-query-service.js +1 -24
- package/dist/application/role-creation-service.d.ts +5 -5
- package/dist/application/role-creation-service.js +5 -7
- package/dist/application/role-repository.d.ts +1 -2
- package/dist/application/role-repository.js +7 -7
- package/dist/application/session-control.d.ts +0 -12
- package/dist/application/session-control.js +0 -42
- package/dist/application/session-mutations.d.ts +5 -5
- package/dist/application/task-room-service.d.ts +37 -2
- package/dist/application/task-room-service.js +49 -7
- package/dist/application/types.d.ts +0 -7
- package/dist/atomic-file.d.ts +5 -3
- package/dist/atomic-file.js +32 -8
- package/dist/briefing.js +1 -4
- package/dist/build-info.json +5 -5
- package/dist/cli.js +9 -23
- package/dist/config.d.ts +7 -7
- package/dist/config.js +13 -34
- package/dist/docs.d.ts +1 -1
- package/dist/docs.js +46 -30
- package/dist/doctor.js +0 -7
- package/dist/fleet-proxy.d.ts +1 -1
- package/dist/harness/acp-session-transport.d.ts +8 -0
- package/dist/harness/acp-session-transport.js +3 -0
- package/dist/harness/agent-session.d.ts +33 -0
- package/dist/harness/agent-session.js +1 -0
- package/dist/harness/claude-code-session.d.ts +18 -0
- package/dist/harness/claude-code-session.js +28 -0
- package/dist/harness/claude-code.d.ts +2 -1
- package/dist/harness/claude-code.js +29 -81
- package/dist/harness/codex-session.d.ts +18 -0
- package/dist/harness/codex-session.js +28 -0
- package/dist/harness/codex.d.ts +2 -1
- package/dist/harness/codex.js +35 -45
- package/dist/harness/types.d.ts +6 -38
- package/dist/index.d.ts +3 -5
- package/dist/index.js +1 -3
- package/dist/isolation/resources.d.ts +3 -7
- package/dist/isolation/resources.js +3 -7
- package/dist/model-env.d.ts +2 -3
- package/dist/model-env.js +2 -3
- package/dist/monitor.d.ts +1 -24
- package/dist/monitor.js +5 -140
- package/dist/owner-channel/attachments.d.ts +2 -4
- package/dist/owner-channel/attachments.js +6 -39
- package/dist/owner-channel/channel.d.ts +2 -2
- package/dist/owner-channel/channel.js +16 -3
- package/dist/owner-channel/commands.d.ts +18 -1
- package/dist/owner-channel/commands.js +73 -4
- package/dist/rooms-tasks/cli.js +121 -2
- package/dist/rooms-tasks/task-lists.d.ts +19 -0
- package/dist/rooms-tasks/task-lists.js +96 -0
- package/dist/rooms-tasks/task-state.d.ts +3 -0
- package/dist/rooms-tasks/task-state.js +281 -175
- package/dist/rooms-tasks/types.d.ts +11 -1
- package/dist/runner.d.ts +5 -15
- package/dist/runner.js +26 -102
- package/dist/session/acp.d.ts +2 -2
- package/dist/session/acp.js +2 -3
- package/dist/session/activity.d.ts +2 -3
- package/dist/session/activity.js +2 -3
- package/dist/session/arbiter.d.ts +4 -4
- package/dist/session/control.d.ts +3 -3
- package/dist/session/types.d.ts +11 -5
- package/dist/spawn.js +8 -9
- package/dist/supervisor/none.d.ts +1 -1
- package/dist/supervisor/none.js +2 -2
- package/dist/tmux.d.ts +1 -14
- package/dist/tmux.js +1 -51
- package/dist/watchdog/config.js +6 -4
- package/dist/watchdog/run.js +8 -23
- package/dist/web/auth.d.ts +1 -1
- package/dist/web/runtime.js +6 -15
- package/dist/web/server.d.ts +3 -3
- package/dist/web/server.js +89 -15
- package/dist/web/topology-promote.js +2 -5
- package/dist/web-app/assets/index-59GF-gsZ.css +1 -0
- package/dist/web-app/assets/{index-BCBK78hw.js → index-DhMDVRU1.js} +6 -6
- package/dist/web-app/index.html +2 -2
- package/package.json +3 -8
- package/dist/session/tmux.d.ts +0 -28
- package/dist/session/tmux.js +0 -80
- package/dist/web/terminal/bridge.d.ts +0 -27
- package/dist/web/terminal/bridge.js +0 -317
- package/dist/web-app/assets/TerminalView-C_G1ID2P.js +0 -9
- package/dist/web-app/assets/index-DuC-xnX4.css +0 -1
package/README.md
CHANGED
|
@@ -8,8 +8,8 @@ harnesses — from one declarative file.**
|
|
|
8
8
|
An AI coding agent in a terminal dies when you close the laptop. `ours-fleet`
|
|
9
9
|
turns such sessions into **roles**: long-lived agents that
|
|
10
10
|
|
|
11
|
-
- **run through
|
|
12
|
-
|
|
11
|
+
- **run through structured ACP sessions** behind a shared Codex/Claude Code
|
|
12
|
+
agent-session interface, which you can inspect or prompt,
|
|
13
13
|
- are **supervised** — systemd (Linux) or launchd (macOS) restarts them on crash
|
|
14
14
|
and brings them back after a reboot,
|
|
15
15
|
- **resume their context** across restarts (when the harness supports it),
|
|
@@ -44,8 +44,7 @@ roles:
|
|
|
44
44
|
~/fleet.yaml + ~/fleet.d/*.yaml your declaration
|
|
45
45
|
│ ours-fleet up
|
|
46
46
|
▼
|
|
47
|
-
briefing.md per role ──►
|
|
48
|
-
└─► ACP client ──► ACP agent (codex-acp …)
|
|
47
|
+
briefing.md per role ──► agent session adapter ──► ACP client/session ──► ACP agent
|
|
49
48
|
▲ │
|
|
50
49
|
systemd --user / launchd ───────┘ restart on crash, start at boot/login
|
|
51
50
|
```
|
|
@@ -78,7 +77,6 @@ The state dir contract:
|
|
|
78
77
|
| What | Why | Install |
|
|
79
78
|
|---|---|---|
|
|
80
79
|
| Node ≥ 20 | runs `ours-fleet` itself | nodejs.org, `apt`, or `brew` |
|
|
81
|
-
| tmux | roles using `session: tmux` (the default) | `apt install tmux` / `brew install tmux` |
|
|
82
80
|
| Node ≥ 22 | Claude roles using `session: acp` | required by the maintained Claude ACP adapter |
|
|
83
81
|
| a harness CLI, logged in | the agent itself | e.g. Claude Code (`claude`) or Codex CLI (`codex`) |
|
|
84
82
|
| `ours` CLI + shared daemon | identity + agent-to-agent messaging | `npm i -g @ours.network/cli && ours daemon start` |
|
|
@@ -98,8 +96,8 @@ ours-fleet doctor # verifies everything above, with actionable messages
|
|
|
98
96
|
The maintained Codex and Claude ACP adapters install as optional dependencies of
|
|
99
97
|
`ours-fleet` and are resolved internally; users do not install adapter commands
|
|
100
98
|
or add them to `PATH`. An explicit `session_options.acp.command` remains
|
|
101
|
-
available for custom adapters. On Node 20–21,
|
|
102
|
-
|
|
99
|
+
available for custom adapters. On Node 20–21, Codex ACP remains available,
|
|
100
|
+
while maintained Claude ACP requires upgrading to Node 22.
|
|
103
101
|
|
|
104
102
|
Each OS user manages their own fleet — to host roles under a sandboxed account,
|
|
105
103
|
become that account and repeat.
|
|
@@ -139,8 +137,8 @@ fleet monitor policy, and same-harness model inherit from the caller. Explicit
|
|
|
139
137
|
flags win; changing harness without a model lets the selected harness/fleet
|
|
140
138
|
defaults choose one. After creation succeeds, fleet can deterministically notify
|
|
141
139
|
the caller's owner channel with the caller and spawned-role details. This is an
|
|
142
|
-
honest-actor convenience and attribution path, not a security boundary;
|
|
143
|
-
|
|
140
|
+
honest-actor convenience and attribution path, not a security boundary; host
|
|
141
|
+
shells and deliberately bypassed absolute binaries retain direct behavior.
|
|
144
142
|
|
|
145
143
|
## Local web console
|
|
146
144
|
|
|
@@ -209,8 +207,8 @@ offline shell and no stale fleet state.
|
|
|
209
207
|
|
|
210
208
|
The console provides evidence-separated inventory and status, ACP activity and
|
|
211
209
|
permission controls, redacted logs, typed text send, confirmed lifecycle
|
|
212
|
-
actions, transactional permanent/temporary creation
|
|
213
|
-
|
|
210
|
+
actions, and transactional permanent/temporary creation. Identity is fixed to
|
|
211
|
+
the role name. Creation uses the authenticated
|
|
214
212
|
daemon inventory and reports verified, missing, or unknown evidence. A missing
|
|
215
213
|
permanent role identity is created deterministically before launch with local
|
|
216
214
|
discovery and auto-accept enabled, then the provisioning lease is released so
|
|
@@ -243,19 +241,16 @@ never force-adopts or deletes identity state. Permanent roles are
|
|
|
243
241
|
provisioned by fleet before launch and never delegate normal identity creation
|
|
244
242
|
to the harness.
|
|
245
243
|
|
|
246
|
-
|
|
247
|
-
non-terminal features remain available and tmux Terminal is disabled with a
|
|
248
|
-
diagnostic.
|
|
244
|
+
The web console uses structured ACP activity rather than exposing a raw agent terminal.
|
|
249
245
|
|
|
250
246
|
Security boundaries:
|
|
251
247
|
|
|
252
248
|
- configured `Host` and `Origin`, CSRF, one-time WebSocket tickets, and explicit
|
|
253
249
|
bind/origin policy are enforced server-side;
|
|
254
250
|
- cwd values must resolve beneath configured local roots;
|
|
255
|
-
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
isolated tmux sockets—never active role state.
|
|
251
|
+
- normal logs are bounded and redacted;
|
|
252
|
+
- tests use temporary fleet homes and fake supervisors/identity providers—never
|
|
253
|
+
active role state.
|
|
259
254
|
|
|
260
255
|
Permanent spawns are written to `~/fleet.d/<Name>.yaml`; the CLI never edits your
|
|
261
256
|
hand-written `~/fleet.yaml`. `ours-fleet rm <Name>` unspawns.
|
|
@@ -292,7 +287,6 @@ them and unsticks them:
|
|
|
292
287
|
|
|
293
288
|
```sh
|
|
294
289
|
ours-fleet peek Worker # what is it doing?
|
|
295
|
-
ours-fleet send Worker --key 1 # answer the menu it's stuck on
|
|
296
290
|
ours-fleet send Worker "continue with the tests, then report"
|
|
297
291
|
```
|
|
298
292
|
|
|
@@ -315,7 +309,7 @@ ours-fleet docs | man AI-friendly complete reference
|
|
|
315
309
|
ours-fleet up|down|restart|force-restart [-c FILE] [Name...]
|
|
316
310
|
ours-fleet config [-c FILE] validate + print merged plan
|
|
317
311
|
ours-fleet ls | attach | peek | logs [-f] | status <Name>
|
|
318
|
-
ours-fleet send <Name> "text"
|
|
312
|
+
ours-fleet send <Name> "text"
|
|
319
313
|
ours-fleet spawn [--temp] [<Name> | --role <Name>] [--harness --session --mission --model --approval ...]
|
|
320
314
|
ours-fleet loops validate|list|status
|
|
321
315
|
ours-fleet loops reload <Role>
|
|
@@ -355,7 +349,7 @@ vars: { work_root: /home/me/work } # ${var} substitution anywhere below
|
|
|
355
349
|
start_stagger_ms: 0 # delay between agent LAUNCHES (host-wide, ms); 0 = no stagger
|
|
356
350
|
defaults:
|
|
357
351
|
harness: claude-code # for roles that don't set one
|
|
358
|
-
session:
|
|
352
|
+
session: acp # supported Fleet↔agent session path
|
|
359
353
|
permissions: # common intent, translated by each harness/backend
|
|
360
354
|
approval: ask # ask | auto | allow (`deny` is a deprecated alias)
|
|
361
355
|
filesystem: workspace # read-only | workspace | unrestricted
|
|
@@ -371,7 +365,7 @@ defaults:
|
|
|
371
365
|
roles:
|
|
372
366
|
Name: # [A-Za-z0-9_-]+
|
|
373
367
|
harness: claude-code
|
|
374
|
-
session: acp #
|
|
368
|
+
session: acp # optional; ACP is the only supported session
|
|
375
369
|
session_options:
|
|
376
370
|
acp:
|
|
377
371
|
command: claude-agent-acp # optional advanced override
|
|
@@ -400,7 +394,6 @@ roles:
|
|
|
400
394
|
max_file_bytes: 10485760 # 10 MiB
|
|
401
395
|
max_request_bytes: 20971520 # 20 MiB total, and >= max_file_bytes
|
|
402
396
|
retention_ms: 86400000 # stale crash cleanup; 1 minute..30 days
|
|
403
|
-
allowed_mime: [application/pdf, text/plain, image/png, audio/ogg]
|
|
404
397
|
model: claude-fable-5 # launch on a specific model (pass-through id; default: launcher default)
|
|
405
398
|
mission: one line
|
|
406
399
|
persona: | # operating contract (published as persona)
|
|
@@ -447,15 +440,14 @@ loops: # trusted local scheduled ACP turns
|
|
|
447
440
|
|
|
448
441
|
Merge order: `fleet.yaml` ← `fleet.d/*.yaml`; a duplicate role name is a hard
|
|
449
442
|
error naming both files. Identities and roles are decoupled — removing a role
|
|
450
|
-
never deletes an identity. `session` is
|
|
451
|
-
|
|
452
|
-
contract. `defaults.harness_options` is shallow-merged with each
|
|
443
|
+
never deletes an identity. `session` is ACP-backed for both supported harnesses.
|
|
444
|
+
`defaults.harness_options` is shallow-merged with each
|
|
453
445
|
role's `harness_options`, so a fleet can set common Codex permission/profile defaults
|
|
454
446
|
and override individual keys per role. `monitor` merges the same way — a role block
|
|
455
447
|
overrides `defaults.monitor` key-by-key.
|
|
456
448
|
|
|
457
449
|
Every supervised role is a client of the operator-configured ours daemon. Fleet strips the
|
|
458
|
-
obsolete, presence-sensitive `OURS_AUTOSTART` variable from
|
|
450
|
+
obsolete, presence-sensitive `OURS_AUTOSTART` variable from ACP harness
|
|
459
451
|
processes. `ours-mcp proxy` is client-only and never starts a daemon; operators and explicit
|
|
460
452
|
installer/setup flows remain responsible for starting it.
|
|
461
453
|
|
|
@@ -598,9 +590,7 @@ since a human can still attach a console and answer.
|
|
|
598
590
|
`auto` selects Codex ACP `agent` (`on-request` + `workspace-write`) / Claude
|
|
599
591
|
`acceptEdits`. `allow` selects Codex ACP's fully non-interactive yolo mode,
|
|
600
592
|
reported by the adapter as `agent-full-access` (`never` +
|
|
601
|
-
`danger-full-access`); Claude uses `bypassPermissions`.
|
|
602
|
-
approval and sandbox flags remain independent, `auto` is `on-request` and
|
|
603
|
-
`allow` is `never` while `filesystem` still chooses the sandbox.
|
|
593
|
+
`danger-full-access`); Claude uses `bypassPermissions`.
|
|
604
594
|
`dontAsk` suppresses only the *prompt*, not the denial, which is why an
|
|
605
595
|
`allow` role previously ran unable to do its job. Legacy `deny` is accepted
|
|
606
596
|
only for compatibility and retains its conservative Codex `on-request` /
|
|
@@ -690,8 +680,8 @@ their boots ~4 s apart instead of firing all seven at once.
|
|
|
690
680
|
With `monitor.mode: fleet` (the default), the **ours-fleet supervisor** delivers
|
|
691
681
|
a role's mail wakes: the per-role runner long-polls the ours daemon's notification API and
|
|
692
682
|
submits a single `[fleet-monitor] N new messages from … — run get_messages` prompt
|
|
693
|
-
through the
|
|
694
|
-
and falls back to structured `session/prompt
|
|
683
|
+
through the shared agent session. ACP uses live steering when its adapter supports it
|
|
684
|
+
and falls back to structured `session/prompt`.
|
|
695
685
|
Set `monitor.interrupt: true` on roles where every configured wake should cancel
|
|
696
686
|
the active turn before the notification is delivered. This is intentionally
|
|
697
687
|
content-blind: the supervisor cannot inspect encrypted message bodies, so all
|
|
@@ -701,8 +691,7 @@ tool result or pending permission. Fleet waits for terminal ACP tool/update
|
|
|
701
691
|
evidence, then steers the wake without calling `session.cancel`. If the tool is
|
|
702
692
|
still active after 120 seconds, or the adapter cannot expose authenticated tool
|
|
703
693
|
boundaries/steering, fleet visibly degrades to non-cancelling steering or queued
|
|
704
|
-
delivery.
|
|
705
|
-
interrupts remain immediate.
|
|
694
|
+
delivery. Explicit human and control interrupts remain immediate.
|
|
706
695
|
The default is `false`: a role that must begin a post-readiness mission
|
|
707
696
|
immediately, including second-and-later mail received while it is working, must
|
|
708
697
|
set `monitor.mode: fleet` and `monitor.interrupt: true` explicitly. Readiness and
|
|
@@ -795,8 +784,8 @@ invites, message bodies, credentials, or keys. A corrupt overlay fails closed
|
|
|
795
784
|
(no effective owners and no mutation), and the last effective owner cannot be
|
|
796
785
|
revoked.
|
|
797
786
|
|
|
798
|
-
These commands require a running
|
|
799
|
-
stopped,
|
|
787
|
+
These commands require a running role with `owner_channel` enabled. Missing,
|
|
788
|
+
stopped, disabled, draining, and unavailable-MCP targets fail without
|
|
800
789
|
starting a second client, binding an identity, or opening a network listener.
|
|
801
790
|
|
|
802
791
|
The managed agent can use its ordinary ours `send_message` or `send_file` tool to
|
|
@@ -967,20 +956,25 @@ message plaintext in fleet state.
|
|
|
967
956
|
Inbound owner attachments use the same authenticated-CID and exact-wire routing
|
|
968
957
|
boundary. Fleet first calls the metadata-only `listIncomingFiles`, groups a
|
|
969
958
|
file-only wake or a same-sender reply-linked text caption, and checks the enabled,
|
|
970
|
-
count,
|
|
971
|
-
|
|
959
|
+
count, per-file size, and total-size policy before retrieving any bytes. MIME values,
|
|
960
|
+
extensions, file categories, and declared-versus-detected mismatches are metadata,
|
|
961
|
+
never rejection criteria. It then calls selective `getFiles` only for the admitted wire IDs. An
|
|
972
962
|
unauthorized sender is ignored without retrieval or reply. A rejected authorized
|
|
973
963
|
request receives a bounded reason correlated to its file wire.
|
|
974
964
|
|
|
975
965
|
Retrieved files must be regular, non-symlink paths whose byte count and SHA-256
|
|
976
|
-
match typed SDK metadata. Fleet
|
|
977
|
-
|
|
966
|
+
match typed SDK metadata. Fleet detects recognizable content only to report metadata,
|
|
967
|
+
sanitizes traversal/control characters from names, and copies each
|
|
978
968
|
file into a random request-scoped directory at mode 0700 with files at mode 0600.
|
|
979
969
|
The `[fleet-owner]` turn receives only bounded metadata, the private local paths,
|
|
980
970
|
and an explicit daemon transcription result for voice messages. Successful
|
|
981
971
|
transcripts are included; `failed` and `unavailable` states are stated plainly so
|
|
982
972
|
the agent must use the audio path rather than inventing text.
|
|
983
973
|
|
|
974
|
+
The legacy `attachments.allowed_mime` key is accepted and ignored so existing
|
|
975
|
+
configurations continue to load. It is omitted from resolved configuration and
|
|
976
|
+
cannot affect attachment admission.
|
|
977
|
+
|
|
984
978
|
Request files are removed after final delivery and stale directories are removed
|
|
985
979
|
after `retention_ms`. A bounded mode-0600 recovery journal stores only owner CID
|
|
986
980
|
and wire routing metadata—never filenames, paths, captions, transcripts, or file
|
|
@@ -1002,20 +996,17 @@ Managed-agent file egress is a separate trust direction. It retains the same
|
|
|
1002
996
|
metadata provenance checks, count/per-file/request byte caps, regular-file and
|
|
1003
997
|
no-symlink reads, actual byte-count/SHA-256 verification, filename sanitization,
|
|
1004
998
|
private staging, fixed owner routing, and bounded body-free recovery metadata.
|
|
1005
|
-
It deliberately does not consult `attachments.enabled
|
|
999
|
+
It deliberately does not consult `attachments.enabled`; MIME is
|
|
1006
1000
|
still detected during byte validation, but a declared-versus-detected mismatch is
|
|
1007
|
-
not
|
|
1008
|
-
binaries are therefore routable only
|
|
1009
|
-
|
|
1001
|
+
not a deny gate in either direction. Markdown, HTML, octet-stream, unknown extensions,
|
|
1002
|
+
and normal binaries are therefore routable only across their authenticated direction.
|
|
1003
|
+
Each outbound file
|
|
1010
1004
|
gets a durable pre-send marker: a failed/ambiguous send becomes `uncertain` and is
|
|
1011
1005
|
not blindly retried, while files not yet attempted remain recoverable after restart.
|
|
1012
1006
|
Logs contain counts and byte totals, never filenames or raw bytes.
|
|
1013
1007
|
|
|
1014
|
-
Owner channels
|
|
1015
|
-
|
|
1016
|
-
cannot reliably distinguish the final answer from thoughts, tool output, or
|
|
1017
|
-
unrelated concurrent work. The config rejects tmux instead of silently offering
|
|
1018
|
-
weaker semantics.
|
|
1008
|
+
Owner channels use the structured agent-session interface for turn-correlated
|
|
1009
|
+
assistant output and reliable automatic replies.
|
|
1019
1010
|
|
|
1020
1011
|
## Codex roles
|
|
1021
1012
|
|
|
@@ -1079,7 +1070,7 @@ role-specific ours daemon, or configure the host default in `~/.ours/config.json
|
|
|
1079
1070
|
|
|
1080
1071
|
Each role can be sandboxed at the environment level via an `isolation:` block —
|
|
1081
1072
|
**fully additive: a role with no block behaves exactly as before.** The agent's
|
|
1082
|
-
|
|
1073
|
+
agent process is wrapped in [bubblewrap](https://github.com/containers/bubblewrap)
|
|
1083
1074
|
(rootless, no setuid), resource-limited by `systemd-run --user --scope`.
|
|
1084
1075
|
|
|
1085
1076
|
An empty `isolation: {}` gives a sensible default posture: filesystem-confined to
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { RoleRecord, RoleCapabilities, RoleStatus } from './types.js';
|
|
2
2
|
export interface CapabilityContext {
|
|
3
|
-
terminalPtyAvailable?: boolean;
|
|
4
3
|
controlProtocolVersion?: number;
|
|
5
4
|
}
|
|
6
5
|
export declare function roleCapabilities(role: RoleRecord, status: RoleStatus, context?: CapabilityContext): RoleCapabilities;
|
|
@@ -1,30 +1,20 @@
|
|
|
1
1
|
export function roleCapabilities(role, status, context = {}) {
|
|
2
|
-
const backend = role.configuredBackend ?? (role.detectedBackend === 'acp'
|
|
2
|
+
const backend = role.configuredBackend ?? (role.detectedBackend === 'acp' ? 'acp' : undefined);
|
|
3
3
|
const online = status.session.reachability === 'online';
|
|
4
4
|
const acp = backend === 'acp';
|
|
5
|
-
const tmux = backend === 'tmux';
|
|
6
5
|
const protocolVersion = context.controlProtocolVersion ?? 1;
|
|
7
6
|
const inactive = status.overall === 'offline'
|
|
8
7
|
|| (!online && status.supervisor.liveness === 'stopped');
|
|
9
|
-
const terminalAvailable = tmux && online && context.terminalPtyAvailable === true;
|
|
10
|
-
const terminalReason = terminalAvailable ? undefined
|
|
11
|
-
: !tmux ? 'wrong_backend'
|
|
12
|
-
: !online ? 'offline'
|
|
13
|
-
: 'pty_unavailable';
|
|
14
8
|
return {
|
|
15
9
|
protocolVersion,
|
|
16
10
|
inventory: true,
|
|
17
11
|
status: true,
|
|
18
12
|
output: {
|
|
19
|
-
recent: acp
|
|
13
|
+
recent: acp, stream: acp && protocolVersion >= 2,
|
|
20
14
|
structured: acp, replayCursor: acp && protocolVersion >= 2,
|
|
21
15
|
},
|
|
22
|
-
input: { text: online &&
|
|
16
|
+
input: { text: online && acp, interrupt: online && acp, steering: false },
|
|
23
17
|
permissions: { observe: acp, respond: acp && online },
|
|
24
|
-
terminal: {
|
|
25
|
-
available: terminalAvailable, reason: terminalReason,
|
|
26
|
-
multiViewer: terminalAvailable, writerLease: terminalAvailable,
|
|
27
|
-
},
|
|
28
18
|
lifecycle: {
|
|
29
19
|
start: role.lifetime === 'permanent' && status.supervisor.liveness !== 'running',
|
|
30
20
|
stop: role.lifetime === 'permanent' && !inactive && status.supervisor.liveness !== 'stopped',
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type FleetErrorCode = 'role_not_found' | 'capability_unavailable' | 'invalid_request' | 'offline' | 'control_unavailable' | 'timeout' | 'rejected' | 'backend_failure' | 'conflict' | 'stale_state' | 'prerequisite_unavailable' | 'idempotency_conflict' | 'rollback_incomplete' | 'launched_unconfirmed' | 'unauthorized' | 'forbidden' | 'rate_limited' | 'incompatible_version' | 'internal';
|
|
1
|
+
export type FleetErrorCode = 'role_not_found' | 'resource_not_found' | 'capability_unavailable' | 'invalid_request' | 'offline' | 'control_unavailable' | 'timeout' | 'rejected' | 'backend_failure' | 'conflict' | 'stale_state' | 'prerequisite_unavailable' | 'idempotency_conflict' | 'rollback_incomplete' | 'launched_unconfirmed' | 'unauthorized' | 'forbidden' | 'rate_limited' | 'incompatible_version' | 'internal';
|
|
2
2
|
export interface FleetErrorShape {
|
|
3
3
|
code: FleetErrorCode;
|
|
4
4
|
message: string;
|
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
import type { SupervisorBackend } from '../supervisor/types.js';
|
|
2
2
|
import { controlRequest } from '../session/control.js';
|
|
3
|
-
import { Tmux } from '../tmux.js';
|
|
4
3
|
import { type CapabilityContext } from './capabilities.js';
|
|
5
4
|
import { RoleRepository } from './role-repository.js';
|
|
6
5
|
import type { RoleCapabilities, RoleRecord, RoleStatus } from './types.js';
|
|
7
6
|
export interface FleetQueryOptions {
|
|
8
7
|
repository: RoleRepository;
|
|
9
8
|
supervisor: SupervisorBackend;
|
|
10
|
-
tmux?: Tmux;
|
|
11
9
|
control?: typeof controlRequest;
|
|
12
10
|
capabilityContext?: CapabilityContext;
|
|
13
11
|
/**
|
|
@@ -24,7 +22,6 @@ export interface FleetQueryOptions {
|
|
|
24
22
|
}
|
|
25
23
|
export declare class FleetQueryService {
|
|
26
24
|
private readonly options;
|
|
27
|
-
private readonly tmux;
|
|
28
25
|
private readonly control;
|
|
29
26
|
constructor(options: FleetQueryOptions);
|
|
30
27
|
list(): Promise<Array<{
|
|
@@ -4,7 +4,6 @@ import { classifyActivity } from '../session/activity.js';
|
|
|
4
4
|
import { controlRequest } from '../session/control.js';
|
|
5
5
|
import { SessionControlError } from '../session/types.js';
|
|
6
6
|
import { readExitRecord, readRestartLedger } from '../runner.js';
|
|
7
|
-
import { Tmux } from '../tmux.js';
|
|
8
7
|
import { roleCapabilities } from './capabilities.js';
|
|
9
8
|
import { FleetError } from './errors.js';
|
|
10
9
|
const clean = (value, max = 512) => value.replace(/[\0-\x08\x0b\x0c\x0e-\x1f\x7f]/g, '').trim().slice(0, max);
|
|
@@ -50,7 +49,7 @@ function sessionOverall(supervisor, session, restart, monitor, isolation, proble
|
|
|
50
49
|
|| problems.some(problem => problem.severity === 'error' || problem.source === 'watchdog')
|
|
51
50
|
|| session.readiness === 'failed')
|
|
52
51
|
return 'attention';
|
|
53
|
-
// A reachable
|
|
52
|
+
// A reachable agent session is the user's live interaction surface. Its
|
|
54
53
|
// evidence remains authoritative even when a service manager no longer owns
|
|
55
54
|
// the process (for example, a detached ACP session with an inactive unit).
|
|
56
55
|
if (session.reachability === 'online'
|
|
@@ -69,11 +68,9 @@ function sessionOverall(supervisor, session, restart, monitor, isolation, proble
|
|
|
69
68
|
}
|
|
70
69
|
export class FleetQueryService {
|
|
71
70
|
options;
|
|
72
|
-
tmux;
|
|
73
71
|
control;
|
|
74
72
|
constructor(options) {
|
|
75
73
|
this.options = options;
|
|
76
|
-
this.tmux = options.tmux ?? new Tmux();
|
|
77
74
|
this.control = options.control ?? controlRequest;
|
|
78
75
|
}
|
|
79
76
|
async list() {
|
|
@@ -174,26 +171,6 @@ export class FleetQueryService {
|
|
|
174
171
|
};
|
|
175
172
|
}
|
|
176
173
|
}
|
|
177
|
-
if (intended === 'tmux' || role.detectedBackend === 'tmux') {
|
|
178
|
-
try {
|
|
179
|
-
const has = await this.tmux.has(role.id);
|
|
180
|
-
return {
|
|
181
|
-
backend: 'tmux', reachability: has ? 'online' : supervisor === 'stopped' ? 'offline' : 'unknown',
|
|
182
|
-
readiness: has ? 'idle' : supervisor === 'running' ? 'starting' : 'failed',
|
|
183
|
-
evidence: 'inferred',
|
|
184
|
-
// tmux exposes no agent-side evidence at all, and `readiness: idle`
|
|
185
|
-
// here is a pane-liveness inference, not an activity claim.
|
|
186
|
-
activity: { state: 'unobservable' },
|
|
187
|
-
};
|
|
188
|
-
}
|
|
189
|
-
catch (error) {
|
|
190
|
-
return {
|
|
191
|
-
backend: 'tmux', reachability: 'unknown', readiness: 'unknown',
|
|
192
|
-
evidence: 'inferred', lastError: clean(error.message),
|
|
193
|
-
activity: { state: 'unobservable' },
|
|
194
|
-
};
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
174
|
return {
|
|
198
175
|
backend: 'unknown', reachability: supervisor === 'stopped' ? 'offline' : 'unknown',
|
|
199
176
|
readiness: supervisor === 'stopped' ? 'failed' : 'unknown', evidence: 'inferred',
|
|
@@ -12,7 +12,7 @@ export interface CreateRoleSessionRequest {
|
|
|
12
12
|
/** null means the selected harness's own default; web blank fields send null. */
|
|
13
13
|
model?: string | null;
|
|
14
14
|
reasoningEffort?: string | null;
|
|
15
|
-
session: 'acp'
|
|
15
|
+
session: 'acp';
|
|
16
16
|
cwd?: string;
|
|
17
17
|
lifetime: 'permanent' | 'temporary';
|
|
18
18
|
mission?: string;
|
|
@@ -41,7 +41,7 @@ export interface CreationCapabilities {
|
|
|
41
41
|
harnesses: Array<{
|
|
42
42
|
id: 'codex' | 'claude-code';
|
|
43
43
|
available: boolean;
|
|
44
|
-
sessions: Array<'acp'
|
|
44
|
+
sessions: Array<'acp'>;
|
|
45
45
|
defaultModel?: string;
|
|
46
46
|
models: string[];
|
|
47
47
|
modelOptions: HarnessModelOption[];
|
|
@@ -71,7 +71,7 @@ export interface CreationPreview {
|
|
|
71
71
|
name: string;
|
|
72
72
|
identity: string;
|
|
73
73
|
harness: string;
|
|
74
|
-
session: 'acp'
|
|
74
|
+
session: 'acp';
|
|
75
75
|
model?: string;
|
|
76
76
|
reasoningEffort?: string;
|
|
77
77
|
cwd?: string;
|
|
@@ -95,7 +95,7 @@ export interface CreationAction {
|
|
|
95
95
|
actionId: string;
|
|
96
96
|
requestHash: string;
|
|
97
97
|
roleId: string;
|
|
98
|
-
session: 'acp'
|
|
98
|
+
session: 'acp';
|
|
99
99
|
lifetime: 'permanent' | 'temporary';
|
|
100
100
|
state: CreationStage;
|
|
101
101
|
stages: Array<{
|
|
@@ -121,7 +121,7 @@ export interface RoleCreationServiceOptions {
|
|
|
121
121
|
tempLauncher?: SupervisorLauncher;
|
|
122
122
|
allowedCwdRoots?: string[];
|
|
123
123
|
journalDir?: string;
|
|
124
|
-
probeReady?: (name: string, session: 'acp'
|
|
124
|
+
probeReady?: (name: string, session: 'acp') => Promise<'ready' | 'attention' | 'unknown'>;
|
|
125
125
|
onProgress?: (action: CreationAction) => void;
|
|
126
126
|
modelCatalogs?: Partial<Record<'codex' | 'claude-code', () => HarnessModelCatalog>>;
|
|
127
127
|
/** Direct/managed callers must not create or restore the web action journal. */
|
|
@@ -109,13 +109,13 @@ export class RoleCreationService {
|
|
|
109
109
|
reasons,
|
|
110
110
|
harnesses: [
|
|
111
111
|
{
|
|
112
|
-
id: 'codex', available: true, sessions: ['acp'
|
|
112
|
+
id: 'codex', available: true, sessions: ['acp'],
|
|
113
113
|
defaultModel: resolveRoleModel(undefined, 'codex', defaults),
|
|
114
114
|
...codexModels, catalogSource: 'codex-runtime-catalog', customModelAllowed: true,
|
|
115
115
|
warnings: codexCatalog.warnings,
|
|
116
116
|
},
|
|
117
117
|
{
|
|
118
|
-
id: 'claude-code', available: true, sessions: ['acp'
|
|
118
|
+
id: 'claude-code', available: true, sessions: ['acp'],
|
|
119
119
|
defaultModel: resolveRoleModel(undefined, 'claude-code', defaults),
|
|
120
120
|
...claudeModels, catalogSource: 'claude-adapter-2.1', customModelAllowed: true,
|
|
121
121
|
warnings: claudeCatalog.warnings,
|
|
@@ -149,7 +149,7 @@ export class RoleCreationService {
|
|
|
149
149
|
const effective = {
|
|
150
150
|
name: request.name, identity: request.name,
|
|
151
151
|
harness: request.harness ?? defaults.harness ?? 'claude-code',
|
|
152
|
-
session: request.session ?? defaults.session ?? '
|
|
152
|
+
session: request.session ?? defaults.session ?? 'acp',
|
|
153
153
|
model: resolveRoleModel(request.model, request.harness, defaults),
|
|
154
154
|
reasoningEffort: request.reasoningEffort ?? undefined,
|
|
155
155
|
cwd, lifetime: request.lifetime,
|
|
@@ -264,9 +264,7 @@ export class RoleCreationService {
|
|
|
264
264
|
this.stage(action, 'waiting_for_session');
|
|
265
265
|
const ready = await this.waitForReady(action.roleId, action.session);
|
|
266
266
|
if (ready === 'ready') {
|
|
267
|
-
action.openPath = action.
|
|
268
|
-
? `/roles/${encodeURIComponent(action.roleId)}/activity`
|
|
269
|
-
: `/roles/${encodeURIComponent(action.roleId)}/terminal`;
|
|
267
|
+
action.openPath = `/roles/${encodeURIComponent(action.roleId)}/activity`;
|
|
270
268
|
this.stage(action, 'session_reachable', 'session is reachable; identity binding has no structured evidence in this version');
|
|
271
269
|
}
|
|
272
270
|
else if (ready === 'attention') {
|
|
@@ -311,7 +309,7 @@ export class RoleCreationService {
|
|
|
311
309
|
throw new FleetError('invalid_request', 'invalid role name');
|
|
312
310
|
if (!['codex', 'claude-code'].includes(input.harness))
|
|
313
311
|
throw new FleetError('invalid_request', 'unsupported harness');
|
|
314
|
-
if (
|
|
312
|
+
if (input.session !== 'acp')
|
|
315
313
|
throw new FleetError('invalid_request', 'unsupported session backend');
|
|
316
314
|
if (!['permanent', 'temporary'].includes(input.lifetime))
|
|
317
315
|
throw new FleetError('invalid_request', 'unsupported lifetime');
|
|
@@ -3,9 +3,8 @@ export interface RoleRepositoryOptions {
|
|
|
3
3
|
configPath?: string;
|
|
4
4
|
permanentRoot?: string;
|
|
5
5
|
temporaryRoot?: string;
|
|
6
|
-
probeBackend?: (name: string, intended?: 'acp'
|
|
6
|
+
probeBackend?: (name: string, intended?: 'acp') => Promise<{
|
|
7
7
|
acp: boolean;
|
|
8
|
-
tmux: boolean;
|
|
9
8
|
}>;
|
|
10
9
|
concurrency?: number;
|
|
11
10
|
timeoutMs?: number;
|
|
@@ -42,7 +42,11 @@ function snapshot(path, name) {
|
|
|
42
42
|
const raw = parse(readFileSync(path, 'utf8'));
|
|
43
43
|
if (!raw || typeof raw !== 'object')
|
|
44
44
|
throw new Error('snapshot must be a mapping');
|
|
45
|
-
|
|
45
|
+
if (raw.session === 'tmux')
|
|
46
|
+
throw new Error('session: tmux is no longer supported; use session: acp with the Codex or Claude Code adapter');
|
|
47
|
+
if (raw.session !== undefined && raw.session !== 'acp')
|
|
48
|
+
throw new Error(`unsupported session '${String(raw.session)}'; expected session: acp`);
|
|
49
|
+
const session = 'acp';
|
|
46
50
|
return {
|
|
47
51
|
role: {
|
|
48
52
|
...raw, name, harness: typeof raw.harness === 'string' ? raw.harness : 'claude-code',
|
|
@@ -167,14 +171,10 @@ export class RoleRepository {
|
|
|
167
171
|
const timeoutMs = this.options.timeoutMs ?? 2_000;
|
|
168
172
|
const result = await Promise.race([
|
|
169
173
|
this.options.probeBackend(name, intended),
|
|
170
|
-
new Promise(resolve => setTimeout(() => resolve({ acp: false
|
|
171
|
-
]).catch(() => ({ acp: false
|
|
172
|
-
if (result.acp && result.tmux)
|
|
173
|
-
return 'ambiguous';
|
|
174
|
+
new Promise(resolve => setTimeout(() => resolve({ acp: false }), timeoutMs)),
|
|
175
|
+
]).catch(() => ({ acp: false }));
|
|
174
176
|
if (result.acp)
|
|
175
177
|
return 'acp';
|
|
176
|
-
if (result.tmux)
|
|
177
|
-
return 'tmux';
|
|
178
178
|
return 'none';
|
|
179
179
|
}
|
|
180
180
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { controlRequest } from '../session/control.js';
|
|
2
2
|
import type { ConversationEventV1, ConversationSnapshot, PromptReceipt } from '../session/conversation-types.js';
|
|
3
3
|
import type { SessionSnapshot } from '../session/types.js';
|
|
4
|
-
import { Tmux } from '../tmux.js';
|
|
5
4
|
import type { OutputPage, SendReceipt, SessionDescriptor } from './types.js';
|
|
6
5
|
export interface ConversationPageView {
|
|
7
6
|
events: ConversationEventV1[];
|
|
@@ -120,14 +119,3 @@ export declare class AcpRoleSessionAdapter implements RoleSessionControl {
|
|
|
120
119
|
}>;
|
|
121
120
|
private call;
|
|
122
121
|
}
|
|
123
|
-
export declare class TmuxRoleSessionAdapter implements RoleSessionControl {
|
|
124
|
-
private readonly roleId;
|
|
125
|
-
private readonly tmux;
|
|
126
|
-
constructor(roleId: string, tmux?: Tmux);
|
|
127
|
-
describe(): Promise<SessionDescriptor>;
|
|
128
|
-
snapshot(): Promise<SessionSnapshot>;
|
|
129
|
-
recentOutput(request?: {
|
|
130
|
-
limit?: number;
|
|
131
|
-
}): Promise<OutputPage>;
|
|
132
|
-
sendText(text: string): Promise<SendReceipt>;
|
|
133
|
-
}
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto';
|
|
2
1
|
import { controlRequest, followConversation } from '../session/control.js';
|
|
3
|
-
import { Tmux } from '../tmux.js';
|
|
4
2
|
import { FleetError, normalizeError } from './errors.js';
|
|
5
3
|
const visibleEvents = (events) => events.filter(event => event.kind !== 'thought');
|
|
6
4
|
export class AcpRoleSessionAdapter {
|
|
@@ -149,43 +147,3 @@ export class AcpRoleSessionAdapter {
|
|
|
149
147
|
}
|
|
150
148
|
}
|
|
151
149
|
}
|
|
152
|
-
export class TmuxRoleSessionAdapter {
|
|
153
|
-
roleId;
|
|
154
|
-
tmux;
|
|
155
|
-
constructor(roleId, tmux = new Tmux()) {
|
|
156
|
-
this.roleId = roleId;
|
|
157
|
-
this.tmux = tmux;
|
|
158
|
-
}
|
|
159
|
-
async describe() {
|
|
160
|
-
return { backend: 'tmux', protocolVersion: 1, features: ['text', 'capture'] };
|
|
161
|
-
}
|
|
162
|
-
async snapshot() {
|
|
163
|
-
const alive = await this.tmux.has(this.roleId);
|
|
164
|
-
return { backend: 'tmux', alive, readiness: alive ? 'idle' : 'failed' };
|
|
165
|
-
}
|
|
166
|
-
async recentOutput(request = {}) {
|
|
167
|
-
try {
|
|
168
|
-
const text = await this.tmux.capture(this.roleId, Math.min(request.limit ?? 100, 500));
|
|
169
|
-
return { events: [], text, truncated: false };
|
|
170
|
-
}
|
|
171
|
-
catch (error) {
|
|
172
|
-
throw normalizeError(error);
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
async sendText(text) {
|
|
176
|
-
if (!text.trim())
|
|
177
|
-
throw new FleetError('invalid_request', 'text is required');
|
|
178
|
-
if (Buffer.byteLength(text) > 32 * 1024)
|
|
179
|
-
throw new FleetError('invalid_request', 'text exceeds 32 KiB');
|
|
180
|
-
try {
|
|
181
|
-
await this.tmux.sendText(this.roleId, text);
|
|
182
|
-
}
|
|
183
|
-
catch (error) {
|
|
184
|
-
throw normalizeError(error);
|
|
185
|
-
}
|
|
186
|
-
return {
|
|
187
|
-
accepted: true, promptId: randomUUID(), queuedBehind: 0, terminalOutcomeKnown: false,
|
|
188
|
-
detail: 'literal text and Enter sent; terminal outcome is unknown',
|
|
189
|
-
};
|
|
190
|
-
}
|
|
191
|
-
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { InterruptOutcome, QueuedPrompt,
|
|
1
|
+
import type { AgentSession, InterruptOutcome, QueuedPrompt, SubmitPromptOptions, TurnCancellationSource } from '../session/types.js';
|
|
2
2
|
/** Thin in-process session mutation kernel. Callers retain every policy decision. */
|
|
3
|
-
export declare const queueSessionPrompt: (session:
|
|
4
|
-
export declare const interruptSession: (session:
|
|
5
|
-
export declare const respondSessionPermission: (session:
|
|
3
|
+
export declare const queueSessionPrompt: (session: AgentSession, text: string, options?: SubmitPromptOptions) => Promise<QueuedPrompt>;
|
|
4
|
+
export declare const interruptSession: (session: AgentSession, source: TurnCancellationSource) => Promise<InterruptOutcome>;
|
|
5
|
+
export declare const respondSessionPermission: (session: AgentSession, permissionId: string, optionId: string) => boolean;
|
|
6
6
|
export type GenerationPermissionResult = 'accepted' | 'stale' | 'unavailable';
|
|
7
|
-
export declare const respondSessionPermissionV2: (session:
|
|
7
|
+
export declare const respondSessionPermissionV2: (session: AgentSession, permissionId: string, optionId: string, sessionGeneration: string) => GenerationPermissionResult;
|