@ours.network/fleet 0.12.0 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +184 -0
  2. package/dist/briefing.js +10 -0
  3. package/dist/cli.js +404 -1
  4. package/dist/config.d.ts +18 -2
  5. package/dist/config.js +67 -3
  6. package/dist/docs.d.ts +1 -1
  7. package/dist/docs.js +111 -0
  8. package/dist/duration.js +7 -3
  9. package/dist/harness/claude-code.js +5 -0
  10. package/dist/harness/types.d.ts +6 -0
  11. package/dist/loops/config.d.ts +30 -0
  12. package/dist/loops/config.js +135 -0
  13. package/dist/loops/manager.d.ts +48 -0
  14. package/dist/loops/manager.js +237 -0
  15. package/dist/loops/state.d.ts +54 -0
  16. package/dist/loops/state.js +148 -0
  17. package/dist/monitor.js +26 -2
  18. package/dist/owner-channel/attachments.d.ts +74 -0
  19. package/dist/owner-channel/attachments.js +378 -0
  20. package/dist/owner-channel/channel.d.ts +114 -2
  21. package/dist/owner-channel/channel.js +622 -43
  22. package/dist/owner-channel/notices.d.ts +21 -0
  23. package/dist/owner-channel/notices.js +66 -0
  24. package/dist/owner-channel/state.d.ts +34 -0
  25. package/dist/owner-channel/state.js +148 -1
  26. package/dist/owner-channel/tasks.d.ts +62 -0
  27. package/dist/owner-channel/tasks.js +246 -0
  28. package/dist/resolved-plan.js +11 -0
  29. package/dist/runner.js +87 -7
  30. package/dist/session/acp.d.ts +5 -2
  31. package/dist/session/acp.js +83 -25
  32. package/dist/session/arbiter.d.ts +42 -0
  33. package/dist/session/arbiter.js +72 -0
  34. package/dist/session/control.d.ts +12 -1
  35. package/dist/session/control.js +56 -3
  36. package/dist/session/types.d.ts +26 -2
  37. package/dist/session/types.js +5 -2
  38. package/dist/spawn.js +1 -0
  39. package/dist/supervisor/systemd.js +12 -2
  40. package/package.json +1 -1
package/README.md CHANGED
@@ -236,6 +236,9 @@ ours-fleet config [-c FILE] validate + print merged plan
236
236
  ours-fleet ls | attach | peek | logs [-f] | status <Name>
237
237
  ours-fleet send <Name> "text" | --key <K>
238
238
  ours-fleet spawn [--temp] <Name> [--harness --session --mission --model --approval ...]
239
+ ours-fleet loops validate|list|status
240
+ ours-fleet loops reload <Role>
241
+ ours-fleet loops run-now|disable|enable <Role> <Loop>
239
242
  ours-fleet rm <Name>
240
243
  ours-fleet doctor [--harness H]
241
244
  ours-fleet init
@@ -285,6 +288,13 @@ roles:
285
288
  owners: [owner-contact-cid] # authenticated ours contact IDs, never display names
286
289
  interrupt: false # false queues; true cancels current work first
287
290
  progress_interval_ms: 30000 # fleet-generated progress notices; 0 disables
291
+ attachments: # secure inbound documents, images, and voice
292
+ enabled: true
293
+ max_files_per_request: 4 # 1..32; rejected from metadata before retrieval
294
+ max_file_bytes: 10485760 # 10 MiB
295
+ max_request_bytes: 20971520 # 20 MiB total, and >= max_file_bytes
296
+ retention_ms: 86400000 # stale crash cleanup; 1 minute..30 days
297
+ allowed_mime: [application/pdf, text/plain, image/png, audio/ogg]
288
298
  model: claude-fable-5 # launch on a specific model (pass-through id; default: launcher default)
289
299
  mission: one line
290
300
  persona: | # operating contract (published as persona)
@@ -316,6 +326,17 @@ roles:
316
326
  fs: { read: [/opt/toolchains], write: [] } # extra binds (state dir + cwd always included)
317
327
  resources: { mem: 2G, cpu: "1.5", pids: 512 }
318
328
  secrets: ["/host/tok:/run/secrets/tok"] # host:container, mounted read-only
