@ours.network/fleet 0.18.1 → 0.19.0-nightly.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -91
- package/dist/application/fleet-query-service.js +0 -12
- package/dist/application/role-creation-service.js +10 -7
- package/dist/application/types.d.ts +0 -11
- package/dist/briefing.js +6 -15
- package/dist/build-info.json +5 -5
- package/dist/cli.js +12 -37
- package/dist/config.d.ts +8 -6
- package/dist/config.js +46 -30
- package/dist/creation.d.ts +38 -22
- package/dist/creation.js +111 -24
- package/dist/docs.d.ts +1 -1
- package/dist/docs.js +44 -95
- package/dist/doctor.d.ts +5 -1
- package/dist/doctor.js +18 -11
- package/dist/fleet-proxy.d.ts +0 -5
- package/dist/harness/acp-agent.js +6 -11
- package/dist/harness/claude-code.js +11 -200
- package/dist/harness/codex.d.ts +1 -4
- package/dist/harness/codex.js +12 -70
- package/dist/harness/types.d.ts +4 -54
- package/dist/loops/manager.d.ts +1 -30
- package/dist/loops/manager.js +6 -69
- package/dist/loops/state.d.ts +0 -18
- package/dist/loops/state.js +0 -4
- package/dist/monitor.js +1 -1
- package/dist/ops.d.ts +3 -0
- package/dist/ops.js +8 -3
- package/dist/owner-channel/attachments.d.ts +25 -2
- package/dist/owner-channel/attachments.js +61 -5
- package/dist/owner-channel/channel.d.ts +29 -30
- package/dist/owner-channel/channel.js +291 -291
- package/dist/owner-channel/commands.js +89 -0
- package/dist/owner-channel/message-recovery.d.ts +25 -0
- package/dist/owner-channel/message-recovery.js +114 -0
- package/dist/owner-channel/notices.d.ts +0 -7
- package/dist/owner-channel/notices.js +0 -9
- package/dist/owner-channel/ours-client.d.ts +148 -0
- package/dist/owner-channel/ours-client.js +231 -0
- package/dist/rooms-tasks/cli.d.ts +4 -0
- package/dist/rooms-tasks/cli.js +565 -0
- package/dist/rooms-tasks/config.d.ts +15 -0
- package/dist/rooms-tasks/config.js +171 -0
- package/dist/rooms-tasks/cowork-adapter.d.ts +80 -0
- package/dist/rooms-tasks/cowork-adapter.js +48 -0
- package/dist/rooms-tasks/index.d.ts +7 -0
- package/dist/rooms-tasks/index.js +7 -0
- package/dist/rooms-tasks/room-state.d.ts +22 -0
- package/dist/rooms-tasks/room-state.js +117 -0
- package/dist/rooms-tasks/task-state.d.ts +31 -0
- package/dist/rooms-tasks/task-state.js +173 -0
- package/dist/rooms-tasks/templates.d.ts +6 -0
- package/dist/rooms-tasks/templates.js +80 -0
- package/dist/rooms-tasks/types.d.ts +153 -0
- package/dist/rooms-tasks/types.js +20 -0
- package/dist/runner.d.ts +0 -48
- package/dist/runner.js +94 -236
- package/dist/session/acp.d.ts +0 -104
- package/dist/session/acp.js +10 -213
- package/dist/session/conversation-normalizer.d.ts +0 -6
- package/dist/session/conversation-normalizer.js +10 -153
- package/dist/session/conversation-types.d.ts +4 -23
- package/dist/session/types.d.ts +0 -35
- package/dist/spawn.js +26 -33
- package/dist/supervisor/systemd.js +29 -2
- package/dist/watchdog/briefing.js +0 -7
- package/dist/watchdog/run.js +3 -3
- package/dist/web-app/assets/{TerminalView-C_G1ID2P.js → TerminalView-BAVk1Bot.js} +1 -1
- package/dist/web-app/assets/{index-BCBK78hw.js → index-C3S-xFRU.js} +5 -5
- package/dist/web-app/index.html +1 -1
- package/dist/worklog.d.ts +1 -7
- package/dist/worklog.js +39 -191
- package/package.json +3 -1
- package/dist/model-env.d.ts +0 -71
- package/dist/model-env.js +0 -106
- package/dist/owner-channel/mcp.d.ts +0 -24
- package/dist/owner-channel/mcp.js +0 -145
- package/dist/session/activity.d.ts +0 -31
- package/dist/session/activity.js +0 -48
package/README.md
CHANGED
|
@@ -68,6 +68,8 @@ The state dir contract:
|
|
|
68
68
|
| `.identity`, `.cwd`, `.session-id`, `.booted`, `.exit-status`, `.config-path` | supervisor | dot-marker state — session resume and boot bookkeeping |
|
|
69
69
|
| `.monitor-state.json`, `.monitor-status` | supervisor monitor | atomic body-free cursor/pending state and health |
|
|
70
70
|
| `.owner-channel-state.json` | owner-channel bridge | bounded wire-ID dedupe only; never message/reply plaintext |
|
|
71
|
+
| `.owner-channel-message-recovery.json` | owner-channel bridge | mode-0600 body-free message claim journal: wire ID, history sequence, and claim time only |
|
|
72
|
+
| `.owner-channel-attachment-recovery.json` | owner-channel bridge | mode-0600 attachment route journal; never filenames, paths, transcript text, or bytes |
|
|
71
73
|
| `.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 |
|
|
72
74
|
| `.session-events.jsonl`, `.control.sock`, `.control-token` | ACP backend | bounded typed console projection and private attachment control |
|
|
73
75
|
|
|
@@ -79,7 +81,7 @@ The state dir contract:
|
|
|
79
81
|
| tmux | roles using `session: tmux` (the default) | `apt install tmux` / `brew install tmux` |
|
|
80
82
|
| Node ≥ 22 | Claude roles using `session: acp` | required by the maintained Claude ACP adapter |
|
|
81
83
|
| a harness CLI, logged in | the agent itself | e.g. Claude Code (`claude`) or Codex CLI (`codex`) |
|
|
82
|
-
| `ours
|
|
84
|
+
| `ours` CLI + shared daemon | identity + agent-to-agent messaging | `npm i -g @ours.network/cli && ours daemon start` |
|
|
83
85
|
|
|
84
86
|
Linux only: `ours-fleet init` enables *linger* so roles run without a login session
|
|
85
87
|
and survive reboots. macOS: launchd agents start **at login** (no linger
|
|
@@ -208,27 +210,12 @@ offline shell and no stale fleet state.
|
|
|
208
210
|
The console provides evidence-separated inventory and status, ACP activity and
|
|
209
211
|
permission controls, redacted logs, typed text send, confirmed lifecycle
|
|
210
212
|
actions, transactional permanent/temporary creation, and a shared tmux browser
|
|
211
|
-
terminal. Identity is fixed to the role name. Creation uses the
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
ACP tool diffs are normalized before they enter the durable conversation
|
|
218
|
-
ledger. Small diffs retain their existing before/after representation. When an
|
|
219
|
-
adapter reports an oversized whole-file snapshot, fleet stores only the changed
|
|
220
|
-
region with path, operation, original byte counts, digest, and boundedness
|
|
221
|
-
metadata. Each retained changed side is capped at 64 KiB as a UTF-8-safe,
|
|
222
|
-
newest-content tail. The tail advances to a line boundary when a complete line
|
|
223
|
-
fits. If one logical line alone exceeds the cap, fleet retains its newest
|
|
224
|
-
UTF-8-safe suffix and records that it starts mid-line, together with omitted-byte
|
|
225
|
-
count and digest. Paths retain at most a 4 KiB suffix with the same explicit
|
|
226
|
-
size/digest/omission provenance, and a 320 KiB cap covers the complete normalized
|
|
227
|
-
update. An append therefore cannot replay a large historical file while hiding
|
|
228
|
-
the current appended text. The live web-console transcript projects only events
|
|
229
|
-
from the current runner generation and excludes adapter `session/load` replay;
|
|
230
|
-
those replay events remain in the durable ledger with `agent_replay` provenance
|
|
231
|
-
for diagnosis and recovery rather than appearing as current work.
|
|
213
|
+
terminal. Identity is fixed to the role name. Creation uses the authenticated
|
|
214
|
+
daemon inventory and reports verified, missing, or unknown evidence. A missing
|
|
215
|
+
permanent role identity is created deterministically before launch with local
|
|
216
|
+
discovery and auto-accept enabled, then the provisioning lease is released so
|
|
217
|
+
the harness only has to bind it. Fleet never silently replaces or force-adopts
|
|
218
|
+
an existing identity.
|
|
232
219
|
|
|
233
220
|
For a temporary role, those first-boot instructions preserve and bind an
|
|
234
221
|
existing identity when one is present. If the assigned identity is missing,
|
|
@@ -236,8 +223,9 @@ the role capability-detects the ours MCP `create_temporary_identity` tool and
|
|
|
236
223
|
uses it when available, so the newly created identity is owned and cleaned up
|
|
237
224
|
by that connector session lifecycle. Older ours servers remain compatible via
|
|
238
225
|
`create_identity`. A collision or creation error stops for operator review;
|
|
239
|
-
fleet never force-adopts or deletes identity state. Permanent roles
|
|
240
|
-
|
|
226
|
+
fleet never force-adopts or deletes identity state. Permanent roles are
|
|
227
|
+
provisioned by fleet before launch and never delegate normal identity creation
|
|
228
|
+
to the harness.
|
|
241
229
|
|
|
242
230
|
`node-pty` is optional: if its native module cannot load, ACP and all
|
|
243
231
|
non-terminal features remain available and tmux Terminal is disabled with a
|
|
@@ -360,10 +348,6 @@ defaults:
|
|
|
360
348
|
max_tokens: 500000 # session cap (harness-interpreted)
|
|
361
349
|
monitor:
|
|
362
350
|
mode: fleet # fleet (default) | native
|
|
363
|
-
worklog: # built-ins shown; set false to opt out
|
|
364
|
-
max_kb: 1024 # rotate only above this active-log size
|
|
365
|
-
keep_tail_kb: 256 # UTF-8 tail; line-aligned when one fits
|
|
366
|
-
max_archives: 12 # recent beside log; older preserved cold
|
|
367
351
|
roles:
|
|
368
352
|
Name: # [A-Za-z0-9_-]+
|
|
369
353
|
harness: claude-code
|
|
@@ -383,7 +367,7 @@ roles:
|
|
|
383
367
|
batch_ms: 2000 # coalesce a burst into one line (default 2000)
|
|
384
368
|
inject: notification # notification (default) | full (bodies inline; roadmap)
|
|
385
369
|
owner_channel: # optional trusted owner ingress; requires session: acp
|
|
386
|
-
identity: "Name Owner Channel" #
|
|
370
|
+
identity: "Name Owner Channel" # dedicated identity; fleet creates it with safe local defaults
|
|
387
371
|
owners: [owner-contact-cid] # authenticated ours contact IDs, never display names
|
|
388
372
|
agent: managed-agent-cid # exact role CID allowed to relay messages/files outward
|
|
389
373
|
interrupt: false # false queues; true cancels current work first
|
|
@@ -450,11 +434,6 @@ role's `harness_options`, so a fleet can set common Codex permission/profile def
|
|
|
450
434
|
and override individual keys per role. `monitor` merges the same way — a role block
|
|
451
435
|
overrides `defaults.monitor` key-by-key.
|
|
452
436
|
|
|
453
|
-
Every supervised role is a client of the operator-configured ours daemon. Fleet
|
|
454
|
-
forces `OURS_AUTOSTART=0` in both tmux and ACP harness processes, after role environment
|
|
455
|
-
overlays, so `env.OURS_AUTOSTART` cannot transfer shared daemon lifecycle ownership to an
|
|
456
|
-
agent. Operators and explicit installer/setup flows remain responsible for starting it.
|
|
457
|
-
|
|
458
437
|
### Scheduled agent loops
|
|
459
438
|
|
|
460
439
|
Top-level `loops` schedule literal prompts from trusted local YAML. Enabled targets
|
|
@@ -545,13 +524,10 @@ native settings actually grant, against a fixed floor:
|
|
|
545
524
|
those requests with nobody to see it. With `unattended: wait` it **warns**,
|
|
546
525
|
since a human can still attach a console and answer.
|
|
547
526
|
|
|
548
|
-
**Security meaning.** `ask` maps to Codex `untrusted` / Claude `default
|
|
549
|
-
`auto`
|
|
550
|
-
`
|
|
551
|
-
|
|
552
|
-
`danger-full-access`); Claude uses `bypassPermissions`. For Codex tmux, where the
|
|
553
|
-
approval and sandbox flags remain independent, `auto` is `on-request` and
|
|
554
|
-
`allow` is `never` while `filesystem` still chooses the sandbox.
|
|
527
|
+
**Security meaning.** `ask` maps to Codex `untrusted` / Claude `default`,
|
|
528
|
+
`auto` to Codex `on-request` / Claude `acceptEdits`, and `allow` to Codex
|
|
529
|
+
`never` / Claude `bypassPermissions`, the non-interactive modes that actually
|
|
530
|
+
permit the actions the role was authorized to take.
|
|
555
531
|
`dontAsk` suppresses only the *prompt*, not the denial, which is why an
|
|
556
532
|
`allow` role previously ran unable to do its job. Legacy `deny` is accepted
|
|
557
533
|
only for compatibility and retains its conservative Codex `on-request` /
|
|
@@ -563,14 +539,10 @@ ACP exposes agent-specific session mode IDs and `session/set_mode`, but no
|
|
|
563
539
|
portable permission-policy capability. Fleet uses that primitive where an
|
|
564
540
|
adapter has a corresponding mode and otherwise translates at the adapter. The
|
|
565
541
|
bundled Codex ACP adapter couples approval and sandboxing in its advertised
|
|
566
|
-
mode IDs
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
corresponding ACP preset; explicit native approval overrides also win. Fleet
|
|
571
|
-
reports a coupled-mode mismatch as approximate in `config`/`doctor`, and
|
|
572
|
-
per-role `isolation:` remains the boundary for an `allow` ACP role. Live session
|
|
573
|
-
metadata reports the normalized policy and the exact native mode selected.
|
|
542
|
+
mode IDs, so fleet preserves the selected sandbox preset and enforces approval
|
|
543
|
+
independently on the app-server turn request. Thus `allow` + `workspace` is
|
|
544
|
+
actually `never` + `workspace-write`, never `danger-full-access`. Live session
|
|
545
|
+
metadata reports the normalized policy and the ACP sandbox-preset ID.
|
|
574
546
|
|
|
575
547
|
### Isolation at creation time
|
|
576
548
|
|
|
@@ -672,13 +644,14 @@ agent's briefing tells it **not** to arm a native harness Monitor.
|
|
|
672
644
|
|
|
673
645
|
With `monitor.mode: native`, ours-fleet does not start its supervisor monitor;
|
|
674
646
|
the generated briefing instead instructs the harness to arm its own wake
|
|
675
|
-
mechanism (`ours
|
|
647
|
+
mechanism (the structured `ours api watch-notifications` JSONL stream for Claude Code,
|
|
648
|
+
or the Codex
|
|
676
649
|
`arm_monitor`/`foreground_monitor` flow). The old `monitor.enabled: true|false`
|
|
677
650
|
form remains accepted as a compatibility alias for `fleet|native`, respectively,
|
|
678
651
|
but new configuration should use `mode`.
|
|
679
652
|
|
|
680
653
|
`inject: full` (pushing message bodies
|
|
681
|
-
inline) is on the roadmap and needs two new
|
|
654
|
+
inline) is on the roadmap and needs two new daemon endpoints; today all
|
|
682
655
|
roles deliver `notification` lines and drain via `get_messages`.
|
|
683
656
|
|
|
684
657
|
ACP stdio remains private to the persistent runner. `send`, `peek`, and the basic
|
|
@@ -689,8 +662,10 @@ console later; no terminal UI is part of the monitor or session backend.
|
|
|
689
662
|
### Trusted owner channel
|
|
690
663
|
|
|
691
664
|
`owner_channel` adds a second ours identity to a role without changing the
|
|
692
|
-
role's normal identity.
|
|
693
|
-
|
|
665
|
+
role's normal identity. Fleet creates that dedicated permanent identity when it
|
|
666
|
+
is missing, with local exposure and local auto-accept disabled, and releases the
|
|
667
|
+
provisioning lease before the supervisor binds it. Connect it to each
|
|
668
|
+
owner/controller identity and the managed role identity, put the owners'
|
|
694
669
|
immutable contact CIDs in `owners`, and put the managed role identity's exact CID
|
|
695
670
|
in `agent`. The channel identity must not be any role identity or another role's
|
|
696
671
|
channel identity. Add it to the control plane just like another contact, then
|
|
@@ -902,22 +877,26 @@ command path sits behind the authenticated owner-CID check: a non-owner sending
|
|
|
902
877
|
`/force-restart` or `/model` is silently ignored exactly like any other
|
|
903
878
|
unauthorized mail.
|
|
904
879
|
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
880
|
+
Handled wire IDs are durably bounded for deduplication, while message and
|
|
881
|
+
response bodies stay out of fleet state. Before consuming mail, fleet reads the
|
|
882
|
+
body-free oldest-first metadata from `listIncomingMessages`, atomically journals
|
|
883
|
+
the exact wire/sequence slice (at most 200), and calls `getMessages` with that
|
|
884
|
+
exact length. A returned wire/sequence set mismatch fails closed. After a crash,
|
|
885
|
+
journaled bodies are loaded only from persistent history with `getHistoryItem`;
|
|
886
|
+
the journal is pruned only after the existing durable owner-channel state marks
|
|
887
|
+
the wire handled. This provides at-least-once turn delivery without placing
|
|
888
|
+
message plaintext in fleet state.
|
|
910
889
|
|
|
911
890
|
Inbound owner attachments use the same authenticated-CID and exact-wire routing
|
|
912
|
-
boundary. Fleet first calls the metadata-only `
|
|
891
|
+
boundary. Fleet first calls the metadata-only `listIncomingFiles`, groups a
|
|
913
892
|
file-only wake or a same-sender reply-linked text caption, and checks the enabled,
|
|
914
893
|
count, declared MIME, per-file size, and total-size policy before retrieving any
|
|
915
|
-
bytes. It then calls selective `
|
|
894
|
+
bytes. It then calls selective `getFiles` only for the admitted wire IDs. An
|
|
916
895
|
unauthorized sender is ignored without retrieval or reply. A rejected authorized
|
|
917
896
|
request receives a bounded reason correlated to its file wire.
|
|
918
897
|
|
|
919
898
|
Retrieved files must be regular, non-symlink paths whose byte count and SHA-256
|
|
920
|
-
match
|
|
899
|
+
match typed SDK metadata. Fleet additionally checks content signatures against the
|
|
921
900
|
declared MIME, sanitizes traversal/control characters from names, and copies each
|
|
922
901
|
file into a random request-scoped directory at mode 0700 with files at mode 0600.
|
|
923
902
|
The `[fleet-owner]` turn receives only bounded metadata, the private local paths,
|
|
@@ -928,17 +907,19 @@ the agent must use the audio path rather than inventing text.
|
|
|
928
907
|
Request files are removed after final delivery and stale directories are removed
|
|
929
908
|
after `retention_ms`. A bounded mode-0600 recovery journal stores only owner CID
|
|
930
909
|
and wire routing metadata—never filenames, paths, captions, transcripts, or file
|
|
931
|
-
bytes. If
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
910
|
+
bytes. If a selected file is already read when fleet restarts, fleet resolves
|
|
911
|
+
only that journaled wire with `getFileInfo` and retrieves its immutable blob with
|
|
912
|
+
`fetchFile`; a claimed managed-agent caption is loaded from message history and
|
|
913
|
+
rejoined with its journaled read files before the group is admitted or relayed.
|
|
914
|
+
Recovered voice files remain transcript-unavailable and are never sent through
|
|
915
|
+
a second transcription attempt. Conversation route state migrates from v1 to a
|
|
916
|
+
bounded v2
|
|
935
917
|
source-wire index so a correlated group keeps the authenticated owner selected by
|
|
936
|
-
its original request even after later owner traffic.
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
`
|
|
940
|
-
|
|
941
|
-
is currently configured.
|
|
918
|
+
its original request even after later owner traffic. Corrupt recovery state disables attachment
|
|
919
|
+
admission. The host must run an ours daemon compatible with SDK 2.0.1 whose
|
|
920
|
+
typed incoming-file, selective retrieval, and file-fetch operations support
|
|
921
|
+
these guarantees; `ours config show --json` reports `sttConfigured` without
|
|
922
|
+
revealing provider credentials.
|
|
942
923
|
|
|
943
924
|
Managed-agent file egress is a separate trust direction. It retains the same
|
|
944
925
|
metadata provenance checks, count/per-file/request byte caps, regular-file and
|
|
@@ -984,7 +965,7 @@ pass the legacy `ours-fleet spawn --monitor` flag) to record explicit persistent
|
|
|
984
965
|
for the harness-native monitor, including supervised restarts. This does not choose the
|
|
985
966
|
wake owner; use `monitor.mode: fleet|native` for that. In the standard-Codex fallback, the agent separately surfaces the
|
|
986
967
|
`ours-codex` recommendation before asking to enter `foreground_monitor`. It never backgrounds
|
|
987
|
-
|
|
968
|
+
the structured CLI watcher, because a detached process cannot wake a Codex turn. The foreground
|
|
988
969
|
wait is re-entered after each handled message; `ours-codex` instead wakes the idle
|
|
989
970
|
session through its App Server integration.
|
|
990
971
|
|
|
@@ -1068,7 +1049,7 @@ picks the most recently active session **in the role's `cwd`**. This works
|
|
|
1068
1049
|
cleanly as long as each role has its own `cwd` (the common case); two roles
|
|
1069
1050
|
sharing an identical `cwd` could have their resumes cross — give them distinct
|
|
1070
1051
|
working directories if that matters. MCP and monitor wiring is provided by
|
|
1071
|
-
[`@ours.network/codex`](https://
|
|
1052
|
+
[`@ours.network/codex`](https://www.npmjs.com/package/@ours.network/codex);
|
|
1072
1053
|
the core ours skill discovers fleet behavior through `ours-fleet docs`.
|
|
1073
1054
|
`ours-fleet doctor --harness codex` verifies the CLI, ours plugin, and enhanced launcher/fallback.
|
|
1074
1055
|
|
|
@@ -1140,7 +1121,7 @@ rollout, anchors, aliases, explicit tags, non-scalar keys, and multiple document
|
|
|
1140
1121
|
produce source-positioned warnings; opt into enforcement with
|
|
1141
1122
|
`--yaml-mode strict`. Strict mode will become the next-major default.
|
|
1142
1123
|
|
|
1143
|
-
|
|
1124
|
+
Long-running roles may opt into bounded durable logs:
|
|
1144
1125
|
|
|
1145
1126
|
```yaml
|
|
1146
1127
|
worklog:
|
|
@@ -1150,23 +1131,8 @@ worklog:
|
|
|
1150
1131
|
```
|
|
1151
1132
|
|
|
1152
1133
|
Rotation is conservative: a concurrent change aborts the attempt and retries at
|
|
1153
|
-
a later fleet lifecycle point.
|
|
1154
|
-
|
|
1155
|
-
alone exceeds the tail budget, its newest suffix remains and
|
|
1156
|
-
`.worklog-rotation.json` explicitly records the mid-line start and omitted byte
|
|
1157
|
-
count. It also records SHA-256 digests for the archive and retained live bytes
|
|
1158
|
-
observed when the manifest is written. The complete prior inode is published
|
|
1159
|
-
under a collision-safe UTC name.
|
|
1160
|
-
`max_archives` bounds recent archives beside `WORKLOG.md`; older complete
|
|
1161
|
-
archives move to `WORKLOG.archives/` and are never deleted. Use `worklog: false` on a role or in
|
|
1162
|
-
`defaults` to opt out. Archives may contain the same sensitive material as
|
|
1163
|
-
`WORKLOG.md`. Rotation refuses a symlinked/non-regular live log or a symlinked
|
|
1164
|
-
cold-archive boundary before replacing the live path, and best-effort removes a
|
|
1165
|
-
duplicate link left by a detected failure while the original inode is still
|
|
1166
|
-
available. These are ordinary path/error safeguards, not a security boundary
|
|
1167
|
-
against a malicious concurrent process with the same Unix authority: intentional
|
|
1168
|
-
symlink swaps or archive-directory renames between checks are outside the threat
|
|
1169
|
-
model and require OS-level isolation from that process.
|
|
1134
|
+
a later fleet lifecycle point. Archives remain in the role state directory and
|
|
1135
|
+
may contain the same sensitive material as `WORKLOG.md`.
|
|
1170
1136
|
|
|
1171
1137
|
Claude roles can use a credential-free loopback proxy:
|
|
1172
1138
|
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { lstatSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import { classifyActivity } from '../session/activity.js';
|
|
4
3
|
import { controlRequest } from '../session/control.js';
|
|
5
4
|
import { SessionControlError } from '../session/types.js';
|
|
6
5
|
import { readExitRecord, readRestartLedger } from '../runner.js';
|
|
@@ -56,10 +55,6 @@ function sessionOverall(supervisor, session, restart, monitor, isolation, proble
|
|
|
56
55
|
if (session.reachability === 'online'
|
|
57
56
|
&& (session.readiness === 'running' || session.readiness === 'awaiting_permission'))
|
|
58
57
|
return 'busy';
|
|
59
|
-
// `readiness: idle` alone never justifies `ready`. A steered wake turn is
|
|
60
|
-
// invisible to readiness (FLEET-002), so observed activity outranks it.
|
|
61
|
-
if (session.reachability === 'online' && session.activity.state === 'active')
|
|
62
|
-
return 'busy';
|
|
63
58
|
if (session.reachability === 'online' && session.readiness === 'idle')
|
|
64
59
|
return 'ready';
|
|
65
60
|
if (session.reachability === 'offline'
|
|
@@ -158,7 +153,6 @@ export class FleetQueryService {
|
|
|
158
153
|
protocolVersion: snapshot.protocolVersion, features: snapshot.features,
|
|
159
154
|
runtimeModel: snapshot.runtimeModel, reasoningEffort: snapshot.reasoningEffort,
|
|
160
155
|
permissionMode: snapshot.permissionMode,
|
|
161
|
-
activity: classifyActivity(snapshot.activity),
|
|
162
156
|
};
|
|
163
157
|
}
|
|
164
158
|
catch (error) {
|
|
@@ -170,7 +164,6 @@ export class FleetQueryService {
|
|
|
170
164
|
: failure === 'control-unavailable' ? 'unavailable' : 'unknown',
|
|
171
165
|
readiness: offline ? 'failed' : 'unknown',
|
|
172
166
|
evidence: 'authoritative', lastError: clean(error.message),
|
|
173
|
-
activity: { state: 'unobservable' },
|
|
174
167
|
};
|
|
175
168
|
}
|
|
176
169
|
}
|
|
@@ -181,23 +174,18 @@ export class FleetQueryService {
|
|
|
181
174
|
backend: 'tmux', reachability: has ? 'online' : supervisor === 'stopped' ? 'offline' : 'unknown',
|
|
182
175
|
readiness: has ? 'idle' : supervisor === 'running' ? 'starting' : 'failed',
|
|
183
176
|
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
177
|
};
|
|
188
178
|
}
|
|
189
179
|
catch (error) {
|
|
190
180
|
return {
|
|
191
181
|
backend: 'tmux', reachability: 'unknown', readiness: 'unknown',
|
|
192
182
|
evidence: 'inferred', lastError: clean(error.message),
|
|
193
|
-
activity: { state: 'unobservable' },
|
|
194
183
|
};
|
|
195
184
|
}
|
|
196
185
|
}
|
|
197
186
|
return {
|
|
198
187
|
backend: 'unknown', reachability: supervisor === 'stopped' ? 'offline' : 'unknown',
|
|
199
188
|
readiness: supervisor === 'stopped' ? 'failed' : 'unknown', evidence: 'inferred',
|
|
200
|
-
activity: { state: 'unobservable' },
|
|
201
189
|
};
|
|
202
190
|
}
|
|
203
191
|
}
|
|
@@ -118,9 +118,7 @@ export class RoleCreationService {
|
|
|
118
118
|
};
|
|
119
119
|
const warnings = [];
|
|
120
120
|
if (request.permissions.approval === 'allow')
|
|
121
|
-
warnings.push(
|
|
122
|
-
? 'approval=allow maps to elevated Codex ACP agent-full-access and widens the native sandbox to danger-full-access'
|
|
123
|
-
: 'approval=allow maps to an elevated native permission mode');
|
|
121
|
+
warnings.push('approval=allow maps to an elevated native permission mode');
|
|
124
122
|
if (request.permissions.filesystem === 'unrestricted')
|
|
125
123
|
warnings.push('filesystem=unrestricted grants access outside the workspace');
|
|
126
124
|
if (request.permissions.unattended === 'wait')
|
|
@@ -208,9 +206,12 @@ export class RoleCreationService {
|
|
|
208
206
|
throw new FleetError('stale_state', `identity '${action.roleId}' appeared after preview; review and confirm reuse`);
|
|
209
207
|
}
|
|
210
208
|
const creation = {
|
|
211
|
-
//
|
|
212
|
-
//
|
|
213
|
-
|
|
209
|
+
// Permanent identity infrastructure is reconciled before launch. A
|
|
210
|
+
// temporary identity must still be created by its eventual connector
|
|
211
|
+
// session because that exact lease owns cleanup.
|
|
212
|
+
identityProvisioner: preview.effective.lifetime === 'permanent'
|
|
213
|
+
? this.identityProvisioner
|
|
214
|
+
: { exists: name => this.identityProvisioner.exists(name) },
|
|
214
215
|
onStage: (stage, evidence) => this.coreStage(action, stage, evidence),
|
|
215
216
|
};
|
|
216
217
|
if (preview.effective.lifetime === 'permanent') {
|
|
@@ -220,7 +221,9 @@ export class RoleCreationService {
|
|
|
220
221
|
await spawnTemp(this.spawnOptions(preview.request, action.actionId), this.options.binPath, this.options.tempLauncher, creation);
|
|
221
222
|
}
|
|
222
223
|
this.stage(action, 'launched', 'launch accepted; readiness not yet confirmed');
|
|
223
|
-
this.stage(action, 'identity_bootstrap_pending',
|
|
224
|
+
this.stage(action, 'identity_bootstrap_pending', preview.effective.lifetime === 'permanent'
|
|
225
|
+
? 'fleet established the permanent identity; the harness must bind it from the generated briefing'
|
|
226
|
+
: 'the harness must choose or create and bind its session-owned temporary identity from the generated briefing');
|
|
224
227
|
this.stage(action, 'waiting_for_session');
|
|
225
228
|
const ready = await this.waitForReady(action.roleId, action.session);
|
|
226
229
|
if (ready === 'ready') {
|
|
@@ -72,17 +72,6 @@ export interface RoleStatus {
|
|
|
72
72
|
runtimeModel?: SessionSnapshot['runtimeModel'];
|
|
73
73
|
reasoningEffort?: SessionSnapshot['reasoningEffort'];
|
|
74
74
|
permissionMode?: SessionSnapshot['permissionMode'];
|
|
75
|
-
/**
|
|
76
|
-
* Activity evidence, kept separate from `readiness` on purpose: `readiness`
|
|
77
|
-
* answers "is a fleet-tracked turn in flight" (the prompt-admission gate),
|
|
78
|
-
* NOT "is this agent working". `state` is the only field a human-facing
|
|
79
|
-
* surface may use to call a role idle or stalled.
|
|
80
|
-
*/
|
|
81
|
-
activity: {
|
|
82
|
-
state: 'active' | 'quiet' | 'unobservable';
|
|
83
|
-
activeToolCalls?: number;
|
|
84
|
-
lastUpdateAt?: string;
|
|
85
|
-
};
|
|
86
75
|
};
|
|
87
76
|
restart: {
|
|
88
77
|
circuit: 'closed' | 'open';
|
package/dist/briefing.js
CHANGED
|
@@ -59,17 +59,15 @@ export function generateBriefing(role, v, opts) {
|
|
|
59
59
|
else if (guarantee === 'unverified') {
|
|
60
60
|
L.push(`2. BIND your ours identity: call the **${v.bindTool}** tool with`);
|
|
61
61
|
L.push(` name "${id}" force=true (search the deferred tool registry first if needed).`);
|
|
62
|
-
L.push(` - This identity was NOT verified
|
|
63
|
-
L.push(
|
|
64
|
-
L.push(' to mint it, then you are bound. Re-binding your OWN identity is always allowed.');
|
|
62
|
+
L.push(` - This permanent identity was NOT verified before launch. If it does not exist, STOP`);
|
|
63
|
+
L.push(' and report the infrastructure error; identity creation belongs to the fleet lifecycle.');
|
|
65
64
|
}
|
|
66
65
|
else {
|
|
67
66
|
L.push(`2. BIND your ours identity: call the **${v.bindTool}** tool with`);
|
|
68
67
|
L.push(` name "${id}" force=true (search the deferred tool registry first if needed).`);
|
|
69
68
|
L.push(` - It was ${guarantee === 'created' ? 'created' : 'verified to exist'} when your role`);
|
|
70
|
-
L.push(' was
|
|
71
|
-
L.push(
|
|
72
|
-
L.push(' something removed it after your role was created.');
|
|
69
|
+
L.push(' was started, so binding should succeed. If it unexpectedly reports no such identity,');
|
|
70
|
+
L.push(' STOP and report the infrastructure race; do not create or replace it yourself.');
|
|
73
71
|
}
|
|
74
72
|
L.push(`3. RECONCILE your profile (idempotent): call **${v.currentIdentityTool}** and read your`);
|
|
75
73
|
L.push(' current bio and persona, so you only write below when they actually differ.');
|
|
@@ -168,11 +166,6 @@ export function generateBriefing(role, v, opts) {
|
|
|
168
166
|
L.push('Never translate any other failure into "dead". A busy agent, an unanswered control');
|
|
169
167
|
L.push('plane and a confirmed stop look identical if you only look at one command.');
|
|
170
168
|
L.push('');
|
|
171
|
-
L.push('`ours-fleet status` reports `session.readiness`, which is TURN OCCUPANCY only: a mail');
|
|
172
|
-
L.push('wake delivered by steering runs a whole turn with readiness pinned at `idle`. Read the');
|
|
173
|
-
L.push('`activity:` line beside it — `active` means the role is working — and never call a role');
|
|
174
|
-
L.push('idle or stalled from `readiness=idle` alone.');
|
|
175
|
-
L.push('');
|
|
176
169
|
L.push('Then judge the console content: stuck on a prompt/menu/trust dialog → answer it directly');
|
|
177
170
|
L.push('with `ours-fleet send <Name> "<text>"` (or `--key <K>` for raw keys); idle with work');
|
|
178
171
|
L.push('assigned → nudge; actively working → do nothing, and do not mistake a long turn for a');
|
|
@@ -183,10 +176,8 @@ export function generateBriefing(role, v, opts) {
|
|
|
183
176
|
L.push('it survives restarts.');
|
|
184
177
|
if (role.worklog) {
|
|
185
178
|
L.push(`Fleet rotates it above ${role.worklog.max_kb} KiB, keeps approximately the newest ` +
|
|
186
|
-
`${role.worklog.keep_tail_kb} KiB here, and
|
|
187
|
-
'beside it.
|
|
188
|
-
'.worklog-rotation.json identifies the latest archive for restart provenance. ' +
|
|
189
|
-
'Continue writing only WORKLOG.md.');
|
|
179
|
+
`${role.worklog.keep_tail_kb} KiB here, and retains ${role.worklog.max_archives} archives ` +
|
|
180
|
+
'beside it. Continue writing only WORKLOG.md.');
|
|
190
181
|
}
|
|
191
182
|
L.push('', '## Routines');
|
|
192
183
|
L.push(`If \`${opts.routinesPath}\` exists, re-read it at the START of every wake — before acting`);
|
package/dist/build-info.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.
|
|
3
|
-
"buildId": "
|
|
4
|
-
"commit": "
|
|
5
|
-
"dirty":
|
|
6
|
-
"builtAt": "2026-08-
|
|
2
|
+
"version": "0.19.0-nightly.2",
|
|
3
|
+
"buildId": "13880d55bd06",
|
|
4
|
+
"commit": "ccf83082de238e6dc8ddb1bc557d2b7ffc7e816f",
|
|
5
|
+
"dirty": true,
|
|
6
|
+
"builtAt": "2026-08-23T08:33:08.792Z",
|
|
7
7
|
"capabilities": [
|
|
8
8
|
"monitor.interrupt.after_tool"
|
|
9
9
|
]
|
package/dist/cli.js
CHANGED
|
@@ -25,9 +25,8 @@ import { stringify } from 'yaml';
|
|
|
25
25
|
import { resolvedRolePlan } from './resolved-plan.js';
|
|
26
26
|
import { creationBuildNote, formatProvenance, readProvenance } from './creation.js';
|
|
27
27
|
import { doctor } from './doctor.js';
|
|
28
|
-
import { allWarnings, analyzeFleetPermissions,
|
|
28
|
+
import { allWarnings, analyzeFleetPermissions, formatNative } from './permissions.js';
|
|
29
29
|
import { AI_DOCS } from './docs.js';
|
|
30
|
-
import { classifyActivity, describeSessionState } from './session/activity.js';
|
|
31
30
|
import { controlRequest, controlSocketPath, followControl, livenessNote, } from './session/control.js';
|
|
32
31
|
import { SessionControlError } from './session/types.js';
|
|
33
32
|
import { readScheduledLoops, storedLoopHealth } from './loops/state.js';
|
|
@@ -38,6 +37,7 @@ import { WebAccessStore, passwordAccess, validatePublicOrigin } from './web/acce
|
|
|
38
37
|
import { FLEET_PROXY_CALLER_ENV, FLEET_PROXY_STATE_DIR_ENV, } from './fleet-proxy.js';
|
|
39
38
|
import './harness/claude-code.js'; // registers the claude-code adapter
|
|
40
39
|
import './harness/codex.js'; // registers the codex adapter
|
|
40
|
+
import { registerTemplateCommands, registerTaskCommands, registerRoomCommands } from './rooms-tasks/cli.js';
|
|
41
41
|
// sudo/su shells lack XDG_RUNTIME_DIR, breaking every systemctl/journalctl
|
|
42
42
|
// --user child (supervisor commands, logs, doctor). Derive it before dispatch. (#9)
|
|
43
43
|
deriveXdgRuntimeDir();
|
|
@@ -364,14 +364,8 @@ program.command('ls').description('list running fleet sessions')
|
|
|
364
364
|
continue;
|
|
365
365
|
try {
|
|
366
366
|
const response = await controlRequest(stateDir, { command: 'status' }, 2_000);
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
// Activity, not readiness: an idle-readiness role may be running a
|
|
370
|
-
// steered wake turn (FLEET-002), so `ls` reports what was observed.
|
|
371
|
-
const observed = classifyActivity(result.activity);
|
|
372
|
-
acp.push(`${name}: acp${observed.state === 'active' ? ' (working)'
|
|
373
|
-
: observed.state === 'quiet' ? ' (no recent agent activity)' : ''}`);
|
|
374
|
-
}
|
|
367
|
+
if (response.ok && response.result?.alive)
|
|
368
|
+
acp.push(`${name}: acp`);
|
|
375
369
|
}
|
|
376
370
|
catch { /* ignore stale sockets */ }
|
|
377
371
|
}
|
|
@@ -512,14 +506,6 @@ program.command('status <name>').description('unit/agent state')
|
|
|
512
506
|
else if (ledger.consecutiveImmediateFailures > 0)
|
|
513
507
|
console.log(`restarts: ${ledger.consecutiveImmediateFailures} consecutive immediate `
|
|
514
508
|
+ `failures, next delay ${ledger.nextDelayMs}ms (${ledger.lastReason})`);
|
|
515
|
-
// A kill the supervisor never saw leaves no other trace here. Say it out
|
|
516
|
-
// loud rather than letting the absence of a failure streak read as "fine".
|
|
517
|
-
if (ledger.lastTermination?.class === 'abrupt')
|
|
518
|
-
console.log(`last termination: ABRUPT — the previous supervisor run (started `
|
|
519
|
-
+ `${ledger.lastTermination.runStartedAt ?? 'unknown'}) died without an orderly exit; `
|
|
520
|
-
+ `observed ${ledger.lastTermination.observedAt}`
|
|
521
|
-
+ `\n abrupt terminations recorded: ${ledger.abruptTerminations ?? 1}`
|
|
522
|
-
+ `\n cause (signal / OOM-kill): journalctl --user -u ours-fleet-agent@${name}.service`);
|
|
523
509
|
const modelStatus = joinPath(agentDir(name), '.model-status');
|
|
524
510
|
if (existsSync(modelStatus)) {
|
|
525
511
|
try {
|
|
@@ -538,14 +524,8 @@ program.command('status <name>').description('unit/agent state')
|
|
|
538
524
|
if (stateDir) {
|
|
539
525
|
try {
|
|
540
526
|
const response = await controlRequest(stateDir, { command: 'status' }, 2_000);
|
|
541
|
-
if (response.ok)
|
|
542
|
-
const snapshot = response.result;
|
|
527
|
+
if (response.ok)
|
|
543
528
|
console.log(`session: ${JSON.stringify(response.result)}`);
|
|
544
|
-
// `readiness` is turn occupancy, never an activity claim: a steered
|
|
545
|
-
// wake turn runs to completion with readiness pinned at `idle`
|
|
546
|
-
// (FLEET-002). Say which question each field answers.
|
|
547
|
-
console.log(describeSessionState(snapshot.readiness, snapshot.activity));
|
|
548
|
-
}
|
|
549
529
|
}
|
|
550
530
|
catch {
|
|
551
531
|
console.log('session: acp control unavailable');
|
|
@@ -656,10 +636,7 @@ function renderLoopRows(role, state, loop) {
|
|
|
656
636
|
return Object.entries(state.loops).filter(([name]) => !loop || name === loop).map(([name, item]) => `${role}/${name} ${item.enabled && !item.operatorDisabled ? 'enabled' : 'disabled'} `
|
|
657
637
|
+ `${item.activeRunId ? 'running' : 'idle'} next=${item.nextDueAt} last=${item.lastOutcome ?? 'never'} `
|
|
658
638
|
+ `counts=${item.counts.started}/${item.counts.completed}/${item.counts.failed} `
|
|
659
|
-
+ `skip=${item.counts.skipped}(busy=${item.counts.skippedBusy},missed=${item.counts.skippedMissed})`
|
|
660
|
-
+ (item.missedGap
|
|
661
|
-
? ` gap=${item.missedGap.count}@${item.missedGap.fromAt}..${item.missedGap.throughAt}`
|
|
662
|
-
: ''));
|
|
639
|
+
+ `skip=${item.counts.skipped}(busy=${item.counts.skippedBusy},missed=${item.counts.skippedMissed})`);
|
|
663
640
|
}
|
|
664
641
|
cOpt(loopsCommand.command('status [role] [loop]').description('show live or stored loop state'))
|
|
665
642
|
.option('--json', 'emit stable JSON')
|
|
@@ -1113,7 +1090,7 @@ cOpt(program.command('spawn [name]').description('spawn a new agent (permanent b
|
|
|
1113
1090
|
.option('--coordinator <name>', 'announce target')
|
|
1114
1091
|
.option('--model <id>', 'model id to launch on (e.g. claude-fable-5); default: launcher default')
|
|
1115
1092
|
.option('--permission-mode <mode>', 'harness permission mode (Codex: untrusted|on-request|never; Claude: native values)')
|
|
1116
|
-
.option('--approval <mode>', 'fleet permission mode: ask|auto|allow (
|
|
1093
|
+
.option('--approval <mode>', 'fleet permission mode: ask|auto|allow (deny is deprecated)')
|
|
1117
1094
|
.option('--filesystem <mode>', 'common filesystem intent: read-only|workspace|unrestricted')
|
|
1118
1095
|
.option('--unattended <mode>', 'permission behavior without a console: deny|wait')
|
|
1119
1096
|
.option('--sandbox <mode>', 'Codex sandbox: read-only|workspace-write|danger-full-access')
|
|
@@ -1185,17 +1162,13 @@ cOpt(program.command('spawn [name]').description('spawn a new agent (permanent b
|
|
|
1185
1162
|
console.log(`spawned ${result.lifetime} agent '${result.role}' through `
|
|
1186
1163
|
+ `${result.caller}'s fleet proxy (state: ${result.statePath})`);
|
|
1187
1164
|
console.log(` ${result.harness}/${result.session}`
|
|
1188
|
-
+ `${result.model ? ` model=${result.model}` : ''}`
|
|
1189
|
-
+ (result.permissionMode
|
|
1190
|
-
? `; permission=${result.permissionMode.fleetMode} native=${result.permissionMode.nativeMode}`
|
|
1191
|
-
: '') + '; '
|
|
1165
|
+
+ `${result.model ? ` model=${result.model}` : ''}; `
|
|
1192
1166
|
+ `monitor=${result.monitor.mode} interrupt=${result.monitor.interrupt}`);
|
|
1193
1167
|
if (result.inherited.length)
|
|
1194
1168
|
console.log(` inherited omitted defaults from ${result.caller}: ${result.inherited.join(', ')}`);
|
|
1195
1169
|
console.log(`→ watch it: ours-fleet peek ${result.role} | attach: ours-fleet attach ${result.role}`);
|
|
1196
1170
|
return;
|
|
1197
1171
|
}
|
|
1198
|
-
const plannedPermissionMode = effectivePermissionMode(spawnDryRun(o).resolvedRole);
|
|
1199
1172
|
if (o.temp) {
|
|
1200
1173
|
const dir = await spawnTemp(o, binPath);
|
|
1201
1174
|
console.log(`spawned temp agent '${roleName}' (state: ${dir}; gone on exit/reboot)`);
|
|
@@ -1213,8 +1186,6 @@ cOpt(program.command('spawn [name]').description('spawn a new agent (permanent b
|
|
|
1213
1186
|
for (const line of formatProvenance(lastProvenance))
|
|
1214
1187
|
console.log(line);
|
|
1215
1188
|
}
|
|
1216
|
-
console.log(` permission=${plannedPermissionMode.fleetMode} `
|
|
1217
|
-
+ `native=${plannedPermissionMode.nativeMode}`);
|
|
1218
1189
|
console.log(`→ watch it: ours-fleet peek ${roleName} | attach: ours-fleet attach ${roleName}`);
|
|
1219
1190
|
}
|
|
1220
1191
|
catch (e) {
|
|
@@ -1433,6 +1404,10 @@ function configureWebAccess(opts) {
|
|
|
1433
1404
|
+ 'or --pairing for protection, or --no-password only for intentional unprotected access');
|
|
1434
1405
|
return undefined;
|
|
1435
1406
|
}
|
|
1407
|
+
// ── rooms & tasks ──────────────────────────────────────────────────────────
|
|
1408
|
+
registerTemplateCommands(program, cOpt);
|
|
1409
|
+
registerTaskCommands(program, cOpt);
|
|
1410
|
+
registerRoomCommands(program, cOpt);
|
|
1436
1411
|
program.command('_run <name>', { hidden: true }).description('internal: supervisor entrypoint')
|
|
1437
1412
|
.option('-c, --configuration <file>')
|
|
1438
1413
|
.action(async (name, opts) => {
|