@ours.network/fleet 1.1.0-nightly.1 → 1.1.0-nightly.10
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 +97 -95
- package/dist/agent-recovery-gate.d.ts +17 -0
- package/dist/agent-recovery-gate.js +135 -0
- package/dist/application/fleet-query-service.js +13 -1
- package/dist/application/role-creation-service.d.ts +2 -21
- package/dist/application/role-creation-service.js +26 -64
- package/dist/application/role-removal-service.js +6 -3
- package/dist/application/task-room-service.d.ts +1 -1
- package/dist/application/task-room-service.js +154 -16
- package/dist/application/types.d.ts +1 -1
- package/dist/briefing.js +11 -7
- package/dist/build-info.json +4 -4
- package/dist/cli.js +136 -42
- package/dist/config-yaml.d.ts +1 -1
- package/dist/config.d.ts +65 -2
- package/dist/config.js +385 -26
- package/dist/creation.d.ts +1 -1
- package/dist/creation.js +2 -2
- 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 +84 -102
- package/dist/doctor.js +18 -4
- package/dist/fleet-command-audit.d.ts +160 -0
- package/dist/fleet-command-audit.js +433 -0
- package/dist/fleet-proxy.d.ts +2 -0
- package/dist/fleet-proxy.js +30 -8
- package/dist/generated-agent-source.d.ts +9 -0
- package/dist/generated-agent-source.js +53 -0
- package/dist/harness/agent-session.d.ts +18 -0
- package/dist/harness/claude-code-session.d.ts +6 -0
- package/dist/harness/claude-code-session.js +8 -0
- package/dist/harness/claude-code.js +12 -0
- package/dist/harness/codex-session.d.ts +6 -0
- package/dist/harness/codex-session.js +8 -0
- package/dist/harness/codex.js +20 -0
- package/dist/harness/registry.js +4 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/init-guidance.d.ts +1 -0
- package/dist/init-guidance.js +6 -0
- package/dist/model-env.d.ts +2 -3
- package/dist/model-env.js +3 -5
- package/dist/ops.js +4 -2
- package/dist/owner-channel/channel.d.ts +56 -0
- package/dist/owner-channel/channel.js +269 -29
- package/dist/owner-channel/commands.d.ts +2 -0
- package/dist/owner-channel/commands.js +31 -13
- package/dist/owner-channel/state.d.ts +2 -0
- package/dist/owner-channel/state.js +8 -2
- package/dist/preset-bootstrap.d.ts +10 -0
- package/dist/preset-bootstrap.js +85 -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.d.ts +3 -2
- package/dist/resolved-plan.js +21 -5
- package/dist/rooms-tasks/cli.js +225 -40
- package/dist/rooms-tasks/config.js +24 -13
- package/dist/rooms-tasks/external-worker.d.ts +1 -0
- package/dist/rooms-tasks/external-worker.js +14 -9
- package/dist/rooms-tasks/provision.js +98 -36
- package/dist/rooms-tasks/templates.d.ts +0 -1
- package/dist/rooms-tasks/templates.js +9 -76
- package/dist/rooms-tasks/types.d.ts +10 -16
- package/dist/rooms-tasks/types.js +1 -4
- package/dist/runner.d.ts +9 -2
- package/dist/runner.js +84 -20
- package/dist/sensitive-config.d.ts +2 -0
- package/dist/sensitive-config.js +4 -0
- package/dist/session/acp.d.ts +5 -0
- package/dist/session/acp.js +27 -3
- package/dist/session/control.d.ts +14 -1
- package/dist/session/control.js +35 -0
- package/dist/spawn.d.ts +8 -33
- package/dist/spawn.js +52 -211
- 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 +11 -8
- package/dist/web/fleet-config-service.js +384 -183
- package/dist/web/topology-promote.js +25 -15
- package/dist/web/topology.js +2 -2
- package/dist/web/yaml-document-edit.js +2 -0
- package/dist/web-app/assets/index-BbTc5KtZ.js +10 -0
- package/dist/web-app/index.html +1 -1
- package/examples/fleet/agents/Alice.yaml +11 -0
- package/examples/fleet/agents/FleetCoordinator.yaml +7 -0
- package/examples/fleet/brains/claude-default.yaml +5 -0
- package/examples/fleet/roles/coordinator.yaml +5 -0
- package/examples/fleet/roles/developer.yaml +3 -0
- package/examples/fleet.yaml +74 -0
- package/package.json +3 -1
- package/presets/fleet/agents/Agent.yaml +3 -0
- package/presets/fleet/agents/Architect.yaml +3 -0
- package/presets/fleet/agents/Critic.yaml +3 -0
- package/presets/fleet/agents/Developer.yaml +3 -0
- package/presets/fleet/agents/Secretary.yaml +3 -0
- package/presets/fleet/agents/Tester.yaml +3 -0
- package/presets/fleet/brains/claude-default.yaml +5 -0
- package/presets/fleet/roles/Agent.yaml +6 -0
- package/presets/fleet/roles/Architect.yaml +6 -0
- package/presets/fleet/roles/Critic.yaml +6 -0
- package/presets/fleet/roles/Developer.yaml +6 -0
- package/presets/fleet/roles/Secretary.yaml +6 -0
- package/presets/fleet/roles/Tester.yaml +6 -0
- package/presets/fleet/room_templates/pair.yaml +11 -0
- package/presets/fleet/room_templates/single.yaml +8 -0
- package/presets/fleet/room_templates/team.yaml +12 -0
- package/presets/fleet.yaml +7 -0
- package/presets/manifest.json +5 -0
- package/dist/web-app/assets/index-DhMDVRU1.js +0 -10
package/README.md
CHANGED
|
@@ -19,10 +19,10 @@ turns such sessions into **roles**: long-lived agents that
|
|
|
19
19
|
- can **spawn subagents** (permanent or temporary) and **oversee** them: peek into
|
|
20
20
|
a ward's console, answer a stuck prompt, nudge it back to work.
|
|
21
21
|
|
|
22
|
-
The
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
reality match
|
|
22
|
+
The fleet uses a v2 manifest plus typed documents: `~/fleet.yaml` holds
|
|
23
|
+
fleet-wide operational policy and automation, while `~/fleet/agents`,
|
|
24
|
+
`~/fleet/roles`, and `~/fleet/brains` hold bare Agent, Role, and Brain documents.
|
|
25
|
+
`ours-fleet up` makes reality match that trusted split source set.
|
|
26
26
|
|
|
27
27
|
**Harness-agnostic by design.** The core never assumes a specific agent CLI; each
|
|
28
28
|
harness is a small adapter (how to launch, how to resume, how to wire config).
|
|
@@ -31,17 +31,19 @@ public — each additional harness (Gemini CLI, OpenCode, …) is a small adapte
|
|
|
31
31
|
A single fleet can mix harnesses per role:
|
|
32
32
|
|
|
33
33
|
```yaml
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
34
|
+
# ~/fleet/agents/Reviewer.yaml
|
|
35
|
+
role: { ref: reviewer }
|
|
36
|
+
brain: { inline: { harness: claude-code, session: acp } }
|
|
37
|
+
|
|
38
|
+
# ~/fleet/agents/Prototyper.yaml
|
|
39
|
+
role: { inline: { mission: Build prototypes } }
|
|
40
|
+
brain: { inline: { harness: codex, session: acp } }
|
|
39
41
|
```
|
|
40
42
|
|
|
41
43
|
## How it works
|
|
42
44
|
|
|
43
45
|
```
|
|
44
|
-
~/fleet.yaml + ~/fleet
|
|
46
|
+
~/fleet.yaml + ~/fleet/{agents,roles,brains,room_templates}/*.yaml your declaration
|
|
45
47
|
│ ours-fleet up
|
|
46
48
|
▼
|
|
47
49
|
briefing.md per role ──► agent session adapter ──► ACP client/session ──► ACP agent
|
|
@@ -89,7 +91,7 @@ equivalent); logs land in `~/.ours-fleet/logs/`.
|
|
|
89
91
|
|
|
90
92
|
```sh
|
|
91
93
|
npm i -g @ours.network/fleet
|
|
92
|
-
ours-fleet init # units/dirs/linger
|
|
94
|
+
ours-fleet init # units/dirs/linger + missing standard presets
|
|
93
95
|
ours-fleet doctor # verifies everything above, with actionable messages
|
|
94
96
|
```
|
|
95
97
|
|
|
@@ -105,8 +107,9 @@ become that account and repeat.
|
|
|
105
107
|
## Quickstart
|
|
106
108
|
|
|
107
109
|
```sh
|
|
108
|
-
|
|
109
|
-
|
|
110
|
+
ours-fleet init # safe to repeat: creates missing files, never replaces edits
|
|
111
|
+
ours-fleet config # validates Agents, Roles, Brains, and Room templates
|
|
112
|
+
$EDITOR ~/fleet/agents/*.yaml # compose Role + Brain and operational settings
|
|
110
113
|
ours-fleet up # boot the fleet (staggered)
|
|
111
114
|
ours-fleet ls # running consoles
|
|
112
115
|
ours-fleet attach Alice # watch one live (Ctrl-b d to leave)
|
|
@@ -123,19 +126,16 @@ ours-fleet spawn Worker --mission "own the worker repo" \
|
|
|
123
126
|
ours-fleet spawn --temp Scout --mission "one-off research" # gone on exit/reboot
|
|
124
127
|
|
|
125
128
|
# Codex role: ours-codex is preferred automatically; plain codex is the fallback
|
|
126
|
-
ours-fleet spawn Coder --
|
|
127
|
-
--
|
|
128
|
-
--profile fleet --search --monitor --coordinator FleetCoordinator
|
|
129
|
-
# Note: --monitor is legacy consent for Codex's native monitor. Choose the
|
|
130
|
-
# wake owner separately in fleet.yaml with monitor.mode: fleet|native.
|
|
129
|
+
ours-fleet spawn Coder --brain codex-fleet --role developer \
|
|
130
|
+
--approval ask --filesystem workspace --coordinator FleetCoordinator
|
|
131
131
|
```
|
|
132
132
|
|
|
133
133
|
Inside a managed ACP role, `ours-fleet spawn` is transparently routed through
|
|
134
|
-
that role's live supervisor. `ours-fleet spawn
|
|
135
|
-
minimal form: omitted
|
|
136
|
-
fleet monitor policy
|
|
137
|
-
|
|
138
|
-
|
|
134
|
+
that role's live supervisor. `ours-fleet spawn DeveloperX --temp` is the
|
|
135
|
+
minimal form: omitted Brain/Role selections, cwd, coordinator, neutral permissions,
|
|
136
|
+
and fleet monitor policy inherit from the caller. Identity, environment, owner routing,
|
|
137
|
+
room startup, and sensitive inline Brain values do not. Explicit flags win. After
|
|
138
|
+
creation succeeds, fleet can deterministically notify
|
|
139
139
|
the caller's owner channel with the caller and spawned-role details. This is an
|
|
140
140
|
honest-actor convenience and attribution path, not a security boundary; host
|
|
141
141
|
shells and deliberately bypassed absolute binaries retain direct behavior.
|
|
@@ -252,27 +252,19 @@ Security boundaries:
|
|
|
252
252
|
- tests use temporary fleet homes and fake supervisors/identity providers—never
|
|
253
253
|
active role state.
|
|
254
254
|
|
|
255
|
-
Permanent spawns
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
block sequence — adding or removing an entry under `watch:`, `oversee:`, `roles:`
|
|
269
|
-
or `wake_sources:` — rewrites that one collection as a whole, which drops inline
|
|
270
|
-
comments written on its individual items. The loss is confined to the collection
|
|
271
|
-
you edited, is shown in the diff before anything is written, and can be declined
|
|
272
|
-
by not saving. Each write is revision-guarded, reviewed as a diff of the real
|
|
273
|
-
file, validated with the real loader, and preceded by a timestamped backup.
|
|
274
|
-
Values under `env:` — and the `vars:` entries they interpolate — are masked in
|
|
275
|
-
the diff and never leave the host.
|
|
255
|
+
Permanent spawns write a bare Agent document to
|
|
256
|
+
`~/fleet/agents/<Name>.yaml`; generated-source ownership is recorded so removal
|
|
257
|
+
never deletes an unproven hand-written neighbor. `ours-fleet rm <Name>` unspawns.
|
|
258
|
+
|
|
259
|
+
The web editor reads and writes the split document model explicitly as
|
|
260
|
+
`{manifest, agents}`. Role and Brain presets are visible through Agent refs but
|
|
261
|
+
remain read-only. Saves use one aggregate revision over the manifest and all
|
|
262
|
+
Agent/Role/Brain sources, validate an exact-stem private staging tree, show a
|
|
263
|
+
deterministic redacted diff per document, and hold one manifest-root lock.
|
|
264
|
+
Changed/deleted files are backed up together; any partial failure restores every
|
|
265
|
+
document or reports the private recovery directory. An unchanged save is a
|
|
266
|
+
byte-for-byte no-op and creates no backup. Nested environment, authentication,
|
|
267
|
+
invite, and isolation secrets never leave the host.
|
|
276
268
|
|
|
277
269
|
From inside Claude Code, Codex, or Hermes with the core `ours` plugin installed,
|
|
278
270
|
say **"spawn an ours agent …"**. The core skill checks for `ours-fleet`, installs
|
|
@@ -290,16 +282,16 @@ ours-fleet peek Worker # what is it doing?
|
|
|
290
282
|
ours-fleet send Worker "continue with the tests, then report"
|
|
291
283
|
```
|
|
292
284
|
|
|
293
|
-
Declare standing assignments in
|
|
285
|
+
Declare standing assignments in the overseer's bare Agent document (rendered into its
|
|
294
286
|
briefing) — or just write "keep an eye on Alice and Bob every 5 minutes" in a
|
|
295
287
|
persona; the bundled `oversee-agents` skill defines what that means operationally:
|
|
296
288
|
|
|
297
289
|
```yaml
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
290
|
+
role: { ref: coordinator }
|
|
291
|
+
brain: { ref: claude-default }
|
|
292
|
+
oversee:
|
|
293
|
+
- { agent: Alice, interval: 5m }
|
|
294
|
+
- { agent: Bob, interval: 5m }
|
|
303
295
|
```
|
|
304
296
|
|
|
305
297
|
## Command reference
|
|
@@ -310,7 +302,7 @@ ours-fleet up|down|restart|force-restart [-c FILE] [Name...]
|
|
|
310
302
|
ours-fleet config [-c FILE] validate + print merged plan
|
|
311
303
|
ours-fleet ls | attach | peek | logs [-f] | status <Name>
|
|
312
304
|
ours-fleet send <Name> "text"
|
|
313
|
-
ours-fleet spawn [--temp] [<Name> | --
|
|
305
|
+
ours-fleet spawn [--temp] [<Name> | --name <Name>] --brain <ID|inline:{...}> --role <ID|inline:{...}> [--approval ...]
|
|
314
306
|
ours-fleet loops validate|list|status
|
|
315
307
|
ours-fleet loops reload <Role>
|
|
316
308
|
ours-fleet loops run-now|disable|enable <Role> <Loop>
|
|
@@ -342,33 +334,31 @@ in its state dir) across supervisor-triggered restarts — systemd/launchd re-in
|
|
|
342
334
|
agent process with no arguments, so without this the role would silently fall back to
|
|
343
335
|
the default `~/fleet.yaml` on its very first crash-restart and fail to resolve.
|
|
344
336
|
|
|
345
|
-
##
|
|
337
|
+
## split configuration field reference
|
|
346
338
|
|
|
347
|
-
|
|
339
|
+
The following is a schematic field inventory, not a single YAML document. Host
|
|
340
|
+
settings belong in `~/fleet.yaml`; reusable behavior and harness settings belong
|
|
341
|
+
in `~/fleet/roles/<id>.yaml` and `~/fleet/brains/<id>.yaml`; operational fields
|
|
342
|
+
belong in `~/fleet/agents/<id>.yaml`.
|
|
343
|
+
|
|
344
|
+
```text
|
|
348
345
|
vars: { work_root: /home/me/work } # ${var} substitution anywhere below
|
|
346
|
+
api_version: ours.network/fleet/v2
|
|
349
347
|
start_stagger_ms: 0 # delay between agent LAUNCHES (host-wide, ms); 0 = no stagger
|
|
350
348
|
defaults:
|
|
351
|
-
harness: claude-code # for roles that don't set one
|
|
352
|
-
session: acp # supported Fleet↔agent session path
|
|
353
349
|
permissions: # common intent, translated by each harness/backend
|
|
354
350
|
approval: ask # ask | auto | allow (`deny` is a deprecated alias)
|
|
355
351
|
filesystem: workspace # read-only | workspace | unrestricted
|
|
356
352
|
unattended: deny # deny | wait
|
|
357
|
-
model: claude-fable-5 # default model for roles that don't set one (per-role model / --model wins)
|
|
358
|
-
max_tokens: 500000 # session cap (harness-interpreted)
|
|
359
353
|
monitor:
|
|
360
354
|
mode: fleet # fleet (default) | native
|
|
361
355
|
worklog: # built-ins shown; set false to opt out
|
|
362
356
|
max_kb: 1024 # rotate only above this active-log size
|
|
363
357
|
keep_tail_kb: 256 # UTF-8 tail; line-aligned when one fits
|
|
364
358
|
max_archives: 12 # recent beside log; older preserved cold
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
session: acp # optional; ACP is the only supported session
|
|
369
|
-
session_options:
|
|
370
|
-
acp:
|
|
371
|
-
command: claude-agent-acp # optional advanced override
|
|
359
|
+
Agent document (\`~/fleet/agents/Name.yaml\`):
|
|
360
|
+
role: { ref: RoleID }
|
|
361
|
+
brain: { ref: BrainID } # Brain owns harness/model/session/reasoning
|
|
372
362
|
identity: "Display Name" # ours identity to bind (default: Name)
|
|
373
363
|
cwd: ${work_root}/repo # where the harness process runs
|
|
374
364
|
coordinator: FleetCoordinator # announce target on boot
|
|
@@ -400,7 +390,7 @@ roles:
|
|
|
400
390
|
bio: | # public card (published as bio)
|
|
401
391
|
briefing_file: curated.md # replaces the generated narrative
|
|
402
392
|
env: { KEY: value } # extra session env
|
|
403
|
-
oversee: [{
|
|
393
|
+
oversee: [{ agent: X, interval: 5m }]
|
|
404
394
|
harness_options: # adapter-owned, adapter-validated
|
|
405
395
|
plugins: { "name@marketplace": false } # claude-code: plugin overrides
|
|
406
396
|
# mem_palace: false # claude-code: disable memory plugin
|
|
@@ -438,13 +428,12 @@ loops: # trusted local scheduled ACP turns
|
|
|
438
428
|
If nothing material changed, complete silently without an owner report.
|
|
439
429
|
```
|
|
440
430
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
and
|
|
447
|
-
overrides `defaults.monitor` key-by-key.
|
|
431
|
+
The v2 loader reads the manifest, then sorted bare Role, Brain, and Agent
|
|
432
|
+
documents from its exact stem directory. Duplicate filename-derived IDs are a
|
|
433
|
+
hard error. Identities and Agents are decoupled—removing an Agent never deletes
|
|
434
|
+
an identity. Role, Brain, and operational ownership are composed explicitly;
|
|
435
|
+
there is no generic cross-kind merge. Manifest operational defaults such as
|
|
436
|
+
`monitor` and `permissions` merge only within their owning Agent fields.
|
|
448
437
|
|
|
449
438
|
Every supervised role is a client of the operator-configured ours daemon. Fleet strips the
|
|
450
439
|
obsolete, presence-sensitive `OURS_AUTOSTART` variable from ACP harness
|
|
@@ -453,6 +442,25 @@ installer/setup flows remain responsible for starting it.
|
|
|
453
442
|
|
|
454
443
|
### Rooms and tasks
|
|
455
444
|
|
|
445
|
+
Init installs editable `single`, `pair`, and `team` definitions under
|
|
446
|
+
`~/fleet/room_templates/`, plus every exact-cased Agent, Role, and Brain they
|
|
447
|
+
reference. Inspect them with `ours-fleet template list` and
|
|
448
|
+
`ours-fleet template show team`. After configuring the authenticated owner below:
|
|
449
|
+
|
|
450
|
+
```sh
|
|
451
|
+
ours-fleet task create --title "Solo task" --template single
|
|
452
|
+
ours-fleet task create --title "Reviewed change" --template pair
|
|
453
|
+
ours-fleet task create --title "Phased delivery" --template team
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
`ours-fleet init -c /path/custom.yaml` seeds `/path/custom/` instead. Init reports
|
|
457
|
+
the packaged preset revision and source directory and only seeds missing files.
|
|
458
|
+
It never upgrades an edited preset. To adopt a newer packaged file explicitly,
|
|
459
|
+
copy the reported source file beside the existing target as `.new-default`, review
|
|
460
|
+
`diff -u`, then replace the target yourself. This is the sole adoption operation;
|
|
461
|
+
rerunning init is not an update. Users upgrading from hardcoded templates should
|
|
462
|
+
run init once (with the same `-c` selection they normally use).
|
|
463
|
+
|
|
456
464
|
Rooms always use `ours-cowork`; there is no room-provider selector. Configure
|
|
457
465
|
the cowork daemon connection and the room owner directly:
|
|
458
466
|
|
|
@@ -1037,33 +1045,27 @@ the structured CLI watcher, because a detached process cannot wake a Codex turn.
|
|
|
1037
1045
|
wait is re-entered after each handled message; `ours-codex` instead wakes the idle
|
|
1038
1046
|
session through its App Server integration.
|
|
1039
1047
|
|
|
1040
|
-
The main Codex controls
|
|
1048
|
+
The main Codex controls are Brain-owned and also available when spawning:
|
|
1041
1049
|
|
|
1042
1050
|
```yaml
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
roles:
|
|
1058
|
-
Reviewer:
|
|
1059
|
-
harness_options:
|
|
1060
|
-
sandbox: read-only # overrides just this default key
|
|
1051
|
+
# ~/fleet/brains/codex-review.yaml
|
|
1052
|
+
harness: codex
|
|
1053
|
+
session: acp
|
|
1054
|
+
model: gpt-5.4
|
|
1055
|
+
harness_options:
|
|
1056
|
+
launcher: auto
|
|
1057
|
+
profile: fleet
|
|
1058
|
+
sandbox: read-only
|
|
1059
|
+
approval: on-request
|
|
1060
|
+
monitor: true
|
|
1061
|
+
search: true
|
|
1062
|
+
add_dirs: [/data/shared]
|
|
1063
|
+
config: { model_reasoning_effort: high }
|
|
1061
1064
|
```
|
|
1062
1065
|
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
`--codex-config key=value`, and repeatable `--add-dir`. Use `env.OURS_PORT`/`env.OURS_CONFIG` for a
|
|
1066
|
+
One-off/permanent spawn selects a Brain that owns model, reasoning, native permission,
|
|
1067
|
+
sandbox, profile, launcher, search, monitor consent, native config, and additional roots.
|
|
1068
|
+
Use `env.OURS_PORT`/`env.OURS_CONFIG` for a
|
|
1067
1069
|
role-specific ours daemon, or configure the host default in `~/.ours/config.json`.
|
|
1068
1070
|
|
|
1069
1071
|
## 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
|
+
}
|
|
@@ -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
|
|
@@ -103,6 +105,16 @@ export class FleetQueryService {
|
|
|
103
105
|
const lastExit = dir ? readExitRecord(join(dir, '.exit-status')) ?? undefined : undefined;
|
|
104
106
|
const session = await this.session(role, dir, live.state);
|
|
105
107
|
const problems = [...role.problems];
|
|
108
|
+
const recovery = dir ? readDaemonRecoveryStatus(dir) : undefined;
|
|
109
|
+
if (recovery && recovery.state !== 'recovered') {
|
|
110
|
+
const paths = ['agent', 'owner']
|
|
111
|
+
.filter(name => recovery.paths[name].state !== 'recovered')
|
|
112
|
+
.map(name => `${name}:${recovery.paths[name].state}`);
|
|
113
|
+
problems.push({
|
|
114
|
+
code: 'daemon_recovery', severity: 'warning', source: 'daemon-recovery',
|
|
115
|
+
detail: `${recovery.state}; ${paths.join(', ') || 'no degraded path'}; epoch ${recovery.epoch || 'unavailable'}`,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
106
118
|
if (live.state === 'running' && session.reachability !== 'online')
|
|
107
119
|
problems.push({
|
|
108
120
|
code: 'supervisor_session_disagreement', severity: 'warning',
|
|
@@ -5,21 +5,14 @@ import type { OpsDeps } from '../ops.js';
|
|
|
5
5
|
import type { ResolvedRole } from '../config.js';
|
|
6
6
|
import { FleetError } from './errors.js';
|
|
7
7
|
import { type ManagedFleetSpawnResult } from '../fleet-proxy.js';
|
|
8
|
-
import { type HarnessModelCatalog, type HarnessModelOption } from './model-catalog.js';
|
|
9
8
|
export interface CreateRoleSessionRequest {
|
|
10
9
|
name: string;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
model?: string | null;
|
|
14
|
-
reasoningEffort?: string | null;
|
|
15
|
-
session: 'acp';
|
|
10
|
+
brain: import('../config.js').AgentSelection;
|
|
11
|
+
role: import('../config.js').AgentSelection;
|
|
16
12
|
cwd?: string;
|
|
17
13
|
lifetime: 'permanent' | 'temporary';
|
|
18
|
-
mission?: string;
|
|
19
14
|
coordinator?: string;
|
|
20
15
|
permissions: CommonPermissions;
|
|
21
|
-
bio?: string;
|
|
22
|
-
persona?: string;
|
|
23
16
|
monitor?: WebCreationMonitor;
|
|
24
17
|
openAfterCreate: boolean;
|
|
25
18
|
highRiskAcknowledged?: boolean;
|
|
@@ -38,17 +31,6 @@ export type WebCreationMonitor = {
|
|
|
38
31
|
export interface CreationCapabilities {
|
|
39
32
|
available: boolean;
|
|
40
33
|
reasons: string[];
|
|
41
|
-
harnesses: Array<{
|
|
42
|
-
id: 'codex' | 'claude-code';
|
|
43
|
-
available: boolean;
|
|
44
|
-
sessions: Array<'acp'>;
|
|
45
|
-
defaultModel?: string;
|
|
46
|
-
models: string[];
|
|
47
|
-
modelOptions: HarnessModelOption[];
|
|
48
|
-
catalogSource: string;
|
|
49
|
-
customModelAllowed: true;
|
|
50
|
-
warnings: string[];
|
|
51
|
-
}>;
|
|
52
34
|
lifetimes: Array<'permanent' | 'temporary'>;
|
|
53
35
|
identityBootstrap: {
|
|
54
36
|
mode: 'current-fleet-first-boot';
|
|
@@ -123,7 +105,6 @@ export interface RoleCreationServiceOptions {
|
|
|
123
105
|
journalDir?: string;
|
|
124
106
|
probeReady?: (name: string, session: 'acp') => Promise<'ready' | 'attention' | 'unknown'>;
|
|
125
107
|
onProgress?: (action: CreationAction) => void;
|
|
126
|
-
modelCatalogs?: Partial<Record<'codex' | 'claude-code', () => HarnessModelCatalog>>;
|
|
127
108
|
/** Direct/managed callers must not create or restore the web action journal. */
|
|
128
109
|
journal?: boolean;
|
|
129
110
|
}
|