@ours.network/fleet 1.1.0-nightly.2 → 1.1.0-nightly.20
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 +169 -54
- package/dist/agent-recovery-gate.d.ts +17 -0
- package/dist/agent-recovery-gate.js +135 -0
- package/dist/application/capabilities.js +18 -6
- package/dist/application/fleet-query-service.js +20 -5
- package/dist/application/role-command-service.js +9 -0
- package/dist/application/role-creation-service.d.ts +6 -25
- package/dist/application/role-creation-service.js +34 -72
- package/dist/application/role-repository.d.ts +4 -2
- package/dist/application/role-repository.js +6 -3
- package/dist/application/session-control.d.ts +3 -1
- package/dist/application/session-control.js +4 -2
- package/dist/application/task-room-service.d.ts +58 -6
- package/dist/application/task-room-service.js +426 -78
- package/dist/briefing.js +74 -24
- package/dist/build-info.json +4 -4
- package/dist/canonical-json.d.ts +2 -0
- package/dist/canonical-json.js +10 -0
- package/dist/cli.js +227 -73
- package/dist/config-yaml.d.ts +1 -1
- package/dist/config.d.ts +67 -2
- package/dist/config.js +260 -30
- package/dist/daemon-recovery.d.ts +93 -0
- package/dist/daemon-recovery.js +328 -0
- package/dist/docs.d.ts +1 -1
- package/dist/docs.js +171 -66
- package/dist/doctor.d.ts +2 -0
- package/dist/doctor.js +54 -4
- package/dist/fleet-command-audit.d.ts +165 -0
- package/dist/fleet-command-audit.js +543 -0
- package/dist/fleet-proxy.d.ts +7 -2
- package/dist/fleet-proxy.js +30 -8
- package/dist/harness/agent-session.d.ts +5 -0
- package/dist/harness/claude-code-session.d.ts +2 -0
- package/dist/harness/claude-code-session.js +6 -1
- package/dist/harness/claude-code.js +2 -0
- package/dist/harness/codex-app-server-proxy.d.ts +40 -0
- package/dist/harness/codex-app-server-proxy.js +348 -14
- package/dist/harness/codex-session.d.ts +11 -2
- package/dist/harness/codex-session.js +22 -2
- package/dist/harness/codex.d.ts +5 -1
- package/dist/harness/codex.js +110 -10
- package/dist/harness/types.d.ts +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.js +4 -3
- package/dist/init-guidance.d.ts +1 -1
- package/dist/init-guidance.js +4 -3
- package/dist/init-wizard.d.ts +98 -0
- package/dist/init-wizard.js +629 -0
- package/dist/lifecycle-summary.d.ts +118 -0
- package/dist/lifecycle-summary.js +161 -0
- package/dist/loops/config.js +3 -2
- package/dist/model-env.d.ts +2 -2
- package/dist/model-env.js +1 -1
- package/dist/model-recovery.d.ts +1 -1
- package/dist/ops.js +2 -17
- package/dist/owner-channel/channel.d.ts +58 -0
- package/dist/owner-channel/channel.js +321 -42
- package/dist/owner-channel/commands.d.ts +6 -1
- package/dist/owner-channel/commands.js +34 -19
- package/dist/owner-channel/state.d.ts +2 -0
- package/dist/owner-channel/state.js +8 -2
- package/dist/permissions.js +1 -1
- package/dist/preset-bootstrap.d.ts +10 -0
- package/dist/preset-bootstrap.js +85 -0
- package/dist/preset-migration.d.ts +42 -0
- package/dist/preset-migration.js +414 -0
- package/dist/reports/artifact.d.ts +10 -0
- package/dist/reports/artifact.js +32 -0
- package/dist/reports/index.d.ts +5 -0
- package/dist/reports/index.js +5 -0
- package/dist/reports/render.d.ts +6 -0
- package/dist/reports/render.js +240 -0
- package/dist/reports/service.d.ts +42 -0
- package/dist/reports/service.js +57 -0
- package/dist/reports/tasks.d.ts +52 -0
- package/dist/reports/tasks.js +177 -0
- package/dist/reports/types.d.ts +315 -0
- package/dist/reports/types.js +6 -0
- package/dist/resolved-plan.js +4 -0
- package/dist/rooms-tasks/cli.d.ts +2 -0
- package/dist/rooms-tasks/cli.js +503 -75
- package/dist/rooms-tasks/close.d.ts +9 -1
- package/dist/rooms-tasks/close.js +20 -4
- package/dist/rooms-tasks/config.js +16 -14
- package/dist/rooms-tasks/cowork-adapter.d.ts +1 -0
- package/dist/rooms-tasks/cowork-adapter.js +6 -0
- package/dist/rooms-tasks/deletion.d.ts +51 -0
- package/dist/rooms-tasks/deletion.js +217 -0
- package/dist/rooms-tasks/external-worker.d.ts +1 -0
- package/dist/rooms-tasks/external-worker.js +14 -9
- package/dist/rooms-tasks/launch-snapshot.d.ts +19 -0
- package/dist/rooms-tasks/launch-snapshot.js +176 -0
- package/dist/rooms-tasks/member-overrides.d.ts +27 -0
- package/dist/rooms-tasks/member-overrides.js +170 -0
- package/dist/rooms-tasks/provision.js +179 -49
- package/dist/rooms-tasks/room-state.d.ts +1 -0
- package/dist/rooms-tasks/room-state.js +12 -1
- package/dist/rooms-tasks/task-state.d.ts +115 -3
- package/dist/rooms-tasks/task-state.js +361 -11
- package/dist/rooms-tasks/templates.d.ts +4 -2
- package/dist/rooms-tasks/templates.js +42 -78
- package/dist/rooms-tasks/terminal.d.ts +3 -0
- package/dist/rooms-tasks/terminal.js +7 -3
- package/dist/rooms-tasks/types.d.ts +94 -17
- package/dist/rooms-tasks/types.js +14 -4
- package/dist/runner.d.ts +9 -2
- package/dist/runner.js +107 -40
- package/dist/session/acp.d.ts +9 -1
- package/dist/session/acp.js +35 -9
- package/dist/session/arbiter.d.ts +2 -1
- package/dist/session/arbiter.js +2 -0
- package/dist/session/codex-app-server-transport.d.ts +53 -0
- package/dist/session/codex-app-server-transport.js +184 -0
- package/dist/session/codex-app-server.d.ts +122 -0
- package/dist/session/codex-app-server.js +1031 -0
- package/dist/session/control.d.ts +14 -1
- package/dist/session/control.js +38 -0
- package/dist/session/conversation-types.d.ts +4 -4
- package/dist/session/types.d.ts +24 -6
- package/dist/session/types.js +28 -0
- package/dist/spawn.d.ts +6 -31
- package/dist/spawn.js +57 -234
- package/dist/temp-lifecycle.d.ts +1 -1
- package/dist/watchdog/config.d.ts +10 -7
- package/dist/watchdog/config.js +29 -19
- package/dist/watchdog/run.js +4 -13
- package/dist/web/fleet-config-service.d.ts +1 -0
- package/dist/web/fleet-config-service.js +27 -8
- package/dist/web/runtime.js +11 -6
- package/dist/web/server.js +28 -1
- package/dist/web/topology-promote.js +8 -1
- package/dist/web-app/assets/index-C0h3ALvs.js +10 -0
- package/dist/web-app/index.html +1 -1
- package/package.json +3 -2
- package/presets/brain-catalog.json +16 -0
- package/presets/fleet/agent_templates/Critic.yaml +4 -0
- package/presets/fleet/agent_templates/Developer.yaml +4 -0
- package/presets/fleet/agent_templates/LocalCoordinator.yaml +4 -0
- package/presets/fleet/agents/FleetCoordinator.yaml +3 -0
- package/presets/fleet/brains/claude-default.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-high.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-low.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-max.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-medium.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-xhigh.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-high.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-low.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-max.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-medium.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-xhigh.yaml +5 -0
- package/presets/fleet/brains/claude-sonnet-5-high.yaml +5 -0
- package/presets/fleet/brains/claude-sonnet-5-low.yaml +5 -0
- package/presets/fleet/brains/claude-sonnet-5-max.yaml +5 -0
- package/presets/fleet/brains/claude-sonnet-5-medium.yaml +5 -0
- package/presets/fleet/brains/claude-sonnet-5-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-3-codex-spark-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-3-codex-spark-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-3-codex-spark-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-3-codex-spark-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-mini-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-mini-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-mini-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-mini-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-5-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-5-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-5-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-5-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-luna-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-luna-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-luna-max.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-luna-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-luna-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-sol-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-sol-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-sol-max.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-sol-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-sol-ultra.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-sol-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-terra-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-terra-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-terra-max.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-terra-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-terra-ultra.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-terra-xhigh.yaml +5 -0
- package/presets/fleet/roles/Coordinator.yaml +30 -0
- package/presets/fleet/roles/Critic.yaml +19 -0
- package/presets/fleet/roles/Developer.yaml +19 -0
- package/presets/fleet/roles/LocalCoordinator.yaml +25 -0
- package/presets/fleet/room_templates/pair.yaml +10 -0
- package/presets/fleet/room_templates/single.yaml +7 -0
- package/presets/fleet/room_templates/team.yaml +12 -0
- package/presets/fleet.yaml +7 -0
- package/presets/manifest.json +6 -0
- package/dist/web-app/assets/index-BbE9EX6n.js +0 -10
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ 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 structured
|
|
11
|
+
- **run through structured managed sessions** behind one provider-neutral
|
|
12
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,
|
|
@@ -37,16 +37,16 @@ brain: { inline: { harness: claude-code, session: acp } }
|
|
|
37
37
|
|
|
38
38
|
# ~/fleet/agents/Prototyper.yaml
|
|
39
39
|
role: { inline: { mission: Build prototypes } }
|
|
40
|
-
brain: { inline: { harness: codex, session:
|
|
40
|
+
brain: { inline: { harness: codex, session: codex-app-server } }
|
|
41
41
|
```
|
|
42
42
|
|
|
43
43
|
## How it works
|
|
44
44
|
|
|
45
45
|
```
|
|
46
|
-
~/fleet.yaml + ~/fleet/{agents,roles,brains}/*.yaml your declaration
|
|
46
|
+
~/fleet.yaml + ~/fleet/{agents,agent_templates,roles,brains,room_templates}/*.yaml your declaration
|
|
47
47
|
│ ours-fleet up
|
|
48
48
|
▼
|
|
49
|
-
briefing.md per role ──► agent session adapter ──►
|
|
49
|
+
briefing.md per role ──► agent session adapter ──► native app-server or ACP transport
|
|
50
50
|
▲ │
|
|
51
51
|
systemd --user / launchd ───────┘ restart on crash, start at boot/login
|
|
52
52
|
```
|
|
@@ -72,7 +72,7 @@ The state dir contract:
|
|
|
72
72
|
| `.owner-channel-message-recovery.json` | owner-channel bridge | mode-0600 body-free message claim journal: wire ID, history sequence, and claim time only |
|
|
73
73
|
| `.owner-channel-attachment-recovery.json` | owner-channel bridge | mode-0600 attachment route journal; never filenames, paths, transcript text, or bytes |
|
|
74
74
|
| `.owner-channel-binder.lock/`, `.owner-channel-binder.json` | owner-channel supervisor | mode-0600 role/identity + PID/start-marker ownership and release metadata; never mail plaintext or credentials |
|
|
75
|
-
| `.session-events.jsonl`, `.control.sock`, `.control-token` |
|
|
75
|
+
| `.session-events.jsonl`, `.control.sock`, `.control-token` | managed session backend | bounded typed console projection and private attachment control |
|
|
76
76
|
|
|
77
77
|
## Prerequisites
|
|
78
78
|
|
|
@@ -91,15 +91,16 @@ equivalent); logs land in `~/.ours-fleet/logs/`.
|
|
|
91
91
|
|
|
92
92
|
```sh
|
|
93
93
|
npm i -g @ours.network/fleet
|
|
94
|
-
ours-fleet init #
|
|
94
|
+
ours-fleet init # interactive reviewed defaults + host setup
|
|
95
95
|
ours-fleet doctor # verifies everything above, with actionable messages
|
|
96
96
|
```
|
|
97
97
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
98
|
+
Native Codex roles use the logged-in Codex CLI's `app-server` command directly.
|
|
99
|
+
The maintained Codex and Claude ACP adapters remain bundled optional dependencies
|
|
100
|
+
and are resolved internally; users do not install adapter commands or add them to
|
|
101
|
+
`PATH`. Explicit `session_options.codex_app_server.command` and
|
|
102
|
+
`session_options.acp.command` overrides remain available. On Node 20–21, native
|
|
103
|
+
Codex and Codex ACP remain available, while maintained Claude ACP requires Node 22.
|
|
103
104
|
|
|
104
105
|
Each OS user manages their own fleet — to host roles under a sandboxed account,
|
|
105
106
|
become that account and repeat.
|
|
@@ -107,8 +108,8 @@ become that account and repeat.
|
|
|
107
108
|
## Quickstart
|
|
108
109
|
|
|
109
110
|
```sh
|
|
110
|
-
|
|
111
|
-
|
|
111
|
+
ours-fleet init # interactive; adds missing defaults and preserves existing files
|
|
112
|
+
ours-fleet config # validates Agents, Roles, Brains, and Room templates
|
|
112
113
|
$EDITOR ~/fleet/agents/*.yaml # compose Role + Brain and operational settings
|
|
113
114
|
ours-fleet up # boot the fleet (staggered)
|
|
114
115
|
ours-fleet ls # running consoles
|
|
@@ -126,19 +127,16 @@ ours-fleet spawn Worker --mission "own the worker repo" \
|
|
|
126
127
|
ours-fleet spawn --temp Scout --mission "one-off research" # gone on exit/reboot
|
|
127
128
|
|
|
128
129
|
# Codex role: ours-codex is preferred automatically; plain codex is the fallback
|
|
129
|
-
ours-fleet spawn Coder --
|
|
130
|
-
--
|
|
131
|
-
--profile fleet --search --monitor --coordinator FleetCoordinator
|
|
132
|
-
# Note: --monitor is legacy consent for Codex's native monitor. Choose the
|
|
133
|
-
# wake owner separately in fleet.yaml with monitor.mode: fleet|native.
|
|
130
|
+
ours-fleet spawn Coder --brain codex-fleet --role developer \
|
|
131
|
+
--approval ask --filesystem workspace --coordinator FleetCoordinator
|
|
134
132
|
```
|
|
135
133
|
|
|
136
|
-
Inside a managed
|
|
137
|
-
that role's live supervisor. `ours-fleet spawn
|
|
138
|
-
minimal form: omitted
|
|
139
|
-
fleet monitor policy
|
|
140
|
-
|
|
141
|
-
|
|
134
|
+
Inside a managed role, `ours-fleet spawn` is transparently routed through
|
|
135
|
+
that role's live supervisor. `ours-fleet spawn DeveloperX --temp` is the
|
|
136
|
+
minimal form: omitted Brain/Role selections, cwd, coordinator, neutral permissions,
|
|
137
|
+
and fleet monitor policy inherit from the caller. Identity, environment, owner routing,
|
|
138
|
+
room startup, and sensitive inline Brain values do not. Explicit flags win. After
|
|
139
|
+
creation succeeds, fleet can deterministically notify
|
|
142
140
|
the caller's owner channel with the caller and spawned-role details. This is an
|
|
143
141
|
honest-actor convenience and attribution path, not a security boundary; host
|
|
144
142
|
shells and deliberately bypassed absolute binaries retain direct behavior.
|
|
@@ -305,7 +303,7 @@ ours-fleet up|down|restart|force-restart [-c FILE] [Name...]
|
|
|
305
303
|
ours-fleet config [-c FILE] validate + print merged plan
|
|
306
304
|
ours-fleet ls | attach | peek | logs [-f] | status <Name>
|
|
307
305
|
ours-fleet send <Name> "text"
|
|
308
|
-
ours-fleet spawn [--temp] [<Name> | --
|
|
306
|
+
ours-fleet spawn [--temp] [<Name> | --name <Name>] --brain <ID|inline:{...}> --role <ID|inline:{...}> [--approval ...]
|
|
309
307
|
ours-fleet loops validate|list|status
|
|
310
308
|
ours-fleet loops reload <Role>
|
|
311
309
|
ours-fleet loops run-now|disable|enable <Role> <Loop>
|
|
@@ -359,13 +357,42 @@ defaults:
|
|
|
359
357
|
max_kb: 1024 # rotate only above this active-log size
|
|
360
358
|
keep_tail_kb: 256 # UTF-8 tail; line-aligned when one fits
|
|
361
359
|
max_archives: 12 # recent beside log; older preserved cold
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
Agent Template document (\`~/fleet/agent_templates/Worker.yaml\`) is inert and reusable:
|
|
363
|
+
|
|
364
|
+
```yaml
|
|
365
|
+
role: { ref: developer }
|
|
366
|
+
brain: { ref: claude-default }
|
|
367
|
+
permissions: { approval: ask, filesystem: workspace, unattended: deny }
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
Persistent Agent instance (\`~/fleet/agents/Name.yaml\`) either remains a canonical
|
|
371
|
+
Role/Brain composition or explicitly reuses a template:
|
|
372
|
+
|
|
373
|
+
```yaml
|
|
374
|
+
template: Worker
|
|
375
|
+
overrides:
|
|
376
|
+
cwd: /work/name
|
|
377
|
+
permissions: { approval: allow }
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
Templates never create identities, supervisors, sessions, or lifecycle/status rows.
|
|
381
|
+
Map overrides merge recursively; scalars and arrays replace; null deletion is rejected.
|
|
382
|
+
|
|
383
|
+
Canonical Agent document (\`~/fleet/agents/Name.yaml\`):
|
|
384
|
+
|
|
385
|
+
```yaml
|
|
386
|
+
role: { ref: RoleID }
|
|
387
|
+
brain: { ref: BrainID } # Brain owns harness/model/session/reasoning
|
|
388
|
+
identity: "Display Name" # ours identity to bind (default: Name)
|
|
389
|
+
cwd: ${work_root}/repo # where the harness process runs
|
|
390
|
+
coordinator: FleetCoordinator # announce target on boot
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
Additional operational field inventory (schematic, not one YAML document):
|
|
394
|
+
|
|
395
|
+
```text
|
|
369
396
|
identity: "Display Name" # ours identity to bind (default: Name)
|
|
370
397
|
cwd: ${work_root}/repo # where the harness process runs
|
|
371
398
|
coordinator: FleetCoordinator # announce target on boot
|
|
@@ -377,13 +404,13 @@ Agent document:
|
|
|
377
404
|
- file_received # local_contact_request, pending_message)
|
|
378
405
|
batch_ms: 2000 # coalesce a burst into one line (default 2000)
|
|
379
406
|
inject: notification # notification (default) | full (bodies inline; roadmap)
|
|
380
|
-
owner_channel: # optional trusted owner ingress; requires session
|
|
407
|
+
owner_channel: # optional trusted owner ingress; requires a managed session
|
|
381
408
|
identity: "Name Owner Channel" # dedicated identity; fleet creates it with safe local defaults
|
|
382
409
|
owners: [owner-contact-cid] # authenticated ours contact IDs, never display names
|
|
383
410
|
agent: managed-agent-cid # exact role CID allowed to relay messages/files outward
|
|
384
411
|
interrupt: false # false queues; true cancels current work first
|
|
385
412
|
progress_interval_ms: 30000 # fleet-generated progress notices; 0 disables
|
|
386
|
-
comments: true # relay live "🟡 Live update:"
|
|
413
|
+
comments: true # relay live "🟡 Live update:" structured commentary (default true);
|
|
387
414
|
# restart baseline for /comments on|off
|
|
388
415
|
attachments: # secure inbound documents, images, and voice
|
|
389
416
|
enabled: true
|
|
@@ -403,13 +430,10 @@ Agent document:
|
|
|
403
430
|
# mem_palace: false # claude-code: disable memory plugin
|
|
404
431
|
# permission_mode: dontAsk # claude-code: launch permission mode —
|
|
405
432
|
# one of default | acceptEdits | plan | dontAsk | bypassPermissions
|
|
406
|
-
#
|
|
407
|
-
#
|
|
408
|
-
# approval: never # codex: --ask-for-approval —
|
|
409
|
-
# one of untrusted | on-request | never
|
|
410
|
-
# permission_mode: never # codex alias for approval
|
|
433
|
+
# Codex ACP compatibility only: native codex-app-server rejects Codex
|
|
434
|
+
# permission overrides here; use the common permissions block above.
|
|
411
435
|
# launcher: auto # codex: auto | ours-codex | codex
|
|
412
|
-
# profile: fleet # codex: $CODEX_HOME/fleet.config.toml
|
|
436
|
+
# profile: fleet # codex ACP only: $CODEX_HOME/fleet.config.toml
|
|
413
437
|
# search: true # codex: enable --search (live web search)
|
|
414
438
|
# add_dirs: [/data/shared] # codex: repeat --add-dir
|
|
415
439
|
# monitor: true # explicit persistent consent to arm mail wake
|
|
@@ -449,6 +473,62 @@ installer/setup flows remain responsible for starting it.
|
|
|
449
473
|
|
|
450
474
|
### Rooms and tasks
|
|
451
475
|
|
|
476
|
+
Init installs editable `single`, `pair`, and `team` definitions under
|
|
477
|
+
`~/fleet/room_templates/`, plus every exact-cased Agent, Role, and Brain they
|
|
478
|
+
reference. Inspect them with `ours-fleet template list` and
|
|
479
|
+
`ours-fleet template show team`. After configuring the authenticated owner below:
|
|
480
|
+
|
|
481
|
+
```sh
|
|
482
|
+
ours-fleet task create --title "Solo task" --template single
|
|
483
|
+
ours-fleet task create --title "Reviewed change" --template pair
|
|
484
|
+
ours-fleet task create --title "Phased delivery" --template team
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
`ours-fleet init -c /path/custom.yaml` reviews the literal resolved manifest and
|
|
488
|
+
`/path/custom/` split directory before doing anything. It requires a TTY and two
|
|
489
|
+
default-No confirmations. You select Codex, Claude, or both; then either one explicit
|
|
490
|
+
model for every job or explicit development/review/coordination models; then one
|
|
491
|
+
Quick/Balanced/Thorough reasoning level (`low`/`medium`/`high`). Catalog entries are
|
|
492
|
+
supported IDs, not recommendations or entitlement claims: use `ours-fleet doctor` for
|
|
493
|
+
local Codex availability, while Claude entitlement is validated at launch. No
|
|
494
|
+
`model_chain` is generated and Fleet never silently substitutes another model.
|
|
495
|
+
|
|
496
|
+
A successful rerun adds any missing packaged defaults while preserving every existing
|
|
497
|
+
manifest, Role, Agent Template, and Room Template byte. Explicit adoption of newer defaults
|
|
498
|
+
is a separate reviewable migration. At either confirmation,
|
|
499
|
+
N, Enter, Escape, Ctrl-C, Ctrl-D, or EOF cancels. In a picker, Escape, Ctrl-C, Ctrl-D,
|
|
500
|
+
or EOF cancels; Enter records the highlighted choice (or continues a non-empty multi-select),
|
|
501
|
+
N is ignored, and an empty subscription selection remains blocked. Every cancellation
|
|
502
|
+
before the final Yes performs no host setup or config publication. Non-TTY use stops with
|
|
503
|
+
the same zero-mutation guarantee. After the final Yes, host integration runs before locked
|
|
504
|
+
publication; a hard kill, power loss, or host crash can therefore leave host integration
|
|
505
|
+
or private stage/recovery evidence to inspect.
|
|
506
|
+
Existing targets and their tree must be owner-private regular files/directories on the
|
|
507
|
+
configuration parent's filesystem; symlinks, foreign ownership, unsafe modes, and a
|
|
508
|
+
non-owner-controlled parent fail closed before host setup and are rechecked under the
|
|
509
|
+
per-setup init lock before publication.
|
|
510
|
+
|
|
511
|
+
The generated task experiences are fixed consequences, not extra questions: `single`
|
|
512
|
+
uses `Developer`; `pair` uses `Developer` with an independent `Critic`; and `team` uses
|
|
513
|
+
task-local `LocalCoordinator`, `Developer`, and `Critic`. The persistent
|
|
514
|
+
`FleetCoordinator` uses the separate packaged `Coordinator` contract and coordination
|
|
515
|
+
model.
|
|
516
|
+
|
|
517
|
+
Revision-3 role defaults have an explicit fail-closed adoption command: first run
|
|
518
|
+
`ours-fleet migrate-role-defaults -c FILE` for a zero-write plan, review every removal,
|
|
519
|
+
replacement, addition, preserved custom file, staging path, and recovery path, then rerun
|
|
520
|
+
with `--write`. Only exact semantic matches for packaged-bootstrap and generated
|
|
521
|
+
revision-3 forms are changed; same-named customized files remain byte-identical, and a
|
|
522
|
+
dangling custom reference refuses publication. Rerunning the migration is a no-op.
|
|
523
|
+
|
|
524
|
+
For manual adoption of a single newer packaged file, copy the reported source beside the
|
|
525
|
+
target as `.new-default`, inspect `diff -u TARGET TARGET.new-default`, then replace the
|
|
526
|
+
target yourself. Rerunning init is not an update. Users with the still older exact generated
|
|
527
|
+
six-worker starter Agent set must first run `ours-fleet migrate-agent-templates -c FILE`
|
|
528
|
+
for its zero-write plan, review every move/addition/recovery path, and rerun with `--write`.
|
|
529
|
+
Customized or ambiguous known starters are refused without mutation; unrelated custom
|
|
530
|
+
persistent Agents are preserved.
|
|
531
|
+
|
|
452
532
|
Rooms always use `ours-cowork`; there is no room-provider selector. Configure
|
|
453
533
|
the cowork daemon connection and the room owner directly:
|
|
454
534
|
|
|
@@ -477,6 +557,13 @@ Cowork role, and task. The agent creates that identity itself with ours MCP
|
|
|
477
557
|
work immediately. Fleet activates the room from Cowork's authenticated seat; there
|
|
478
558
|
is no briefing hash, startup ACK, or separate role-briefing readiness gate.
|
|
479
559
|
|
|
560
|
+
Set `room.anonymous: true` on a room template, or pass `--anonymous` to
|
|
561
|
+
`task create`, `task start`, `task work`, or `room create`, to create an
|
|
562
|
+
anonymous Cowork room. `--no-anonymous` explicitly overrides an anonymous
|
|
563
|
+
template. Fleet records the resolved value before room creation so retries keep
|
|
564
|
+
the same choice. Temporary members of an anonymous room are instructed to call
|
|
565
|
+
`create_temporary_identity` with `expose_local=false`.
|
|
566
|
+
|
|
480
567
|
Human task and room results use the same compact Markdown presentation in the
|
|
481
568
|
CLI and authenticated owner channel: a short heading, icon-plus-word status,
|
|
482
569
|
code-formatted identifiers, bounded summaries, and actionable recovery or error
|
|
@@ -495,7 +582,7 @@ optional `rooms.owner.provider` setting is separate and defaults to
|
|
|
495
582
|
### Scheduled agent loops
|
|
496
583
|
|
|
497
584
|
Top-level `loops` schedule literal prompts from trusted local YAML. Enabled targets
|
|
498
|
-
must use
|
|
585
|
+
must use a managed session (`acp` or `codex-app-server`); temporary roles never inherit loops, including `roles:
|
|
499
586
|
["*"]`. Fleet rejects an enabled loop when its explicitly selected base config is
|
|
500
587
|
a symlink, is owned by another user, or is group/world writable. Prompts are
|
|
501
588
|
bounded and normalized at validation time, but only their size and SHA-256 appear
|
|
@@ -582,11 +669,13 @@ native settings actually grant, against a fixed floor:
|
|
|
582
669
|
those requests with nobody to see it. With `unattended: wait` it **warns**,
|
|
583
670
|
since a human can still attach a console and answer.
|
|
584
671
|
|
|
585
|
-
**Security meaning.**
|
|
586
|
-
`auto`
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
`
|
|
672
|
+
**Security meaning.** For native Codex, Fleet translates `ask` → `untrusted`,
|
|
673
|
+
`auto` → `on-request`, and `allow` → `never`; users configure only the Fleet
|
|
674
|
+
names. `permissions.filesystem` independently maps `read-only` → `read-only`,
|
|
675
|
+
`workspace` → `workspace-write`, and `unrestricted` → `danger-full-access`.
|
|
676
|
+
Claude maps `ask` to `default`, `auto` to `acceptEdits`, and `allow` to
|
|
677
|
+
`bypassPermissions`. Codex ACP retains its adapter-specific coupled modes:
|
|
678
|
+
`auto` selects `agent`, while `allow` selects `agent-full-access`.
|
|
590
679
|
`dontAsk` suppresses only the *prompt*, not the denial, which is why an
|
|
591
680
|
`allow` role previously ran unable to do its job. Legacy `deny` is accepted
|
|
592
681
|
only for compatibility and retains its conservative Codex `on-request` /
|
|
@@ -1038,23 +1127,49 @@ The main Codex controls are Brain-owned and also available when spawning:
|
|
|
1038
1127
|
```yaml
|
|
1039
1128
|
# ~/fleet/brains/codex-review.yaml
|
|
1040
1129
|
harness: codex
|
|
1041
|
-
session:
|
|
1130
|
+
session: codex-app-server
|
|
1042
1131
|
model: gpt-5.4
|
|
1132
|
+
permissions:
|
|
1133
|
+
approval: auto
|
|
1134
|
+
filesystem: read-only
|
|
1135
|
+
unattended: deny
|
|
1043
1136
|
harness_options:
|
|
1044
1137
|
launcher: auto
|
|
1045
|
-
profile: fleet
|
|
1046
|
-
sandbox: read-only
|
|
1047
|
-
approval: on-request
|
|
1048
1138
|
monitor: true
|
|
1049
1139
|
search: true
|
|
1050
1140
|
add_dirs: [/data/shared]
|
|
1051
1141
|
config: { model_reasoning_effort: high }
|
|
1052
1142
|
```
|
|
1053
1143
|
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1144
|
+
`codex-app-server` is the opt-in direct Codex transport. It streams native items
|
|
1145
|
+
and message phases, maps Codex approval
|
|
1146
|
+
requests into Fleet permissions, supports turn steering/interrupt, and resumes
|
|
1147
|
+
the durable Codex thread through Fleet's shared `.session-id` lifecycle.
|
|
1148
|
+
Configure its authority only through the shared `permissions:` block;
|
|
1149
|
+
`on-request` and other Codex policy names are internal adapter values.
|
|
1150
|
+
Codex rejects `--profile` for `app-server`, so native roles also reject
|
|
1151
|
+
`harness_options.profile`; use the explicit config map only for non-authority
|
|
1152
|
+
settings. Native config currently allows only `model_reasoning_effort`; Codex ACP
|
|
1153
|
+
retains its legacy profile and config support.
|
|
1154
|
+
Packaged Codex brains and the init wizard continue to select `session: acp` for
|
|
1155
|
+
compatibility; Claude Code also uses ACP. Existing durable sessions are not assumed portable
|
|
1156
|
+
between unrelated providers; a failed fast resume is handled by Fleet's normal
|
|
1157
|
+
bounded fresh-session recovery.
|
|
1158
|
+
|
|
1159
|
+
To replace the default launcher for a native Codex role:
|
|
1160
|
+
|
|
1161
|
+
```yaml
|
|
1162
|
+
session_options:
|
|
1163
|
+
codex_app_server:
|
|
1164
|
+
command: [codex, app-server]
|
|
1165
|
+
```
|
|
1166
|
+
|
|
1167
|
+
The command override is exact: Fleet does not append `--profile`, `--search`, or
|
|
1168
|
+
`app-server`, so include every required argument in the command itself.
|
|
1169
|
+
|
|
1170
|
+
One-off/permanent spawn selects a Brain that owns model, reasoning, native permission,
|
|
1171
|
+
sandbox, profile, launcher, search, monitor consent, native config, and additional roots.
|
|
1172
|
+
Use `env.OURS_PORT`/`env.OURS_CONFIG` for a
|
|
1058
1173
|
role-specific ours daemon, or configure the host default in `~/.ours/config.json`.
|
|
1059
1174
|
|
|
1060
1175
|
## Agent isolation
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ConversationEventV1 } from './session/conversation-types.js';
|
|
2
|
+
import type { AgentSession } from './session/types.js';
|
|
3
|
+
export interface AgentRecoveryEvidence {
|
|
4
|
+
ok: boolean;
|
|
5
|
+
reason: 'RECOVERY_TOOLS_VERIFIED' | 'RECOVERY_PROMPT_MISSING' | 'RECOVERY_TURN_INCOMPLETE' | 'RECOVERY_TURN_FAILED' | 'RECOVERY_CHOOSE_MISSING' | 'RECOVERY_CURRENT_MISSING' | 'RECOVERY_GET_MESSAGES_MISSING' | 'RECOVERY_TOOL_ORDER_INVALID';
|
|
6
|
+
chooseIdentity: boolean;
|
|
7
|
+
currentIdentity: boolean;
|
|
8
|
+
getMessages: boolean;
|
|
9
|
+
turnCompleted: boolean;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Verify one exact recovery turn from its durable conversation ledger. Tool
|
|
13
|
+
* arguments/results are inspected only to derive these booleans and are never
|
|
14
|
+
* returned or persisted by this gate.
|
|
15
|
+
*/
|
|
16
|
+
export declare function evaluateAgentRecovery(events: readonly ConversationEventV1[], promptId: string, identity: string): AgentRecoveryEvidence;
|
|
17
|
+
export declare function recoverAgentIdentity(session: AgentSession, identity: string): Promise<AgentRecoveryEvidence>;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
const TOOL_NAMES = {
|
|
2
|
+
choose: new Set(['choose_identity', 'ours.choose_identity', 'mcp__ours__choose_identity']),
|
|
3
|
+
current: new Set(['current_identity', 'ours.current_identity', 'mcp__ours__current_identity']),
|
|
4
|
+
messages: new Set(['get_messages', 'ours.get_messages', 'mcp__ours__get_messages']),
|
|
5
|
+
};
|
|
6
|
+
function recordFor(records, event) {
|
|
7
|
+
if (event.kind !== 'tool.upsert' || !event.toolCallId)
|
|
8
|
+
return undefined;
|
|
9
|
+
const payload = event.payload;
|
|
10
|
+
const previous = records.get(event.toolCallId) ?? {};
|
|
11
|
+
const next = {
|
|
12
|
+
...previous,
|
|
13
|
+
...(payload.title !== undefined ? { title: payload.title } : {}),
|
|
14
|
+
...(payload.status !== undefined ? { status: payload.status } : {}),
|
|
15
|
+
...(payload.rawInput !== undefined ? { rawInput: payload.rawInput } : {}),
|
|
16
|
+
...(payload.status === 'completed' ? { completedSeq: event.seq } : {}),
|
|
17
|
+
};
|
|
18
|
+
records.set(event.toolCallId, next);
|
|
19
|
+
return next;
|
|
20
|
+
}
|
|
21
|
+
function objectInput(input) {
|
|
22
|
+
if (!input || input.truncated || input.redacted || !input.json
|
|
23
|
+
|| typeof input.json !== 'object' || Array.isArray(input.json))
|
|
24
|
+
return undefined;
|
|
25
|
+
return input.json;
|
|
26
|
+
}
|
|
27
|
+
function safeChoose(record, identity) {
|
|
28
|
+
if (!record.title || !TOOL_NAMES.choose.has(record.title) || record.status !== 'completed')
|
|
29
|
+
return false;
|
|
30
|
+
const input = objectInput(record.rawInput);
|
|
31
|
+
if (!input)
|
|
32
|
+
return false;
|
|
33
|
+
const keys = Object.keys(input).sort();
|
|
34
|
+
if (keys.some(key => key !== 'force' && key !== 'name'))
|
|
35
|
+
return false;
|
|
36
|
+
return input.name === identity && (input.force === undefined || input.force === false);
|
|
37
|
+
}
|
|
38
|
+
function safeNoOrBoundedInput(record, names, allowed) {
|
|
39
|
+
if (!record.title || !names.has(record.title) || record.status !== 'completed')
|
|
40
|
+
return false;
|
|
41
|
+
if (!record.rawInput)
|
|
42
|
+
return allowed({});
|
|
43
|
+
const input = objectInput(record.rawInput);
|
|
44
|
+
return input !== undefined && allowed(input);
|
|
45
|
+
}
|
|
46
|
+
function safeCurrent(record) {
|
|
47
|
+
return safeNoOrBoundedInput(record, TOOL_NAMES.current, input => Object.keys(input).length === 0);
|
|
48
|
+
}
|
|
49
|
+
function safeMessages(record) {
|
|
50
|
+
return safeNoOrBoundedInput(record, TOOL_NAMES.messages, input => {
|
|
51
|
+
const keys = Object.keys(input);
|
|
52
|
+
if (keys.some(key => key !== 'limit'))
|
|
53
|
+
return false;
|
|
54
|
+
return input.limit === undefined
|
|
55
|
+
|| (Number.isSafeInteger(input.limit) && input.limit >= 1 && input.limit <= 200);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Verify one exact recovery turn from its durable conversation ledger. Tool
|
|
60
|
+
* arguments/results are inspected only to derive these booleans and are never
|
|
61
|
+
* returned or persisted by this gate.
|
|
62
|
+
*/
|
|
63
|
+
export function evaluateAgentRecovery(events, promptId, identity) {
|
|
64
|
+
const ordered = [...events].sort((a, b) => a.seq - b.seq);
|
|
65
|
+
const admitted = ordered.find(event => event.kind === 'prompt.admitted' && event.promptId === promptId);
|
|
66
|
+
const empty = (reason) => ({
|
|
67
|
+
ok: false, reason, chooseIdentity: false, currentIdentity: false,
|
|
68
|
+
getMessages: false, turnCompleted: false,
|
|
69
|
+
});
|
|
70
|
+
if (!admitted)
|
|
71
|
+
return empty('RECOVERY_PROMPT_MISSING');
|
|
72
|
+
const terminal = ordered.find(event => event.seq > admitted.seq
|
|
73
|
+
&& event.kind === 'turn.completed'
|
|
74
|
+
&& event.promptId === promptId
|
|
75
|
+
&& event.sessionGeneration === admitted.sessionGeneration);
|
|
76
|
+
if (!terminal)
|
|
77
|
+
return empty('RECOVERY_TURN_INCOMPLETE');
|
|
78
|
+
const outcome = terminal.payload.outcome;
|
|
79
|
+
if (outcome !== 'completed')
|
|
80
|
+
return empty('RECOVERY_TURN_FAILED');
|
|
81
|
+
const records = new Map();
|
|
82
|
+
for (const event of ordered) {
|
|
83
|
+
if (event.seq <= admitted.seq || event.seq >= terminal.seq
|
|
84
|
+
|| event.promptId !== promptId
|
|
85
|
+
|| event.sessionGeneration !== admitted.sessionGeneration)
|
|
86
|
+
continue;
|
|
87
|
+
recordFor(records, event);
|
|
88
|
+
}
|
|
89
|
+
const values = [...records.values()];
|
|
90
|
+
const chooses = values.filter(record => safeChoose(record, identity));
|
|
91
|
+
const currents = values.filter(safeCurrent);
|
|
92
|
+
const messages = values.filter(safeMessages);
|
|
93
|
+
const chooseIdentity = chooses.length > 0;
|
|
94
|
+
const currentIdentity = currents.length > 0;
|
|
95
|
+
const getMessages = messages.length > 0;
|
|
96
|
+
const base = { chooseIdentity, currentIdentity, getMessages, turnCompleted: true };
|
|
97
|
+
if (!chooseIdentity)
|
|
98
|
+
return { ok: false, reason: 'RECOVERY_CHOOSE_MISSING', ...base };
|
|
99
|
+
if (!currentIdentity)
|
|
100
|
+
return { ok: false, reason: 'RECOVERY_CURRENT_MISSING', ...base };
|
|
101
|
+
if (!getMessages)
|
|
102
|
+
return { ok: false, reason: 'RECOVERY_GET_MESSAGES_MISSING', ...base };
|
|
103
|
+
const orderedChain = chooses.some(choose => currents.some(current => messages.some(message => choose.completedSeq !== undefined && current.completedSeq !== undefined
|
|
104
|
+
&& message.completedSeq !== undefined
|
|
105
|
+
&& choose.completedSeq < current.completedSeq
|
|
106
|
+
&& current.completedSeq < message.completedSeq)));
|
|
107
|
+
if (!orderedChain)
|
|
108
|
+
return { ok: false, reason: 'RECOVERY_TOOL_ORDER_INVALID', ...base };
|
|
109
|
+
return { ok: true, reason: 'RECOVERY_TOOLS_VERIFIED', ...base };
|
|
110
|
+
}
|
|
111
|
+
export async function recoverAgentIdentity(session, identity) {
|
|
112
|
+
if (!session.subscribeConversation)
|
|
113
|
+
return {
|
|
114
|
+
ok: false, reason: 'RECOVERY_PROMPT_MISSING', chooseIdentity: false,
|
|
115
|
+
currentIdentity: false, getMessages: false, turnCompleted: false,
|
|
116
|
+
};
|
|
117
|
+
const events = [];
|
|
118
|
+
const unsubscribe = session.subscribeConversation(event => events.push(event));
|
|
119
|
+
try {
|
|
120
|
+
const queued = await session.queuePrompt([
|
|
121
|
+
'[fleet-recovery] The shared ours daemon restarted.',
|
|
122
|
+
`Call ours choose_identity with name ${JSON.stringify(identity)} and force false.`,
|
|
123
|
+
'Then call current_identity, then get_messages. Complete all three in that order.',
|
|
124
|
+
'Do not create/delete identities, force-bind, interrupt, or restart any service/session.',
|
|
125
|
+
].join('\n'), { origin: { kind: 'fleet-monitor' } });
|
|
126
|
+
await queued.completion;
|
|
127
|
+
// Conversation publication is synchronous with terminal settlement in the
|
|
128
|
+
// in-tree ACP store; filtering by exact promptId/sessionGeneration remains
|
|
129
|
+
// the authority even if unrelated events arrived concurrently.
|
|
130
|
+
return evaluateAgentRecovery(events, queued.promptId, identity);
|
|
131
|
+
}
|
|
132
|
+
finally {
|
|
133
|
+
unsubscribe();
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import { sessionBackendCapabilities } from '../session/types.js';
|
|
1
2
|
export function roleCapabilities(role, status, context = {}) {
|
|
2
|
-
const backend = role.configuredBackend
|
|
3
|
+
const backend = role.configuredBackend
|
|
4
|
+
?? (role.detectedBackend === 'acp' || role.detectedBackend === 'codex-app-server'
|
|
5
|
+
? role.detectedBackend : undefined);
|
|
3
6
|
const online = status.session.reachability === 'online';
|
|
4
|
-
const
|
|
7
|
+
const session = backend ? sessionBackendCapabilities(backend, role.config?.harness) : undefined;
|
|
5
8
|
const protocolVersion = context.controlProtocolVersion ?? 1;
|
|
6
9
|
const inactive = status.overall === 'offline'
|
|
7
10
|
|| (!online && status.supervisor.liveness === 'stopped');
|
|
@@ -10,11 +13,20 @@ export function roleCapabilities(role, status, context = {}) {
|
|
|
10
13
|
inventory: true,
|
|
11
14
|
status: true,
|
|
12
15
|
output: {
|
|
13
|
-
recent:
|
|
14
|
-
|
|
16
|
+
recent: Boolean(session?.streaming),
|
|
17
|
+
stream: Boolean(session?.streaming) && protocolVersion >= 2,
|
|
18
|
+
structured: Boolean(session?.durableConversation),
|
|
19
|
+
replayCursor: Boolean(session?.durableConversation) && protocolVersion >= 2,
|
|
20
|
+
},
|
|
21
|
+
input: {
|
|
22
|
+
text: online && Boolean(session?.promptInput),
|
|
23
|
+
interrupt: online && Boolean(session?.interrupt),
|
|
24
|
+
steering: online && Boolean(session?.steering),
|
|
25
|
+
},
|
|
26
|
+
permissions: {
|
|
27
|
+
observe: Boolean(session?.permissions),
|
|
28
|
+
respond: online && Boolean(session?.permissions),
|
|
15
29
|
},
|
|
16
|
-
input: { text: online && acp, interrupt: online && acp, steering: false },
|
|
17
|
-
permissions: { observe: acp, respond: acp && online },
|
|
18
30
|
lifecycle: {
|
|
19
31
|
start: role.lifetime === 'permanent' && status.supervisor.liveness !== 'running',
|
|
20
32
|
stop: role.lifetime === 'permanent' && !inactive && status.supervisor.liveness !== 'stopped',
|
|
@@ -4,6 +4,7 @@ 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 { readDaemonRecoveryStatus } from '../daemon-recovery.js';
|
|
7
8
|
import { roleCapabilities } from './capabilities.js';
|
|
8
9
|
import { FleetError } from './errors.js';
|
|
9
10
|
const clean = (value, max = 512) => value.replace(/[\0-\x08\x0b\x0c\x0e-\x1f\x7f]/g, '').trim().slice(0, max);
|
|
@@ -46,7 +47,8 @@ function readIsolation(dir) {
|
|
|
46
47
|
function sessionOverall(supervisor, session, restart, monitor, isolation, problems) {
|
|
47
48
|
if (restart.circuit === 'open' || monitor.health === 'failed' || monitor.health === 'degraded'
|
|
48
49
|
|| isolation.degraded
|
|
49
|
-
|| problems.some(problem => problem.severity === 'error'
|
|
50
|
+
|| problems.some(problem => problem.severity === 'error'
|
|
51
|
+
|| problem.source === 'watchdog' || problem.source === 'daemon-recovery')
|
|
50
52
|
|| session.readiness === 'failed')
|
|
51
53
|
return 'attention';
|
|
52
54
|
// A reachable agent session is the user's live interaction surface. Its
|
|
@@ -74,7 +76,8 @@ export class FleetQueryService {
|
|
|
74
76
|
this.control = options.control ?? controlRequest;
|
|
75
77
|
}
|
|
76
78
|
async list() {
|
|
77
|
-
const roles = await this.options.repository.list()
|
|
79
|
+
const roles = (await this.options.repository.list())
|
|
80
|
+
.filter(role => role.configured && role.lifetime === 'permanent');
|
|
78
81
|
return Promise.all(roles.map(async (role) => {
|
|
79
82
|
const status = await this.status(role);
|
|
80
83
|
return { role, status, capabilities: roleCapabilities(role, status, this.options.capabilityContext) };
|
|
@@ -103,6 +106,16 @@ export class FleetQueryService {
|
|
|
103
106
|
const lastExit = dir ? readExitRecord(join(dir, '.exit-status')) ?? undefined : undefined;
|
|
104
107
|
const session = await this.session(role, dir, live.state);
|
|
105
108
|
const problems = [...role.problems];
|
|
109
|
+
const recovery = dir ? readDaemonRecoveryStatus(dir) : undefined;
|
|
110
|
+
if (recovery && recovery.state !== 'recovered') {
|
|
111
|
+
const paths = ['agent', 'owner']
|
|
112
|
+
.filter(name => recovery.paths[name].state !== 'recovered')
|
|
113
|
+
.map(name => `${name}:${recovery.paths[name].state}`);
|
|
114
|
+
problems.push({
|
|
115
|
+
code: 'daemon_recovery', severity: 'warning', source: 'daemon-recovery',
|
|
116
|
+
detail: `${recovery.state}; ${paths.join(', ') || 'no degraded path'}; epoch ${recovery.epoch || 'unavailable'}`,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
106
119
|
if (live.state === 'running' && session.reachability !== 'online')
|
|
107
120
|
problems.push({
|
|
108
121
|
code: 'supervisor_session_disagreement', severity: 'warning',
|
|
@@ -141,14 +154,16 @@ export class FleetQueryService {
|
|
|
141
154
|
}
|
|
142
155
|
async session(role, dir, supervisor) {
|
|
143
156
|
const intended = role.configuredBackend;
|
|
144
|
-
if (intended
|
|
157
|
+
if (intended && dir) {
|
|
145
158
|
try {
|
|
146
159
|
const response = await this.control(dir, { command: 'snapshot' }, 2_000);
|
|
147
160
|
if (!response.ok)
|
|
148
161
|
throw new SessionControlError(response.kind ?? 'backend', response.error ?? 'snapshot failed');
|
|
149
162
|
const snapshot = response.result;
|
|
163
|
+
if (snapshot.backend !== intended)
|
|
164
|
+
throw new SessionControlError('backend', `live session reports backend '${String(snapshot.backend)}', expected '${intended}'`);
|
|
150
165
|
return {
|
|
151
|
-
backend:
|
|
166
|
+
backend: intended, reachability: snapshot.alive ? 'online' : 'offline',
|
|
152
167
|
readiness: snapshot.readiness, evidence: 'authoritative',
|
|
153
168
|
sessionId: snapshot.sessionId, lastError: snapshot.lastError,
|
|
154
169
|
pendingPermissionId: snapshot.pendingPermissionId,
|
|
@@ -162,7 +177,7 @@ export class FleetQueryService {
|
|
|
162
177
|
const failure = error instanceof SessionControlError ? error.kind : 'backend';
|
|
163
178
|
const offline = failure === 'offline';
|
|
164
179
|
return {
|
|
165
|
-
backend:
|
|
180
|
+
backend: intended,
|
|
166
181
|
reachability: offline ? 'offline'
|
|
167
182
|
: failure === 'control-unavailable' ? 'unavailable' : 'unknown',
|
|
168
183
|
readiness: offline ? 'failed' : 'unknown',
|
|
@@ -12,6 +12,10 @@ export class RoleLifecycleService {
|
|
|
12
12
|
const config = input.config ?? loadConfig(this.options.configPath);
|
|
13
13
|
const selected = input.roleIds.length ? input.roleIds : config.roles.map(role => role.name);
|
|
14
14
|
for (const roleId of selected) {
|
|
15
|
+
if (config.agentTemplates?.[roleId] && !config.roles.some(role => role.name === roleId))
|
|
16
|
+
throw new FleetError('capability_unavailable', `'${roleId}' is an inert Agent Template; lifecycle commands target only persistent fleet/agents instances`);
|
|
17
|
+
if (!config.roles.some(role => role.name === roleId))
|
|
18
|
+
throw new FleetError('role_not_found', `no such role '${roleId}'`);
|
|
15
19
|
const role = await this.options.repository.get(roleId);
|
|
16
20
|
if (!role)
|
|
17
21
|
throw new FleetError('role_not_found', `no such role '${roleId}'`);
|
|
@@ -51,6 +55,11 @@ export class RoleCommandService {
|
|
|
51
55
|
throw new FleetError('idempotency_conflict', 'action ID already belongs to another operation');
|
|
52
56
|
return prior;
|
|
53
57
|
}
|
|
58
|
+
const config = loadConfig(this.options.configPath);
|
|
59
|
+
if (config.agentTemplates?.[input.roleId] && !config.roles.some(role => role.name === input.roleId))
|
|
60
|
+
throw new FleetError('capability_unavailable', `'${input.roleId}' is an inert Agent Template; lifecycle commands target only persistent fleet/agents instances`);
|
|
61
|
+
if (!config.roles.some(role => role.name === input.roleId))
|
|
62
|
+
throw new FleetError('role_not_found', `no such role '${input.roleId}'`);
|
|
54
63
|
const role = await this.options.repository.get(input.roleId);
|
|
55
64
|
if (!role)
|
|
56
65
|
throw new FleetError('role_not_found', `no such role '${input.roleId}'`);
|