329
+
330
+ loops: # trusted local scheduled ACP turns
331
+ coordinator_pass:
332
+ roles: [FleetCoordinator] # or ["*"] for permanent roles only
333
+ interval: 10m # 1m..30d
334
+ initial_delay: 10m # default: one full interval; explicit 0s is immediate
335
+ jitter: 30s # default 0; less than interval and at most 1h
336
+ enabled: true
337
+ prompt: |
338
+ Review current fleet state once. Unstick only actionable work.
339
+ If nothing material changed, complete silently without an owner report.
319
340
  ```
320
341
 
321
342
  Merge order: `fleet.yaml` ← `fleet.d/*.yaml`; a duplicate role name is a hard
@@ -327,6 +348,36 @@ role's `harness_options`, so a fleet can set common Codex permission/profile def
327
348
  and override individual keys per role. `monitor` merges the same way — a role block
328
349
  overrides `defaults.monitor` key-by-key.
329
350
 
351
+ ### Scheduled agent loops
352
+
353
+ Top-level `loops` schedule literal prompts from trusted local YAML. Enabled targets
354
+ must use `session: acp`; temporary roles never inherit loops, including `roles:
355
+ ["*"]`. Fleet rejects an enabled loop when its explicitly selected base config is
356
+ a symlink, is owned by another user, or is group/world writable. Prompts are
357
+ bounded and normalized at validation time, but only their size and SHA-256 appear
358
+ in `config`, `list`, logs, or durable state.
359
+
360
+ Each occurrence is idle-only. An owner, console, monitor, or earlier turn already
361
+ using the role causes that occurrence to be recorded as `skipped_busy` and
362
+ discarded. Missed ticks, races, and failures are likewise recorded once: there is
363
+ no backlog, coalescing, catch-up turn, retry-on-idle, or cadence drift. The default
364
+ first run is one full interval after startup; set `initial_delay: 0s` explicitly
365
+ for an immediate first attempt. Restart recovery marks an in-flight run abandoned,
366
+ skips overdue ticks, and resumes the fixed nominal cadence.
367
+
368
+ Operational state is a mode-0600 `.scheduled-loops.json` in the permanent role's
369
+ state directory. `disable` persists across restarts; `enable` cannot override
370
+ `enabled: false` in YAML. `reload` re-reads the remembered trusted config through
371
+ the authenticated private control socket. Prompt-only edits retain cadence;
372
+ schedule or selector changes reset that loop to its configured initial delay.
373
+ `run-now` still obeys idle-only admission and returns exit 3 when busy; an
374
+ unavailable or uncertain control plane returns exit 2 and is never retried.
375
+
376
+ A scheduled turn has typed internal provenance and no owner authority. It cannot
377
+ cancel owner work and its ordinary completion is local only. Material proactive
378
+ owner reporting remains possible solely through an already-open authenticated
379
+ owner-channel task route; a no-op Coordinator pass should complete silently.
380
+
330
381
  ### Never-prompt failure
331
382
 
332
383
  An unattended role has no console, so a permission request has nobody to answer
@@ -457,6 +508,12 @@ Set `monitor.interrupt: true` on roles where every configured wake should cancel
457
508
  the active turn before the notification is delivered. This is intentionally
458
509
  content-blind: the supervisor cannot inspect encrypted message bodies, so all
459
510
  events selected by `wake_sources` receive the same interrupt policy.
511
+ The default is `false`: a role that must begin a post-readiness mission
512
+ immediately, including second-and-later mail received while it is working, must
513
+ set `monitor.mode: fleet` and `monitor.interrupt: true` explicitly. Readiness and
514
+ mission delivery still use ordinary ours mail: the role announces readiness,
515
+ waits for a body-free `[fleet-monitor]` wake, then calls `get_messages`; fleet
516
+ does not inject the mission body through ACP.
460
517
  It primes the notification cursor *before* the session launches
461
518
  (no missed arrivals), cannot be orphaned or left deaf-but-armed, and writes its
462
519
  health to `<agentDir>/.monitor-status` (`armed | degraded | failed`), surfaced in
@@ -492,6 +549,105 @@ to each owner/controller identity, and put the owners' immutable contact CIDs in
492
549
  channel identity. Add it to the control plane just like another contact, then
493
550
  message it directly.
494
551
 
552
+ The running supervisor remains the only process which binds that identity.
553
+ Operators manage it, and an active agent turn emits bounded updates, through the
554
+ role's authenticated Unix control socket:
555
+
556
+ ```sh
557
+ ours-fleet owner-channel contact list Coordinator
558
+ ours-fleet owner-channel contact invite Coordinator --name Mobile
559
+ ours-fleet owner-channel contact add Coordinator --invite-file ./invite.txt --name Mobile
560
+ # Or keep invite material out of both argv and a file:
561
+ ours-fleet owner-channel contact add Coordinator --invite-stdin
562
+
563
+ ours-fleet owner-channel owner list Coordinator
564
+ ours-fleet owner-channel owner authorize Coordinator <exact-64-hex-contact-cid>
565
+ ours-fleet owner-channel owner revoke Coordinator <exact-64-hex-contact-cid>
566
+
567
+ # Used only from the active [fleet-owner] turn; body is stdin, never argv:
568
+ ours-fleet owner-channel update Coordinator <request-id> --phase working --message-stdin
569
+
570
+ # Register background work while that request is active, then report after its final:
571
+ ours-fleet owner-channel task open Coordinator <active-request-id>
572
+ ours-fleet owner-channel task report Coordinator <task-id> --phase done --message-stdin
573
+ ```
574
+
575
+ Pairing is deliberately two-step. `contact add` accepts an invite and reports a
576
+ pending contact handshake; it never grants authority. After peer verification,
577
+ use `contact list` to obtain the immutable CID, then explicitly `owner authorize`
578
+ that exact CID. Invite generation prints the invite only to stdout. Acceptance
579
+ reads it from stdin or a file, never a process argument.
580
+
581
+ Configured `owners` are the declared baseline. Authorize/revoke operations add
582
+ a bounded dynamic overlay stored beside the role state in mode 0600 and applied
583
+ immediately by the already-bound channel. `owner list` shows each CID's
584
+ `baseline`/`dynamic` source and whether it is effective. The overlay survives
585
+ session/supervisor restart; it stores only CIDs and a bounded action audit—never
586
+ invites, message bodies, credentials, or keys. A corrupt overlay fails closed
587
+ (no effective owners and no mutation), and the last effective owner cannot be
588
+ revoked.
589
+
590
+ These commands require a running ACP role with `owner_channel` enabled. Missing,
591
+ stopped, tmux, disabled, draining, and unavailable-MCP targets fail without
592
+ starting a second client, binding an identity, or opening a network listener.
593
+
594
+ An owner request follows one ordered lifecycle on its authenticated source wire:
595
+
596
+ 1. Fleet sends an immediate receipt describing started, queued, or interrupting state.
597
+ 2. Periodic fleet-generated summaries may report allowlisted ACP activity shapes.
598
+ 3. The agent may explicitly send multiple high-level updates with the per-request
599
+ ID injected into its prompt. Phases are `working`, `approval`, and `blocked`,
600
+ rendered as precise `🔄`, `🔐`, and `🚧` notices. The one-line body is limited
601
+ to 280 characters/1024 bytes, deduplicated, capped at 20, and rate-limited to
602
+ one every five seconds. Reasoning, secret-like material, raw logs/tool output,
603
+ control characters, and late or unknown request IDs are rejected.
604
+ 4. Fleet waits for accepted intermediate sends, then emits exactly one final ACP
605
+ response (or a sanitized terminal outcome). Successful turns send regular files
606
+ from the request outbox afterward, correlated to the same source wire.
607
+
608
+ Fleet chooses the stored authenticated sender for every update; neither the CLI
609
+ caller nor model supplies a recipient. Update audit logs contain only the hashed
610
+ request ID prefix, phase, character count, sequence, and delivery result. Bodies
611
+ remain memory-only and never enter the wire-ID state, authorization overlay, or
612
+ logs. A crash therefore replays the deferred owner request instead of persisting
613
+ an unfinished update body. `/interrupt` remains responsive and makes later
614
+ updates for the cancelled request fail closed.
615
+
616
+ Background work must not keep an ACP turn open. During the active authenticated
617
+ request, `task open` accepts only its injected request ID and emits no owner
618
+ message. Fleet creates a random opaque task ID and durably stores only the exact
619
+ originating CID/wire route, expiry, counters, and content hashes. The agent may
620
+ then tell the owner that a specialist is working, finalize, and idle. After a
621
+ later fleet-mail wake it verifies the result and uses `task report` with
622
+ `progress`, `done`, or `blocked`; fleet sends a new proactive follow-up from the
623
+ already-bound channel identity, correlated to the original wire. The CLI has no
624
+ recipient option and never broadcasts. `done` and `blocked` close the task only
625
+ after a successful send.
626
+
627
+ Tasks expire after seven days and are capped at 32 open tasks per role and eight
628
+ per originating owner. Each allows at most 20 reports, one every five seconds.
629
+ Reports reuse the one-sentence 280-character/1024-byte safety checks and body
630
+ deduplication. Authorization is rechecked at send time; revocation deletes that
631
+ owner's pending routes. The mode-0600 task file is bounded and contains no
632
+ message/report bodies. Corruption fails closed. A durable `sending` marker is
633
+ written before transport: if delivery fails, the response is lost, or the
634
+ supervisor crashes mid-send, the task becomes `uncertain` and refuses automatic
635
+ retry or later reordering. This at-most-once retry policy avoids double delivery
636
+ when ours-mcp cannot prove whether a send crossed the boundary; an operator must
637
+ resolve an uncertain task out of band.
638
+
639
+ Coordinator workflow: spawn the specialist, run `task open` before the active
640
+ owner turn ends, tell the owner work is continuing and finalize, then idle. On
641
+ the fleet-monitor wake, inspect and verify the specialist's result before using
642
+ `task report ... --phase done|blocked`; do not keep the ACP turn alive or poll.
643
+
644
+ For mobile onboarding, create or accept the contact first, wait until `contact
645
+ list` reports it established, then authorize that exact CID. Authorization and
646
+ revocation take effect immediately and the bounded mode-0600 CID overlay survives
647
+ role/supervisor restarts; update bodies do not. After restart, the supervisor is
648
+ still the sole channel binder and deferred unfinished requests retain the normal
649
+ at-least-once replay contract.
650
+
495
651
  The two paths are deliberately simultaneous and have different authority:
496
652
 
497
653
  - Mail to the role's normal `identity` remains peer mail. The content-blind
@@ -512,6 +668,34 @@ across a crash (the bridge requeues fetched input before starting a turn); true
512
668
  exactly-once processing would require a leased claim/idempotency primitive in
513
669
  ours-mcp.
514
670
 
671
+ Inbound owner attachments use the same authenticated-CID and exact-wire routing
672
+ boundary. Fleet first calls the metadata-only `list_incoming_files`, groups a
673
+ file-only wake or a same-sender reply-linked text caption, and checks the enabled,
674
+ count, declared MIME, per-file size, and total-size policy before retrieving any
675
+ bytes. It then calls selective `get_files` only for the admitted wire IDs. An
676
+ unauthorized sender is ignored without retrieval or reply. A rejected authorized
677
+ request receives a bounded reason correlated to its file wire.
678
+
679
+ Retrieved files must be regular, non-symlink paths whose byte count and SHA-256
680
+ match ours-mcp metadata. Fleet additionally checks content signatures against the
681
+ declared MIME, sanitizes traversal/control characters from names, and copies each
682
+ file into a random request-scoped directory at mode 0700 with files at mode 0600.
683
+ The `[fleet-owner]` turn receives only bounded metadata, the private local paths,
684
+ and an explicit daemon transcription result for voice messages. Successful
685
+ transcripts are included; `failed` and `unavailable` states are stated plainly so
686
+ the agent must use the audio path rather than inventing text.
687
+
688
+ Request files are removed after final delivery and stale directories are removed
689
+ after `retention_ms`. A bounded mode-0600 recovery journal stores only owner CID
690
+ and wire routing metadata—never filenames, paths, captions, transcripts, or file
691
+ bytes. If ours-mcp already marked a selected file processed when fleet restarts,
692
+ fleet resumes only that journaled wire with `save_file`; recovered voice is
693
+ explicitly marked transcript-unavailable. Corrupt recovery state disables
694
+ attachment admission. The host must run an ours-mcp version whose
695
+ `list_incoming_files`, selective `get_files`, and `save_file` schemas support
696
+ these guarantees; `ours-mcp voice-status --json` reports whether transcription
697
+ is currently configured.
698
+
515
699
  Owner channels currently require `session: acp`. Fleet needs structured,
516
700
  turn-correlated assistant output for automatic replies; scraping a tmux pane
517
701
  cannot reliably distinguish the final answer from thoughts, tool output, or
package/dist/briefing.js CHANGED
@@ -70,6 +70,16 @@ export function generateBriefing(role, v, opts) {
70
70
  L.push(` appropriate, and reply explicitly with **${v.sendTool}** to that peer.`);
71
71
  L.push('System acceptance, queue, progress, interruption, failure, and final-delivery notices');
72
72
  L.push('on the owner channel are fleet-generated; do not imitate or resend them.');
73
+ L.push('', '### Background specialist follow-ups');
74
+ L.push('If an authenticated owner request starts work that will finish after your current turn:');
75
+ L.push(`1. During that active turn run \`ours-fleet owner-channel task open ${role.name} <active-request-id>\`.`);
76
+ L.push('2. Keep the returned opaque task ID, tell the owner the specialist is working, and');
77
+ L.push(' finalize normally. Do not hold the ACP turn open and do not poll.');
78
+ L.push('3. When fleet mail later wakes you, verify the specialist result, then run');
79
+ L.push(` \`ours-fleet owner-channel task report ${role.name} <task-id> `
80
+ + '--phase <progress|done|blocked> --message-stdin`.');
81
+ L.push('Fleet sends the bounded follow-up only to the exact authenticated originating owner;');
82
+ L.push('`done` and `blocked` close the task. Never supply or infer a recipient yourself.');
73
83
  }
74
84
  if (role.coordinator) {
75
85
  L.push(`7. ANNOUNCE yourself: call **${v.sendTool}** to contact "${role.coordinator}" with text:`);