@ours.network/fleet 0.16.0 → 0.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -1
- package/dist/application/role-creation-service.d.ts +2 -2
- package/dist/config.d.ts +4 -2
- package/dist/config.js +3 -2
- package/dist/docs.d.ts +1 -1
- package/dist/docs.js +9 -4
- package/dist/fleet-proxy.js +2 -2
- package/dist/harness/codex.js +43 -6
- package/dist/harness/types.d.ts +2 -0
- package/dist/monitor.d.ts +4 -3
- package/dist/monitor.js +11 -2
- package/dist/owner-channel/channel.js +4 -1
- package/dist/permissions.d.ts +2 -0
- package/dist/permissions.js +5 -0
- package/dist/runner.js +20 -5
- package/dist/session/acp.d.ts +23 -0
- package/dist/session/acp.js +298 -17
- package/dist/session/arbiter.d.ts +5 -0
- package/dist/session/arbiter.js +8 -0
- package/dist/session/conversation-normalizer.d.ts +6 -0
- package/dist/session/conversation-normalizer.js +4 -3
- package/dist/session/conversation-types.d.ts +9 -2
- package/dist/session/types.d.ts +13 -1
- package/dist/web-app/assets/{TerminalView-Mxxypj9w.js → TerminalView-B3rnVWbo.js} +1 -1
- package/dist/web-app/assets/{index-CsHEL0f6.js → index-CliHATFt.js} +4 -4
- package/dist/web-app/index.html +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -338,7 +338,7 @@ roles:
|
|
|
338
338
|
coordinator: FleetCoordinator # announce target on boot
|
|
339
339
|
monitor:
|
|
340
340
|
mode: fleet # fleet = ours-fleet supervisor; native = harness monitor
|
|
341
|
-
interrupt: false # true cancels
|
|
341
|
+
interrupt: false # false queues; true cancels; after_tool steers at an ACP tool boundary
|
|
342
342
|
wake_sources: # which daemon events wake the console (default:
|
|
343
343
|
- message_received # message_received, file_received,
|
|
344
344
|
- file_received # local_contact_request, pending_message)
|
|
@@ -579,6 +579,13 @@ Set `monitor.interrupt: true` on roles where every configured wake should cancel
|
|
|
579
579
|
the active turn before the notification is delivered. This is intentionally
|
|
580
580
|
content-blind: the supervisor cannot inspect encrypted message bodies, so all
|
|
581
581
|
events selected by `wake_sources` receive the same interrupt policy.
|
|
582
|
+
Set `monitor.interrupt: after_tool` when a wake must preserve an in-flight ACP
|
|
583
|
+
tool result or pending permission. Fleet waits for terminal ACP tool/update
|
|
584
|
+
evidence, then steers the wake without calling `session.cancel`. If the tool is
|
|
585
|
+
still active after 120 seconds, or the adapter cannot expose authenticated tool
|
|
586
|
+
boundaries/steering, fleet visibly degrades to non-cancelling steering or queued
|
|
587
|
+
delivery. Tmux never receives `C-c` for `after_tool`. Explicit human and control
|
|
588
|
+
interrupts remain immediate.
|
|
582
589
|
The default is `false`: a role that must begin a post-readiness mission
|
|
583
590
|
immediately, including second-and-later mail received while it is working, must
|
|
584
591
|
set `monitor.mode: fleet` and `monitor.interrupt: true` explicitly. Readiness and
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type CommonPermissions, type MonitorConfig, type NotifyEventType } from '../config.js';
|
|
1
|
+
import { type CommonPermissions, type MonitorConfig, type MonitorInterrupt, type NotifyEventType } from '../config.js';
|
|
2
2
|
import { type IdentityProvisioner } from '../creation.js';
|
|
3
3
|
import { type SupervisorLauncher } from '../spawn.js';
|
|
4
4
|
import type { OpsDeps } from '../ops.js';
|
|
@@ -28,7 +28,7 @@ export type WebCreationMonitor = {
|
|
|
28
28
|
mode: 'native';
|
|
29
29
|
} | {
|
|
30
30
|
mode: 'fleet';
|
|
31
|
-
interrupt:
|
|
31
|
+
interrupt: MonitorInterrupt;
|
|
32
32
|
wake_sources: NotifyEventType[];
|
|
33
33
|
batch_ms: number;
|
|
34
34
|
inject: 'notification';
|
package/dist/config.d.ts
CHANGED
|
@@ -29,6 +29,8 @@ export type FleetPermissionMode = 'ask' | 'auto' | 'allow';
|
|
|
29
29
|
export type ApprovalMode = FleetPermissionMode | 'deny';
|
|
30
30
|
export type FilesystemMode = 'read-only' | 'workspace' | 'unrestricted';
|
|
31
31
|
export type UnattendedMode = 'deny' | 'wait';
|
|
32
|
+
/** Monitor wake policy: preserve legacy booleans and add one explicit safe boundary. */
|
|
33
|
+
export type MonitorInterrupt = boolean | 'after_tool';
|
|
32
34
|
export interface CommonPermissions {
|
|
33
35
|
approval: ApprovalMode;
|
|
34
36
|
filesystem: FilesystemMode;
|
|
@@ -52,8 +54,8 @@ export interface MonitorConfig {
|
|
|
52
54
|
wake_sources: string[];
|
|
53
55
|
batch_ms: number;
|
|
54
56
|
inject: InjectMode;
|
|
55
|
-
/**
|
|
56
|
-
interrupt:
|
|
57
|
+
/** Immediate cancel, ordinary non-cancelling delivery, or ACP tool-boundary steering. */
|
|
58
|
+
interrupt: MonitorInterrupt;
|
|
57
59
|
/**
|
|
58
60
|
* Consecutive delivered wakes that must end in an `API Error:`-terminated turn
|
|
59
61
|
* (with no completed turn in between) before `.monitor-status` degrades to
|
package/dist/config.js
CHANGED
|
@@ -51,8 +51,9 @@ export function validateMonitorConfig(raw) {
|
|
|
51
51
|
problems.push('monitor.batch_ms: must be a non-negative number');
|
|
52
52
|
if (m.inject !== undefined && !INJECT_MODES.includes(m.inject))
|
|
53
53
|
problems.push(`monitor.inject: invalid value '${m.inject}'; allowed: ${INJECT_MODES.join(', ')}`);
|
|
54
|
-
if (m.interrupt !== undefined
|
|
55
|
-
|
|
54
|
+
if (m.interrupt !== undefined
|
|
55
|
+
&& typeof m.interrupt !== 'boolean' && m.interrupt !== 'after_tool')
|
|
56
|
+
problems.push("monitor.interrupt: must be true, false, or 'after_tool'");
|
|
56
57
|
if (m.turn_fail_threshold !== undefined
|
|
57
58
|
&& (typeof m.turn_fail_threshold !== 'number' || !Number.isInteger(m.turn_fail_threshold)
|
|
58
59
|
|| m.turn_fail_threshold < 1))
|
package/dist/docs.d.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Keep this concise enough to place directly in an agent context. Unlike
|
|
5
5
|
* Commander's per-command help, this describes how the pieces compose.
|
|
6
6
|
*/
|
|
7
|
-
export declare const AI_DOCS = "# ours-fleet reference\n\nours-fleet runs persistent or temporary, identity-bound AI roles. A role selects\na harness independently from its session backend:\n\n- harness: `claude-code` or `codex`\n- session: `tmux` (default) or `acp`\n- lifetime: permanent (supervised, restartable) or `spawn --temp`\n\n## Discover and validate\n\n```sh\nours-fleet docs # this complete reference (`man` is an alias)\nours-fleet help <command> # exact flags for one command\nours-fleet config [-c FILE] # validate and print the merged plan; no changes\nours-fleet doctor [-c FILE] [--harness codex|claude-code]\n```\n\nDefault configuration is `~/fleet.yaml` plus sorted `~/fleet.d/*.yaml` role\ndrop-ins. An explicit `-c FILE` replaces `~/fleet.yaml`; fleet.d still adds\nroles. Validate with `config` and `doctor` before starting or restarting.\n\nThe CLI never writes the base file: `spawn` writes `~/fleet.d/Name.yaml`. The\nweb console does write it, as a whole document \u2014 its setup wizard and\nconfiguration editor may create, change or remove any top-level block, including\n`vars:`, `defaults:`, `roles:`, `watchdogs:` and `loops:`. Only the base\nfile may hold `defaults:`, `watchdogs:` and `loops:`; a fleet.d drop-in may\ndeclare `roles:` and nothing else. Unrecognised top-level keys are round-tripped\nuntouched. Console edits are applied as surgical splices against the file's exact\nbytes, so an unchanged save is byte-identical and lines outside the edit keep their\ncomments and spacing. One exception: changing the length of a block sequence\n(`watch:`, `oversee:`, `roles:`, `wake_sources:`) may replace that collection\nwholesale and drop inline comments written on its items; lines outside that\ncollection remain byte-preserved. Each save is revision-guarded, reviewed as a diff\nof the real file before anything is written, validated by the real loader, and\nbacked up next to the file first.\n\n## Lifecycle and console commands\n\n```sh\nours-fleet init\nours-fleet up|down [Name...]\nours-fleet restart [Name...] # preserve/resume harness context\nours-fleet force-restart [Name...] # fresh context; briefing is reloaded\nours-fleet ls\nours-fleet status|peek|attach|logs Name\nours-fleet logs -f Name\nours-fleet send Name \"prompt\"\nours-fleet send Name --key Enter # tmux only\nours-fleet rm Name\nours-fleet watchdog-report <name> [run-id] [--list] [--json]\nours-fleet watchdog-run <name>\n```\n\n`peek`, `attach`, and text `send` work with tmux and ACP. ACP attachment\nalso accepts `/permit <permission-id> <option-id>`, `/interrupt`, and\n`/detach`. Raw `--key` input is tmux-only.\n\n## Local web console\n\nThe npm package includes the web console; installed users do not clone the repo\nor run `npm run build`:\n\n```sh\nnpm i -g @ours.network/fleet\nours-fleet init\nours-fleet doctor\nours-fleet web # install/update service, start, pair browser\n```\n\nThe normal command uses stable `http://127.0.0.1:49271/`, installs an\nowner-level systemd user service (Linux) or LaunchAgent (macOS), and opens a\nfive-minute one-use pairing link in the local browser. After pairing, bookmark\nthe plain URL or install the PWA. To pair a new, signed-out, or revoked browser,\nrun `ours-fleet web open`.\n\n```sh\nours-fleet web status\nours-fleet web start|stop|restart\nours-fleet web open\nours-fleet web revoke-all # revoke every browser and active session\nours-fleet web uninstall\nours-fleet web serve --port 0 --no-open # isolated foreground/testing mode\n```\n\nThe console is IPv4-loopback-only by default. Both `localhost` and\n`127.0.0.1` are accepted locally. For an nginx/TLS reverse proxy, keep the\ndefault bind and declare the exact browser origin:\n\n`ours-fleet web install --public-origin https://fleet.example.com --password-file /secure/fleet-password`\n\nFleet reads the password file during setup and persists only a salted scrypt\nverifier. New browsers authenticate and retain rotating HttpOnly/SameSite\ntrusted-device credentials. If nginx already authenticates, the operator may\ndeliberately select `--no-password`; the CLI and browser warn that anyone\nreaching the origin can control the fleet. First setup requires an explicit\nchoice: `--password-file` or `--pairing` for protected access, or\n`--no-password` for intentional unprotected access.\n\nUse `--bind ADDRESS` only for an intentional direct listen. A non-loopback\nbind is rejected unless `--public-origin` is also present. Host/Origin checks\nuse the declaration and do not trust forwarded headers. Configure nginx to\nproxy HTTP and WebSocket upgrades to `127.0.0.1:49271` and terminate TLS;\nfleet accepts nginx's loopback upstream Host, so no Host rewrite is required.\nBrowser credentials add Secure for HTTPS, and `revoke-all` invalidates all\ntrusted devices. Role creation offers harness-scoped known-model choices\nwhile still accepting a typed model ID; blank explicitly uses the selected\nharness's own default.\n\n## Spawn\n\n```sh\nours-fleet spawn [--temp] [Name | --role Name] \\\n --harness codex|claude-code --session tmux|acp \\\n --mission \"one line\" --cwd /absolute/path --identity Identity \\\n --coordinator Coordinator --model MODEL \\\n --approval ask|auto|allow \\\n --filesystem read-only|workspace|unrestricted \\\n --unattended deny|wait \\\n --bio-file /path/bio.md --persona-file /path/persona.md\n```\n\nPermanent spawn writes `~/fleet.d/Name.yaml` and starts a supervised role.\n`--temp` writes ephemeral state, starts a detached supervisor, and removes the\nrole after exit/reboot. Both lifetimes support `--session acp`.\n\nTemporary-role identity bootstrap is capability-based. The generated briefing\nfirst tries to bind the exact assigned identity and preserves it when it already\nexists. If missing, it uses ours MCP `create_temporary_identity` when that tool\nis exposed, tying a newly-created identity to the connector session lifecycle;\nolder servers fall back to `create_identity`. Collisions and creation errors\nstop safely without force-adopting or deleting identity state. Permanent roles\nretain normal `create_identity` behavior.\n\nInside a managed ACP role, the same CLI automatically routes a real `spawn`\nthrough that role's authenticated supervisor control socket. `--role Name` is\naccepted as an alternative to the positional name, so a minimal delegated call\nis `ours-fleet spawn --role DeveloperX --temp`. The supervisor records the\ncalling role, performs creation, and only after success sends a structured\nspawn notice through the caller's owner channel when one is configured.\n\nOmitted harness, session, working directory, coordinator, neutral permissions,\nfleet monitor policy, and (when the harness is unchanged) model inherit from the\ncalling role. Explicit options always win. Selecting a different harness without\n`--model` leaves model selection to that harness/fleet defaults rather than\ncopying an incompatible caller model. This automatic proxy is a convenience and\nattribution mechanism, not an isolation boundary: an unrestricted role can still\ninvoke another binary path directly. Tmux roles and host/operator shells keep the\nordinary direct CLI behavior.\n\nCodex-specific spawn flags: `--sandbox`, `--permission-mode`, `--launcher`,\n`--profile`, `--search`, repeatable `--codex-config key=value`, repeatable\n`--add-dir`, and legacy `--monitor` (consent for the native Codex monitor,\nnot the `monitor.mode` wake-owner selector). Run `ours-fleet help spawn` for\nexact values.\n\n## fleet.yaml\n\n```yaml\nvars:\n work_root: /home/me/work\nstart_stagger_ms: 0\ndefaults:\n harness: codex\n session: acp\n model: gpt-model-id\n permissions:\n approval: ask\n filesystem: workspace\n unattended: deny\n monitor:\n mode: fleet # fleet (default) | native\nroles:\n Coordinator:\n harness: codex\n session: acp\n identity: Coordinator\n cwd: ${work_root}/project\n mission: Coordinate work and delegate implementation.\n model: gpt-model-id\n permissions:\n approval: ask\n filesystem: workspace\n unattended: deny\n session_options: # advanced overrides; normally omit\n # acp:\n # command: [/custom/codex-acp, --flag]\n tmux:\n boot_grace_ms: 10000\n monitor:\n mode: fleet # fleet supervisor | native harness monitor\n interrupt: false # true cancels active work before every configured wake\n wake_sources: [message_received, file_received, local_contact_request, pending_message]\n batch_ms: 2000\n inject: notification\n turn_fail_threshold: 3\n harness_options:\n launcher: auto\n sandbox: workspace-write\n approval: on-request\n search: false\n profile: fleet\n add_dirs: [/data/shared]\n config:\n model_reasoning_effort: high\n bio: Public role card and when peers should engage it.\n persona: Local operating contract, boundaries, and escalation policy.\n briefing_file: /absolute/custom-briefing.md\n coordinator: AnotherCoordinator\n env:\n KEY: value\n oversee:\n - { role: Worker, interval: 5m }\nwatchdogs:\n nightwatch: # [A-Za-z0-9_-], must not collide with a role name\n coordinator: FleetCoordinator # required \u2014 where alerts go\n # everything below is optional\n enabled: true # default true; false = configured but never scheduled\n interval: 10m # default 10m; 30s | 10m | 2h, minimum 1m\n watch: [Alice, CodexReviewer] # explicit lists are exact; omit for configured + live temp roles\n harness: claude-code # default: defaults.harness\n model: claude-fable-5 # default: same resolution rule roles use (resolveRoleModel)\n session: acp # default: defaults.session\n identity: Watchdog-nightwatch # default: Watchdog-<name>\n timeout: 5m # default 5m; a run past this is killed and recorded as error\n keep_reports: 50 # default 50 reports retained per watchdog\n alert_cooldown: 60m # default 60m before the same finding alerts again\n prompt_file: /abs/extra.md # optional extra focus, APPENDED to the fixed contract\n isolation: # optional; omitted means no OS sandbox, like an ordinary role\n backend: bubblewrap # when present, the ordinary role isolation schema applies\n network: broker\n fs: { read: [/opt/watch-data] }\n```\n\nA watchdog observes and reports; it never restarts, stops, spawns, or removes a\nrole, answers a pending permission, edits a workspace, or approves anything on\nthe owner's behalf. `watchdogs:` may appear only in the base config\n(`~/fleet.yaml` or `-c FILE`), not in `~/fleet.d/*.yaml` drop-ins.\nWatchdogs are not isolated by default. An explicit watchdog `isolation:` block\nuses the same policy schema as a role and is applied unchanged; declare every\nextra filesystem access required by a custom prompt there.\nWhen `watch:` is omitted, each run watches the configured roles plus temporary\nfleet roles that are live when the run starts. An explicit `watch:` list is\nnever augmented.\n\nRole values override defaults. `${name}` substitutes entries from `vars`.\nOther role fields include `max_tokens`, `autocompact_pct`, and `isolation`.\nUse README.md for the complete isolation policy and resource-cap schema.\n\n## Permissions\n\nPrefer the harness-neutral `permissions` block:\n\n- `approval: ask|auto|allow`: portable permission policy. `deny` remains a\n deprecated, fail-closed compatibility alias for existing fleet files.\n- `filesystem: read-only|workspace|unrestricted`: filesystem intent\n- `unattended: deny|wait`: what ACP does when no console can answer a request\n\nThe backend translates this common intent. Harness-native settings in\n`harness_options` take precedence where supplied. Do not choose\n`allow`/`unrestricted`, Codex `never`/`danger-full-access`, or Claude\n`bypassPermissions` without explicit authorization.\n\n### Creation-time isolation\n\n`ours-fleet spawn --isolation-file <path>` supplies a role's sandbox policy at\ncreation, so the FIRST launch is already confined \u2014 a role that only gains\n`isolation:` on a later `up` ran unsandboxed until then.\n\nThe file holds exactly the `isolation:` mapping documented above and nothing\nelse \u2014 the same schema, validated by the same code, so a policy written here\ncannot mean something different from the identical block in fleet.yaml:\n\n```yaml\nnetwork: deny\nfs:\n read: [/opt/reference]\nresources:\n mem: 2G\n```\n\nInvalid files are rejected before anything is created: no config, no state\ndirectory, no identity reservation. Works for both permanent and `--temp` roles.\n\n### Never-prompt failure\n\nThe failure this section exists to prevent leaves no error message anywhere.\n\nAn unattended role has no console. When the harness needs a permission decision\nthere is nobody to ask, so the request is refused INSIDE the harness \u2014 no\nprompt, no error, no log line. The agent simply does less than its briefing told\nit to, reports success, and nothing distinguishes that from having done the\nwork. Two settings produce it:\n\n1. a permission mode that suppresses the prompt without granting the action\n (Claude `dontAsk`, which is why neutral `allow` maps to\n `bypassPermissions` instead); and\n2. `unattended: deny`, which refuses every request that reaches it.\n\n**Automatic decisions are now recorded.** Every permission request decided\nwithout a human emits a completed event into\n`~/.ours-fleet/agents/<Name>/.session-events.jsonl` carrying the decision,\nwhether policy or a person made it, the policy that produced it\n(`permissions.unattended=deny` vs `permissions.approval=deny`/`=allow`),\nthe reason, and the option selected. `ours-fleet peek` and `attach` render\nthem. Automatic denial asks for a one-shot rejection, never a standing one, so a\nsingle unattended refusal cannot disable a tool for the rest of the session.\n\nA role that can auto-deny logs one line at startup saying so.\n\nTo detect an under-permissioned role BEFORE it runs, use the capability floor\nbelow: `ours-fleet doctor` fails such a role rather than letting it discover\nthe problem silently at work.\n\n### The unattended capability floor\n\nAn unattended role has no console, so a permission request cannot be answered \u2014\nit is refused, silently, inside the harness. The agent then does less than it\nwas told to and reports no error. To make that visible before launch,\n`ours-fleet config` and `ours-fleet doctor` resolve each role's neutral\npermissions through its harness and check the result against a fixed floor:\n\n- `read-state` \u2014 read its briefing, ROUTINES.md, and WORKLOG.md\n- `write-state` \u2014 append its WORKLOG and its own state files\n- `messaging` \u2014 bind its identity, send and receive ours mail\n- `monitor` \u2014 arm and observe its mail monitor\n- `workspace-edit` \u2014 edit and test files in its working directory\n- `status-commands` \u2014 run the inspection commands its briefing prescribes\n\n`doctor` reports this per role as `unattended floor: <Role>`. A role with\n`unattended: deny` that cannot meet the floor FAILS doctor, because it will\ndeny those requests with nobody to see it; with `unattended: wait` it warns,\nbecause a human can still attach and answer.\n\nSecurity meaning: `ask` maps to Codex `untrusted` and Claude `default`;\n`auto` maps to Codex `on-request` and Claude `acceptEdits`; and\n`approval: allow` maps to Codex `never` and Claude `bypassPermissions`,\nwhich genuinely permits the actions the role was authorized to take \u2014\n`dontAsk` only suppresses the prompt while still refusing the action. Nothing\nother than an explicit `allow` becomes non-interactive. Legacy `deny` keeps\nits conservative Codex `on-request` / Claude `plan` translation. `allow` is therefore a real grant and\nrequires explicit authorization; per-role `isolation:` remains the outer\nboundary that a permission mode cannot cross.\n\nACP carries agent-advertised session mode IDs and `session/set_mode`, but those\nIDs are agent-specific and ACP defines no portable permission-policy capability.\nFleet therefore uses the ACP primitive where an adapter exposes a matching mode\nand otherwise performs the harness translation above. The live session reports\nboth its effective normalized mode and exact harness-native mode.\n\nSee also: `spawn --approval/--filesystem/--unattended` set this intent at\ncreation, and `ours-fleet config` prints each role's neutral settings, their\nnative translation, and any warning \u2014 the same text `doctor` reports.\n\nClaude `harness_options`: `permission_mode` (default, acceptEdits, plan,\ndontAsk, bypassPermissions), `plugins`, `mem_palace`, and\n`mem_palace_midsession_autosave`.\n\nCodex `harness_options`: `launcher` (auto, ours-codex, codex), `sandbox`\n(read-only, workspace-write, danger-full-access), `approval` or\n`permission_mode` (untrusted, on-request, never), `profile`, `search`,\n`config`, `add_dirs`, and `monitor`.\n\n## ACP adapters\n\nThe maintained `@agentclientprotocol/codex-acp` and\n`@agentclientprotocol/claude-agent-acp` runtimes are bundled automatically as\noptional ours-fleet dependencies. The supervisor resolves their executable\nentrypoints internally, so default ACP roles do not depend on global PATH.\nThe maintained Claude adapter requires Node 22; tmux and Codex ACP continue to\nwork on the ours-fleet core minimum of Node 20.\n\nOverride an adapter only when necessary with `session_options.acp.command`\n(string or argv list). If optional dependencies were deliberately omitted,\nours-fleet falls back to a compatible globally installed `codex-acp` or\n`claude-agent-acp`. `ours-fleet doctor -c FILE` verifies the resolved adapter.\n\n## Reliable mail wake\n\n`monitor.mode` selects exactly one wake owner:\n\n- `fleet` (default): the ours-fleet supervisor consumes body-free daemon\n events and advances its durable cursor only after delivery is accepted. ACP\n uses live steering when supported and falls back to structured\n `session/prompt`; tmux uses verified console injection.\n- `native`: ours-fleet starts no supervisor monitor; the generated briefing\n instructs Claude Code or Codex to arm its harness-native wake mechanism.\n\nSet `monitor.interrupt: true` in fleet mode to cancel active work before every\nconfigured wake. The policy is content-blind because the supervisor cannot\ninspect encrypted message bodies. Message bodies are released only when the\nrole calls the ours `get_messages` tool.\n\nThe default is `false`. For a temporary role whose mission intentionally arrives\nafter its readiness announcement, set `mode: fleet` and `interrupt: true`\nexplicitly. The readiness announcement does not change the transport: the\nmission remains ordinary ours mail, fleet injects only the body-free wake, and\nthe role calls `get_messages` before acting. Every later configured wake uses\nthe same interruption policy.\n\nLegacy `monitor.enabled: true|false` remains accepted as an alias for\n`mode: fleet|native`; use `mode` in new configuration. Codex's separate\n`harness_options.monitor: true` is native-monitor consent, not monitor-owner\nselection.\nInspect `ours-fleet status Name`, `peek Name`, role logs, and\n`~/.ours-fleet/agents/Name/.monitor-status` when diagnosing delivery.\n\n## Trusted owner channel\n\nAn ACP role may declare a separate, existing ours identity which fleet \u2014 never\nthe agent \u2014 binds:\n\n```yaml\nowner_channel:\n identity: Coordinator Owner Channel\n owners: [authenticated-owner-contact-cid]\n agent: authenticated-managed-agent-cid\n interrupt: false\n progress_interval_ms: 30000\n comments: true\n attachments:\n enabled: true\n max_files_per_request: 4\n max_file_bytes: 10485760\n max_request_bytes: 20971520\n retention_ms: 86400000\n allowed_mime: [application/pdf, text/plain, image/png, audio/ogg]\n```\n\nThis does not replace the role identity. Normal identity mail remains untrusted\npeer input: the agent reads it through `get_messages` and replies through\n`send_message`. Mail arriving on the dedicated channel from a CID in `owners`\nis injected as a direct `[fleet-owner]` prompt. Mail from the exact `agent`\nCID is forwarded as a new message to the latest authenticated owner conversation;\nits files may also be relayed through this channel. A reply reference selects the\nowner of that authenticated source wire instead of the latest conversation.\nEvery other CID is rejected and warned about without reflecting its body. Fleet sends\naccepted/queued/progress/interrupted/failure notices and routes the ACP turn's\nfinal assistant text back to the authenticated sender with its source wire ID.\nFor file replies, fleet injects a request-specific outbox path into the owner\nprompt. The agent copies completed artifacts there; fleet sends every regular\nfile from the channel identity with the same source wire ID and removes the\ntemporary outbox only after successful delivery. For proactive or in-turn agent\nattachments, the agent calls ours `send_file` to the channel identity and may\npair it with a reply-linked caption; fleet, not the agent, chooses the owner.\nOwner messages whose trimmed text starts with `/` are deterministic\nsupervisor commands and never enter the model: `/help` (alias `/commands`),\n`/status`, `/comments [status|on|off]`, `/interrupt`, `/clear`,\n`/compact`, `/model <model-id>`, `/restart`, `/force-restart`, `/ls`,\n`/peek`, `/worklog`, and\n`/version`. Unknown or malformed commands answer with the help text instead of\nbeing forwarded; plain messages reach the agent unchanged. `/clear`,\n`/compact`, and `/model` are forwarded only when the role's bundled ACP\nadapter executes them locally (claude-code: all three; codex: `/compact`\nonly) and are otherwise refused with a notice, so slash text never reaches the\nmodel as a prompt.\n\nWhile a request runs, the agent's live ACP commentary is relayed as messages\nprefixed with the single stable label `\uD83D\uDFE1 Live update:`, so an owner can see\nexactly which messages the setting controls. `owner_channel.comments`\n(default `true`, so existing channels keep their current behavior) is the\nRESTART BASELINE; `/comments on|off` changes only the running session and is\ndeliberately not persisted, so a restart always returns to the checked-in\nconfiguration. `/comments status` reports the live value, the baseline, and\nwhether the backend emits live comments at all. Suppressing live comments never\nsuppresses receipts, progress notices, or the final answer.\n\nOwner documents, images, and voice messages use the same authenticated sender\nand source-wire boundary. Fleet inspects body-free metadata first and rejects\ndisabled, over-count, over-size, or disallowed-MIME requests before selective\nretrieval. Unauthorized CIDs are never retrieved or answered. Reply-linked text\nand files from the same sender become one ordered request; a file-only wake also\nstarts a turn. Retrieved bytes must match their structured size and SHA-256,\ntheir content signature must match the declared MIME, and symlinks or non-regular\npaths fail closed. Sanitized copies live only in a mode-0700 request directory as\nmode-0600 files and are removed after completion or bounded stale retention.\n\nVoice prompts include a bounded transcript only when ours-mcp reports success.\nFailure or unavailability is explicit and preserves the private audio path as the\nfallback. Run `ours-mcp voice-status --json` to inspect the host configuration.\nA mode-0600 crash journal contains only authenticated CID and wire routing data;\nit never stores captions, filenames, paths, transcript text, or bytes. Journaled\npost-retrieval files resume selectively through `save_file`. A deferred agent\ncaption is replayed with its processed files before the group is admitted. Fleet\nresolves one authenticated owner route before retrieving bytes, admits every file\nbefore emitting the caption or any file, and sends every part to that same route.\nUnknown correlated routes remain queued without retrieval and receive one bounded\ncorrelated notice. Admission rejection consumes the whole group with one NACK;\nonce emission starts, a transport error becomes terminal uncertain delivery and\nthe group is never blind-retried. Bounded v2 source-wire routing state is migrated\nfrom v1 on read. Corrupt state disables attachment admission rather than weakening\nprovenance checks.\n\nThe channel identity must be unique and must not be a role identity. The bridge\npersists bounded wire IDs only, never message/reply plaintext, and requeues input\nbefore starting its turn for at-least-once crash recovery. It currently requires\n`session: acp`: tmux has no structured, turn-correlated final answer, and pane\nscraping cannot provide the same reliable reply guarantee.\n\n### Live contact and owner administration\n\nThe supervisor which is already running the ACP role remains the sole binder of\n`owner_channel.identity`. The CLI reaches that exact live `OwnerChannel`\nthrough the role's token-authenticated, mode-0600 Unix control socket for contact\ninspection and setup; it never starts another ours client and never force-binds:\n\nRapid supervised restart is serialized by a role-scoped single-binder lease.\nThe predecessor closes its authenticated control socket and MCP proxy before\nreleasing ownership. The replacement waits at most five seconds and retries the\ndaemon bind only when PID/start-marker metadata proves the holder was the same\nrole and owner-channel identity. Foreign, live, corrupt, or otherwise\nunverifiable ownership remains fail-closed; fleet never uses `force=true`.\n\nIf that matching predecessor misses the bound, its still-authenticated control\nroute may send one fixed, digest-deduplicated recovery notice through the latest\nauthenticated owner conversation (or the sole configured owner). Notice\nplaintext is never persisted. With no safe deterministic route fleet guesses no\nrecipient and leaves the actionable failure in the web console and role logs.\nThe remote recovery action is `/restart`; inspect repeated failures with\n`ours-fleet logs <Role>` or the web console.\n\n```sh\nours-fleet owner-channel contact list <Role>\nours-fleet owner-channel contact invite <Role> [--name <label>]\nours-fleet owner-channel contact add <Role> (--invite-file <path> | --invite-stdin) [--name <label>]\nours-fleet owner-channel owner list <Role>\nours-fleet owner-channel owner authorize <Role> <exact-64-hex-contact-cid>\nours-fleet owner-channel owner revoke <Role> <exact-64-hex-contact-cid>\n```\n\nContact establishment and owner authorization are separate security steps.\n`contact add` never authorizes: invite redemption is pending until the peer\nverifies it. Once `contact list` reports the established contact, authorize\nits exact immutable CID explicitly. Invite creation emits invite material only\non stdout; acceptance reads it from a file or stdin, not argv.\n\nConfigured `owners` remain the baseline. On legacy channels without `agent`,\nlive authorizations/revocations are an immediately effective, restart-persistent\noverlay. Managed-agent CID gating makes fleet configuration authoritative and\ndisables live owner mutation and direct control-socket sends. `owner list` labels\nbaseline versus dynamic entries and effective status. The atomic mode-0600 file\ncontains bounded CIDs and audit actions only. Corruption disables all effective\nowners and refuses mutation rather than resurrecting authority; revoking the\nlast effective owner is always refused.\n\nA missing/stopped role, tmux session, role without `owner_channel`, unavailable\nMCP client, or a role entering shutdown returns an actionable error with no\nside effects. Management uses no network listener and never logs or persists\ninvite material.\n\nFor any non-final message\u2014progress, blocker, suggestion, or later proactive note\u2014\nthe managed agent calls ordinary ours `send_message` to the channel identity.\nFleet checks only that the authenticated sender CID exactly equals `agent`, then\nforwards the text as a new message. There is no task/request/update type, phase,\nreply correlation, or owner recipient argument. A sole owner is the safe fallback;\nwith multiple owners and no inbound route history the relay fails closed. Devices\nsharing one identity share its CID; separate owner identities hand off the route\nwhen either sends channel mail. The ACP final is separate: fleet extracts it from\nthe completed turn and deterministically replies to the initiating owner wire.\n\nThe bounded mode-0600 route state stores CIDs, wire IDs, timestamps, delivery state,\nand hashes but never message plaintext. Unauthorized attempts produce a bounded\nCID-only owner warning; attempted bodies are neither reflected nor persisted.\n\nFor a mobile owner, establish the contact first, wait for peer verification,\nauthorize its exact CID, and revoke that same CID when access ends. The bounded\nmode-0600 CID overlay survives supervisor restart and remains fail-closed on\ncorruption. Update bodies remain memory-only. After a crash/restart, unfinished\ndeferred owner input follows the existing at-least-once replay path; the restarted\nsupervisor remains the sole binder.\n\n## Stable config and YAML migration\n\n`ours-fleet config --json` emits schemaVersion 1 resolved plans. Environment\nvalues and mission/persona/bio bodies are withheld; environment keys are sorted\nand values are marked redacted. Additive fields may appear in schema 1, while a\nremoval or semantic reuse requires a new schema version.\n\nYAML parsing always rejects duplicate keys. The current default\n`--yaml-mode compat` warns with file/line/column for anchors, aliases, explicit\ntags, non-scalar keys, and multiple documents. Use `--yaml-mode strict` in CI\nnow; strict becomes the next-major default and compat is the temporary migration\nescape hatch.\n\n## Bounded worklogs, auth proxy, and model recovery\n\nAn optional `worklog: { max_kb, keep_tail_kb, max_archives }` policy rotates a\nstable snapshot at fleet-owned lifecycle points. Concurrent changes defer\nrotation. Archives remain beside WORKLOG.md with the same sensitive-state\nboundary; retention deletes only recognized fleet archive names.\n\n`auth_proxy: { kind: anthropic, base_url, required, health_url }` is Claude-only\nand loopback-only. Fleet injects only ANTHROPIC_BASE_URL and doctor rejects\ncredential env keys. The privileged reference companion is\n`contrib/anthropic-auth-proxy.mjs`; deploy it separately as a dedicated account\nwith a 0600 token file and per-role listener access. Fleet never installs it or\nreads its credential.\n\n`model_chain` is an ordered authorization list and its first entry must equal\n`model`. Only sustained high-confidence entitlement/quota 429 evidence advances\none entry. Transient 429, overload, auth, policy, and unknown errors never\ndown-shift. Runtime state is atomic in .model-recovery.json; exhaustion is\nfail-closed and held down. Change the declared chain/model and restart to\nreconcile explicitly; no chain preserves detection-only behavior.\n";
|
|
7
|
+
export declare const AI_DOCS = "# ours-fleet reference\n\nours-fleet runs persistent or temporary, identity-bound AI roles. A role selects\na harness independently from its session backend:\n\n- harness: `claude-code` or `codex`\n- session: `tmux` (default) or `acp`\n- lifetime: permanent (supervised, restartable) or `spawn --temp`\n\n## Discover and validate\n\n```sh\nours-fleet docs # this complete reference (`man` is an alias)\nours-fleet help <command> # exact flags for one command\nours-fleet config [-c FILE] # validate and print the merged plan; no changes\nours-fleet doctor [-c FILE] [--harness codex|claude-code]\n```\n\nDefault configuration is `~/fleet.yaml` plus sorted `~/fleet.d/*.yaml` role\ndrop-ins. An explicit `-c FILE` replaces `~/fleet.yaml`; fleet.d still adds\nroles. Validate with `config` and `doctor` before starting or restarting.\n\nThe CLI never writes the base file: `spawn` writes `~/fleet.d/Name.yaml`. The\nweb console does write it, as a whole document \u2014 its setup wizard and\nconfiguration editor may create, change or remove any top-level block, including\n`vars:`, `defaults:`, `roles:`, `watchdogs:` and `loops:`. Only the base\nfile may hold `defaults:`, `watchdogs:` and `loops:`; a fleet.d drop-in may\ndeclare `roles:` and nothing else. Unrecognised top-level keys are round-tripped\nuntouched. Console edits are applied as surgical splices against the file's exact\nbytes, so an unchanged save is byte-identical and lines outside the edit keep their\ncomments and spacing. One exception: changing the length of a block sequence\n(`watch:`, `oversee:`, `roles:`, `wake_sources:`) may replace that collection\nwholesale and drop inline comments written on its items; lines outside that\ncollection remain byte-preserved. Each save is revision-guarded, reviewed as a diff\nof the real file before anything is written, validated by the real loader, and\nbacked up next to the file first.\n\n## Lifecycle and console commands\n\n```sh\nours-fleet init\nours-fleet up|down [Name...]\nours-fleet restart [Name...] # preserve/resume harness context\nours-fleet force-restart [Name...] # fresh context; briefing is reloaded\nours-fleet ls\nours-fleet status|peek|attach|logs Name\nours-fleet logs -f Name\nours-fleet send Name \"prompt\"\nours-fleet send Name --key Enter # tmux only\nours-fleet rm Name\nours-fleet watchdog-report <name> [run-id] [--list] [--json]\nours-fleet watchdog-run <name>\n```\n\n`peek`, `attach`, and text `send` work with tmux and ACP. ACP attachment\nalso accepts `/permit <permission-id> <option-id>`, `/interrupt`, and\n`/detach`. Raw `--key` input is tmux-only.\n\n## Local web console\n\nThe npm package includes the web console; installed users do not clone the repo\nor run `npm run build`:\n\n```sh\nnpm i -g @ours.network/fleet\nours-fleet init\nours-fleet doctor\nours-fleet web # install/update service, start, pair browser\n```\n\nThe normal command uses stable `http://127.0.0.1:49271/`, installs an\nowner-level systemd user service (Linux) or LaunchAgent (macOS), and opens a\nfive-minute one-use pairing link in the local browser. After pairing, bookmark\nthe plain URL or install the PWA. To pair a new, signed-out, or revoked browser,\nrun `ours-fleet web open`.\n\n```sh\nours-fleet web status\nours-fleet web start|stop|restart\nours-fleet web open\nours-fleet web revoke-all # revoke every browser and active session\nours-fleet web uninstall\nours-fleet web serve --port 0 --no-open # isolated foreground/testing mode\n```\n\nThe console is IPv4-loopback-only by default. Both `localhost` and\n`127.0.0.1` are accepted locally. For an nginx/TLS reverse proxy, keep the\ndefault bind and declare the exact browser origin:\n\n`ours-fleet web install --public-origin https://fleet.example.com --password-file /secure/fleet-password`\n\nFleet reads the password file during setup and persists only a salted scrypt\nverifier. New browsers authenticate and retain rotating HttpOnly/SameSite\ntrusted-device credentials. If nginx already authenticates, the operator may\ndeliberately select `--no-password`; the CLI and browser warn that anyone\nreaching the origin can control the fleet. First setup requires an explicit\nchoice: `--password-file` or `--pairing` for protected access, or\n`--no-password` for intentional unprotected access.\n\nUse `--bind ADDRESS` only for an intentional direct listen. A non-loopback\nbind is rejected unless `--public-origin` is also present. Host/Origin checks\nuse the declaration and do not trust forwarded headers. Configure nginx to\nproxy HTTP and WebSocket upgrades to `127.0.0.1:49271` and terminate TLS;\nfleet accepts nginx's loopback upstream Host, so no Host rewrite is required.\nBrowser credentials add Secure for HTTPS, and `revoke-all` invalidates all\ntrusted devices. Role creation offers harness-scoped known-model choices\nwhile still accepting a typed model ID; blank explicitly uses the selected\nharness's own default.\n\n## Spawn\n\n```sh\nours-fleet spawn [--temp] [Name | --role Name] \\\n --harness codex|claude-code --session tmux|acp \\\n --mission \"one line\" --cwd /absolute/path --identity Identity \\\n --coordinator Coordinator --model MODEL \\\n --approval ask|auto|allow \\\n --filesystem read-only|workspace|unrestricted \\\n --unattended deny|wait \\\n --bio-file /path/bio.md --persona-file /path/persona.md\n```\n\nPermanent spawn writes `~/fleet.d/Name.yaml` and starts a supervised role.\n`--temp` writes ephemeral state, starts a detached supervisor, and removes the\nrole after exit/reboot. Both lifetimes support `--session acp`.\n\nTemporary-role identity bootstrap is capability-based. The generated briefing\nfirst tries to bind the exact assigned identity and preserves it when it already\nexists. If missing, it uses ours MCP `create_temporary_identity` when that tool\nis exposed, tying a newly-created identity to the connector session lifecycle;\nolder servers fall back to `create_identity`. Collisions and creation errors\nstop safely without force-adopting or deleting identity state. Permanent roles\nretain normal `create_identity` behavior.\n\nInside a managed ACP role, the same CLI automatically routes a real `spawn`\nthrough that role's authenticated supervisor control socket. `--role Name` is\naccepted as an alternative to the positional name, so a minimal delegated call\nis `ours-fleet spawn --role DeveloperX --temp`. The supervisor records the\ncalling role, performs creation, and only after success sends a structured\nspawn notice through the caller's owner channel when one is configured.\n\nOmitted harness, session, working directory, coordinator, neutral permissions,\nfleet monitor policy, and (when the harness is unchanged) model inherit from the\ncalling role. Explicit options always win. Selecting a different harness without\n`--model` leaves model selection to that harness/fleet defaults rather than\ncopying an incompatible caller model. This automatic proxy is a convenience and\nattribution mechanism, not an isolation boundary: an unrestricted role can still\ninvoke another binary path directly. Tmux roles and host/operator shells keep the\nordinary direct CLI behavior.\n\nCodex-specific spawn flags: `--sandbox`, `--permission-mode`, `--launcher`,\n`--profile`, `--search`, repeatable `--codex-config key=value`, repeatable\n`--add-dir`, and legacy `--monitor` (consent for the native Codex monitor,\nnot the `monitor.mode` wake-owner selector). Run `ours-fleet help spawn` for\nexact values.\n\n## fleet.yaml\n\n```yaml\nvars:\n work_root: /home/me/work\nstart_stagger_ms: 0\ndefaults:\n harness: codex\n session: acp\n model: gpt-model-id\n permissions:\n approval: ask\n filesystem: workspace\n unattended: deny\n monitor:\n mode: fleet # fleet (default) | native\nroles:\n Coordinator:\n harness: codex\n session: acp\n identity: Coordinator\n cwd: ${work_root}/project\n mission: Coordinate work and delegate implementation.\n model: gpt-model-id\n permissions:\n approval: ask\n filesystem: workspace\n unattended: deny\n session_options: # advanced overrides; normally omit\n # acp:\n # command: [/custom/codex-acp, --flag]\n tmux:\n boot_grace_ms: 10000\n monitor:\n mode: fleet # fleet supervisor | native harness monitor\n interrupt: false # false queues; true cancels; after_tool steers at an ACP tool boundary\n wake_sources: [message_received, file_received, local_contact_request, pending_message]\n batch_ms: 2000\n inject: notification\n turn_fail_threshold: 3\n harness_options:\n launcher: auto\n sandbox: workspace-write\n approval: on-request\n search: false\n profile: fleet\n add_dirs: [/data/shared]\n config:\n model_reasoning_effort: high\n bio: Public role card and when peers should engage it.\n persona: Local operating contract, boundaries, and escalation policy.\n briefing_file: /absolute/custom-briefing.md\n coordinator: AnotherCoordinator\n env:\n KEY: value\n oversee:\n - { role: Worker, interval: 5m }\nwatchdogs:\n nightwatch: # [A-Za-z0-9_-], must not collide with a role name\n coordinator: FleetCoordinator # required \u2014 where alerts go\n # everything below is optional\n enabled: true # default true; false = configured but never scheduled\n interval: 10m # default 10m; 30s | 10m | 2h, minimum 1m\n watch: [Alice, CodexReviewer] # explicit lists are exact; omit for configured + live temp roles\n harness: claude-code # default: defaults.harness\n model: claude-fable-5 # default: same resolution rule roles use (resolveRoleModel)\n session: acp # default: defaults.session\n identity: Watchdog-nightwatch # default: Watchdog-<name>\n timeout: 5m # default 5m; a run past this is killed and recorded as error\n keep_reports: 50 # default 50 reports retained per watchdog\n alert_cooldown: 60m # default 60m before the same finding alerts again\n prompt_file: /abs/extra.md # optional extra focus, APPENDED to the fixed contract\n isolation: # optional; omitted means no OS sandbox, like an ordinary role\n backend: bubblewrap # when present, the ordinary role isolation schema applies\n network: broker\n fs: { read: [/opt/watch-data] }\n```\n\nA watchdog observes and reports; it never restarts, stops, spawns, or removes a\nrole, answers a pending permission, edits a workspace, or approves anything on\nthe owner's behalf. `watchdogs:` may appear only in the base config\n(`~/fleet.yaml` or `-c FILE`), not in `~/fleet.d/*.yaml` drop-ins.\nWatchdogs are not isolated by default. An explicit watchdog `isolation:` block\nuses the same policy schema as a role and is applied unchanged; declare every\nextra filesystem access required by a custom prompt there.\nWhen `watch:` is omitted, each run watches the configured roles plus temporary\nfleet roles that are live when the run starts. An explicit `watch:` list is\nnever augmented.\n\nRole values override defaults. `${name}` substitutes entries from `vars`.\nOther role fields include `max_tokens`, `autocompact_pct`, and `isolation`.\nUse README.md for the complete isolation policy and resource-cap schema.\n\n## Permissions\n\nPrefer the harness-neutral `permissions` block:\n\n- `approval: ask|auto|allow`: portable permission policy. `deny` remains a\n deprecated, fail-closed compatibility alias for existing fleet files.\n- `filesystem: read-only|workspace|unrestricted`: filesystem intent\n- `unattended: deny|wait`: what ACP does when no console can answer a request\n\nThe backend translates this common intent. Harness-native settings in\n`harness_options` take precedence where supplied. Do not choose\n`allow`/`unrestricted`, Codex `never`/`danger-full-access`, or Claude\n`bypassPermissions` without explicit authorization.\n\n### Creation-time isolation\n\n`ours-fleet spawn --isolation-file <path>` supplies a role's sandbox policy at\ncreation, so the FIRST launch is already confined \u2014 a role that only gains\n`isolation:` on a later `up` ran unsandboxed until then.\n\nThe file holds exactly the `isolation:` mapping documented above and nothing\nelse \u2014 the same schema, validated by the same code, so a policy written here\ncannot mean something different from the identical block in fleet.yaml:\n\n```yaml\nnetwork: deny\nfs:\n read: [/opt/reference]\nresources:\n mem: 2G\n```\n\nInvalid files are rejected before anything is created: no config, no state\ndirectory, no identity reservation. Works for both permanent and `--temp` roles.\n\n### Never-prompt failure\n\nThe failure this section exists to prevent leaves no error message anywhere.\n\nAn unattended role has no console. When the harness needs a permission decision\nthere is nobody to ask, so the request is refused INSIDE the harness \u2014 no\nprompt, no error, no log line. The agent simply does less than its briefing told\nit to, reports success, and nothing distinguishes that from having done the\nwork. Two settings produce it:\n\n1. a permission mode that suppresses the prompt without granting the action\n (Claude `dontAsk`, which is why neutral `allow` maps to\n `bypassPermissions` instead); and\n2. `unattended: deny`, which refuses every request that reaches it.\n\n**Automatic decisions are now recorded.** Every permission request decided\nwithout a human emits a completed event into\n`~/.ours-fleet/agents/<Name>/.session-events.jsonl` carrying the decision,\nwhether policy or a person made it, the policy that produced it\n(`permissions.unattended=deny` vs `permissions.approval=deny`/`=allow`),\nthe reason, and the option selected. `ours-fleet peek` and `attach` render\nthem. Automatic denial asks for a one-shot rejection, never a standing one, so a\nsingle unattended refusal cannot disable a tool for the rest of the session.\n\nA role that can auto-deny logs one line at startup saying so.\n\nTo detect an under-permissioned role BEFORE it runs, use the capability floor\nbelow: `ours-fleet doctor` fails such a role rather than letting it discover\nthe problem silently at work.\n\n### The unattended capability floor\n\nAn unattended role has no console, so a permission request cannot be answered \u2014\nit is refused, silently, inside the harness. The agent then does less than it\nwas told to and reports no error. To make that visible before launch,\n`ours-fleet config` and `ours-fleet doctor` resolve each role's neutral\npermissions through its harness and check the result against a fixed floor:\n\n- `read-state` \u2014 read its briefing, ROUTINES.md, and WORKLOG.md\n- `write-state` \u2014 append its WORKLOG and its own state files\n- `messaging` \u2014 bind its identity, send and receive ours mail\n- `monitor` \u2014 arm and observe its mail monitor\n- `workspace-edit` \u2014 edit and test files in its working directory\n- `status-commands` \u2014 run the inspection commands its briefing prescribes\n\n`doctor` reports this per role as `unattended floor: <Role>`. A role with\n`unattended: deny` that cannot meet the floor FAILS doctor, because it will\ndeny those requests with nobody to see it; with `unattended: wait` it warns,\nbecause a human can still attach and answer.\n\nSecurity meaning: `ask` maps to Codex `untrusted` and Claude `default`;\n`auto` maps to Codex `on-request` and Claude `acceptEdits`; and\n`approval: allow` maps to Codex `never` and Claude `bypassPermissions`,\nwhich genuinely permits the actions the role was authorized to take \u2014\n`dontAsk` only suppresses the prompt while still refusing the action. Nothing\nother than an explicit `allow` becomes non-interactive. Legacy `deny` keeps\nits conservative Codex `on-request` / Claude `plan` translation. `allow` is therefore a real grant and\nrequires explicit authorization; per-role `isolation:` remains the outer\nboundary that a permission mode cannot cross.\n\nACP carries agent-advertised session mode IDs and `session/set_mode`, but those\nIDs are agent-specific and ACP defines no portable permission-policy capability.\nFleet therefore uses the ACP primitive where an adapter exposes a matching mode\nand otherwise performs the harness translation above. The live session reports\nboth its effective normalized mode and exact harness-native mode.\n\nSee also: `spawn --approval/--filesystem/--unattended` set this intent at\ncreation, and `ours-fleet config` prints each role's neutral settings, their\nnative translation, and any warning \u2014 the same text `doctor` reports.\n\nClaude `harness_options`: `permission_mode` (default, acceptEdits, plan,\ndontAsk, bypassPermissions), `plugins`, `mem_palace`, and\n`mem_palace_midsession_autosave`.\n\nCodex `harness_options`: `launcher` (auto, ours-codex, codex), `sandbox`\n(read-only, workspace-write, danger-full-access), `approval` or\n`permission_mode` (untrusted, on-request, never), `profile`, `search`,\n`config`, `add_dirs`, and `monitor`.\n\n## ACP adapters\n\nThe maintained `@agentclientprotocol/codex-acp` and\n`@agentclientprotocol/claude-agent-acp` runtimes are bundled automatically as\noptional ours-fleet dependencies. The supervisor resolves their executable\nentrypoints internally, so default ACP roles do not depend on global PATH.\nThe maintained Claude adapter requires Node 22; tmux and Codex ACP continue to\nwork on the ours-fleet core minimum of Node 20.\n\nOverride an adapter only when necessary with `session_options.acp.command`\n(string or argv list). If optional dependencies were deliberately omitted,\nours-fleet falls back to a compatible globally installed `codex-acp` or\n`claude-agent-acp`. `ours-fleet doctor -c FILE` verifies the resolved adapter.\n\n## Reliable mail wake\n\n`monitor.mode` selects exactly one wake owner:\n\n- `fleet` (default): the ours-fleet supervisor consumes body-free daemon\n events and advances its durable cursor only after delivery is accepted. ACP\n uses live steering when supported and falls back to structured\n `session/prompt`; tmux uses verified console injection.\n- `native`: ours-fleet starts no supervisor monitor; the generated briefing\n instructs Claude Code or Codex to arm its harness-native wake mechanism.\n\nSet `monitor.interrupt: true` in fleet mode to cancel active work before every\nconfigured wake. Set it to `after_tool` to preserve an active ACP tool (and any\npending permission), then steer the wake at the first tool-terminal boundary\nwithout cancellation. A hung boundary is bounded at 120 seconds and falls back\nto non-cancelling steering/queueing; adapters without authenticated tool events\nuse the same conservative fallback. Explicit human/control interrupts remain\nimmediate. The policy is content-blind because the supervisor cannot inspect\nencrypted message bodies. Message bodies are released only when the role calls\nthe ours `get_messages` tool.\n\nThe default is `false`. For a temporary role whose mission intentionally arrives\nafter its readiness announcement, set `mode: fleet` and `interrupt: true`\nexplicitly. The readiness announcement does not change the transport: the\nmission remains ordinary ours mail, fleet injects only the body-free wake, and\nthe role calls `get_messages` before acting. Every later configured wake uses\nthe same interruption policy.\n\nLegacy `monitor.enabled: true|false` remains accepted as an alias for\n`mode: fleet|native`; use `mode` in new configuration. Codex's separate\n`harness_options.monitor: true` is native-monitor consent, not monitor-owner\nselection.\nInspect `ours-fleet status Name`, `peek Name`, role logs, and\n`~/.ours-fleet/agents/Name/.monitor-status` when diagnosing delivery.\n\n## Trusted owner channel\n\nAn ACP role may declare a separate, existing ours identity which fleet \u2014 never\nthe agent \u2014 binds:\n\n```yaml\nowner_channel:\n identity: Coordinator Owner Channel\n owners: [authenticated-owner-contact-cid]\n agent: authenticated-managed-agent-cid\n interrupt: false\n progress_interval_ms: 30000\n comments: true\n attachments:\n enabled: true\n max_files_per_request: 4\n max_file_bytes: 10485760\n max_request_bytes: 20971520\n retention_ms: 86400000\n allowed_mime: [application/pdf, text/plain, image/png, audio/ogg]\n```\n\nThis does not replace the role identity. Normal identity mail remains untrusted\npeer input: the agent reads it through `get_messages` and replies through\n`send_message`. Mail arriving on the dedicated channel from a CID in `owners`\nis injected as a direct `[fleet-owner]` prompt. Mail from the exact `agent`\nCID is forwarded as a new message to the latest authenticated owner conversation;\nits files may also be relayed through this channel. A reply reference selects the\nowner of that authenticated source wire instead of the latest conversation.\nEvery other CID is rejected and warned about without reflecting its body. Fleet sends\naccepted/queued/progress/interrupted/failure notices and routes the ACP turn's\nfinal assistant text back to the authenticated sender with its source wire ID.\nFor file replies, fleet injects a request-specific outbox path into the owner\nprompt. The agent copies completed artifacts there; fleet sends every regular\nfile from the channel identity with the same source wire ID and removes the\ntemporary outbox only after successful delivery. For proactive or in-turn agent\nattachments, the agent calls ours `send_file` to the channel identity and may\npair it with a reply-linked caption; fleet, not the agent, chooses the owner.\nOwner messages whose trimmed text starts with `/` are deterministic\nsupervisor commands and never enter the model: `/help` (alias `/commands`),\n`/status`, `/comments [status|on|off]`, `/interrupt`, `/clear`,\n`/compact`, `/model <model-id>`, `/restart`, `/force-restart`, `/ls`,\n`/peek`, `/worklog`, and\n`/version`. Unknown or malformed commands answer with the help text instead of\nbeing forwarded; plain messages reach the agent unchanged. `/clear`,\n`/compact`, and `/model` are forwarded only when the role's bundled ACP\nadapter executes them locally (claude-code: all three; codex: `/compact`\nonly) and are otherwise refused with a notice, so slash text never reaches the\nmodel as a prompt.\n\nWhile a request runs, the agent's live ACP commentary is relayed as messages\nprefixed with the single stable label `\uD83D\uDFE1 Live update:`, so an owner can see\nexactly which messages the setting controls. `owner_channel.comments`\n(default `true`, so existing channels keep their current behavior) is the\nRESTART BASELINE; `/comments on|off` changes only the running session and is\ndeliberately not persisted, so a restart always returns to the checked-in\nconfiguration. `/comments status` reports the live value, the baseline, and\nwhether the backend emits live comments at all. Suppressing live comments never\nsuppresses receipts, progress notices, or the final answer.\n\nOwner documents, images, and voice messages use the same authenticated sender\nand source-wire boundary. Fleet inspects body-free metadata first and rejects\ndisabled, over-count, over-size, or disallowed-MIME requests before selective\nretrieval. Unauthorized CIDs are never retrieved or answered. Reply-linked text\nand files from the same sender become one ordered request; a file-only wake also\nstarts a turn. Retrieved bytes must match their structured size and SHA-256,\ntheir content signature must match the declared MIME, and symlinks or non-regular\npaths fail closed. Sanitized copies live only in a mode-0700 request directory as\nmode-0600 files and are removed after completion or bounded stale retention.\n\nVoice prompts include a bounded transcript only when ours-mcp reports success.\nFailure or unavailability is explicit and preserves the private audio path as the\nfallback. Run `ours-mcp voice-status --json` to inspect the host configuration.\nA mode-0600 crash journal contains only authenticated CID and wire routing data;\nit never stores captions, filenames, paths, transcript text, or bytes. Journaled\npost-retrieval files resume selectively through `save_file`. A deferred agent\ncaption is replayed with its processed files before the group is admitted. Fleet\nresolves one authenticated owner route before retrieving bytes, admits every file\nbefore emitting the caption or any file, and sends every part to that same route.\nUnknown correlated routes remain queued without retrieval and receive one bounded\ncorrelated notice. Admission rejection consumes the whole group with one NACK;\nonce emission starts, a transport error becomes terminal uncertain delivery and\nthe group is never blind-retried. Bounded v2 source-wire routing state is migrated\nfrom v1 on read. Corrupt state disables attachment admission rather than weakening\nprovenance checks.\n\nThe channel identity must be unique and must not be a role identity. The bridge\npersists bounded wire IDs only, never message/reply plaintext, and requeues input\nbefore starting its turn for at-least-once crash recovery. It currently requires\n`session: acp`: tmux has no structured, turn-correlated final answer, and pane\nscraping cannot provide the same reliable reply guarantee.\n\n### Live contact and owner administration\n\nThe supervisor which is already running the ACP role remains the sole binder of\n`owner_channel.identity`. The CLI reaches that exact live `OwnerChannel`\nthrough the role's token-authenticated, mode-0600 Unix control socket for contact\ninspection and setup; it never starts another ours client and never force-binds:\n\nRapid supervised restart is serialized by a role-scoped single-binder lease.\nThe predecessor closes its authenticated control socket and MCP proxy before\nreleasing ownership. The replacement waits at most five seconds and retries the\ndaemon bind only when PID/start-marker metadata proves the holder was the same\nrole and owner-channel identity. Foreign, live, corrupt, or otherwise\nunverifiable ownership remains fail-closed; fleet never uses `force=true`.\n\nIf that matching predecessor misses the bound, its still-authenticated control\nroute may send one fixed, digest-deduplicated recovery notice through the latest\nauthenticated owner conversation (or the sole configured owner). Notice\nplaintext is never persisted. With no safe deterministic route fleet guesses no\nrecipient and leaves the actionable failure in the web console and role logs.\nThe remote recovery action is `/restart`; inspect repeated failures with\n`ours-fleet logs <Role>` or the web console.\n\n```sh\nours-fleet owner-channel contact list <Role>\nours-fleet owner-channel contact invite <Role> [--name <label>]\nours-fleet owner-channel contact add <Role> (--invite-file <path> | --invite-stdin) [--name <label>]\nours-fleet owner-channel owner list <Role>\nours-fleet owner-channel owner authorize <Role> <exact-64-hex-contact-cid>\nours-fleet owner-channel owner revoke <Role> <exact-64-hex-contact-cid>\n```\n\nContact establishment and owner authorization are separate security steps.\n`contact add` never authorizes: invite redemption is pending until the peer\nverifies it. Once `contact list` reports the established contact, authorize\nits exact immutable CID explicitly. Invite creation emits invite material only\non stdout; acceptance reads it from a file or stdin, not argv.\n\nConfigured `owners` remain the baseline. On legacy channels without `agent`,\nlive authorizations/revocations are an immediately effective, restart-persistent\noverlay. Managed-agent CID gating makes fleet configuration authoritative and\ndisables live owner mutation and direct control-socket sends. `owner list` labels\nbaseline versus dynamic entries and effective status. The atomic mode-0600 file\ncontains bounded CIDs and audit actions only. Corruption disables all effective\nowners and refuses mutation rather than resurrecting authority; revoking the\nlast effective owner is always refused.\n\nA missing/stopped role, tmux session, role without `owner_channel`, unavailable\nMCP client, or a role entering shutdown returns an actionable error with no\nside effects. Management uses no network listener and never logs or persists\ninvite material.\n\nFor any non-final message\u2014progress, blocker, suggestion, or later proactive note\u2014\nthe managed agent calls ordinary ours `send_message` to the channel identity.\nFleet checks only that the authenticated sender CID exactly equals `agent`, then\nforwards the text as a new message. There is no task/request/update type, phase,\nreply correlation, or owner recipient argument. A sole owner is the safe fallback;\nwith multiple owners and no inbound route history the relay fails closed. Devices\nsharing one identity share its CID; separate owner identities hand off the route\nwhen either sends channel mail. The ACP final is separate: fleet extracts it from\nthe completed turn and deterministically replies to the initiating owner wire.\n\nThe bounded mode-0600 route state stores CIDs, wire IDs, timestamps, delivery state,\nand hashes but never message plaintext. Unauthorized attempts produce a bounded\nCID-only owner warning; attempted bodies are neither reflected nor persisted.\n\nFor a mobile owner, establish the contact first, wait for peer verification,\nauthorize its exact CID, and revoke that same CID when access ends. The bounded\nmode-0600 CID overlay survives supervisor restart and remains fail-closed on\ncorruption. Update bodies remain memory-only. After a crash/restart, unfinished\ndeferred owner input follows the existing at-least-once replay path; the restarted\nsupervisor remains the sole binder.\n\n## Stable config and YAML migration\n\n`ours-fleet config --json` emits schemaVersion 1 resolved plans. Environment\nvalues and mission/persona/bio bodies are withheld; environment keys are sorted\nand values are marked redacted. Additive fields may appear in schema 1, while a\nremoval or semantic reuse requires a new schema version.\n\nYAML parsing always rejects duplicate keys. The current default\n`--yaml-mode compat` warns with file/line/column for anchors, aliases, explicit\ntags, non-scalar keys, and multiple documents. Use `--yaml-mode strict` in CI\nnow; strict becomes the next-major default and compat is the temporary migration\nescape hatch.\n\n## Bounded worklogs, auth proxy, and model recovery\n\nAn optional `worklog: { max_kb, keep_tail_kb, max_archives }` policy rotates a\nstable snapshot at fleet-owned lifecycle points. Concurrent changes defer\nrotation. Archives remain beside WORKLOG.md with the same sensitive-state\nboundary; retention deletes only recognized fleet archive names.\n\n`auth_proxy: { kind: anthropic, base_url, required, health_url }` is Claude-only\nand loopback-only. Fleet injects only ANTHROPIC_BASE_URL and doctor rejects\ncredential env keys. The privileged reference companion is\n`contrib/anthropic-auth-proxy.mjs`; deploy it separately as a dedicated account\nwith a 0600 token file and per-role listener access. Fleet never installs it or\nreads its credential.\n\n`model_chain` is an ordered authorization list and its first entry must equal\n`model`. Only sustained high-confidence entitlement/quota 429 evidence advances\none entry. Transient 429, overload, auth, policy, and unknown errors never\ndown-shift. Runtime state is atomic in .model-recovery.json; exhaustion is\nfail-closed and held down. Change the declared chain/model and restart to\nreconcile explicitly; no chain preserves detection-only behavior.\n";
|
|
8
8
|
/**
|
|
9
9
|
* What every shipped spawn-skill variant must say, and must not say (7.1).
|
|
10
10
|
*
|
package/dist/docs.js
CHANGED
|
@@ -195,7 +195,7 @@ roles:
|
|
|
195
195
|
boot_grace_ms: 10000
|
|
196
196
|
monitor:
|
|
197
197
|
mode: fleet # fleet supervisor | native harness monitor
|
|
198
|
-
interrupt: false # true cancels
|
|
198
|
+
interrupt: false # false queues; true cancels; after_tool steers at an ACP tool boundary
|
|
199
199
|
wake_sources: [message_received, file_received, local_contact_request, pending_message]
|
|
200
200
|
batch_ms: 2000
|
|
201
201
|
inject: notification
|
|
@@ -393,9 +393,14 @@ ours-fleet falls back to a compatible globally installed \`codex-acp\` or
|
|
|
393
393
|
instructs Claude Code or Codex to arm its harness-native wake mechanism.
|
|
394
394
|
|
|
395
395
|
Set \`monitor.interrupt: true\` in fleet mode to cancel active work before every
|
|
396
|
-
configured wake.
|
|
397
|
-
|
|
398
|
-
|
|
396
|
+
configured wake. Set it to \`after_tool\` to preserve an active ACP tool (and any
|
|
397
|
+
pending permission), then steer the wake at the first tool-terminal boundary
|
|
398
|
+
without cancellation. A hung boundary is bounded at 120 seconds and falls back
|
|
399
|
+
to non-cancelling steering/queueing; adapters without authenticated tool events
|
|
400
|
+
use the same conservative fallback. Explicit human/control interrupts remain
|
|
401
|
+
immediate. The policy is content-blind because the supervisor cannot inspect
|
|
402
|
+
encrypted message bodies. Message bodies are released only when the role calls
|
|
403
|
+
the ours \`get_messages\` tool.
|
|
399
404
|
|
|
400
405
|
The default is \`false\`. For a temporary role whose mission intentionally arrives
|
|
401
406
|
after its readiness announcement, set \`mode: fleet\` and \`interrupt: true\`
|
package/dist/fleet-proxy.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { inheritedPermissionMode } from './permissions.js';
|
|
2
2
|
/** Present only inside a managed role process. The CLI treats it as a routing hint, not authority. */
|
|
3
3
|
export const FLEET_PROXY_STATE_DIR_ENV = 'OURS_FLEET_PROXY_STATE_DIR';
|
|
4
4
|
export const FLEET_PROXY_CALLER_ENV = 'OURS_FLEET_PROXY_CALLER';
|
|
@@ -21,7 +21,7 @@ export function inheritCallerSpawnDefaults(caller, requested, configPath) {
|
|
|
21
21
|
take('cwd', caller.cwd);
|
|
22
22
|
take('coordinator', caller.name);
|
|
23
23
|
if (options.approval === undefined)
|
|
24
|
-
take('approval',
|
|
24
|
+
take('approval', inheritedPermissionMode(caller));
|
|
25
25
|
take('filesystem', caller.permissions.filesystem);
|
|
26
26
|
take('unattended', caller.permissions.unattended);
|
|
27
27
|
take('monitorConfig', structuredClone(caller.monitor));
|
package/dist/harness/codex.js
CHANGED
|
@@ -14,6 +14,7 @@ const LAUNCHERS = ['auto', 'ours-codex', 'codex'];
|
|
|
14
14
|
const SANDBOX_MODES = ['read-only', 'workspace-write', 'danger-full-access'];
|
|
15
15
|
/** Codex CLI's accepted `--ask-for-approval` values. */
|
|
16
16
|
const APPROVAL_POLICIES = ['untrusted', 'on-request', 'never'];
|
|
17
|
+
const BUNDLED_CODEX_ACP_VERSION = '1.1.7';
|
|
17
18
|
/**
|
|
18
19
|
* What an unattended role can actually do under Codex's native settings.
|
|
19
20
|
* `on-request` and `untrusted` stop to ask, and with no console attached that
|
|
@@ -55,6 +56,22 @@ function acpAgentMode(role) {
|
|
|
55
56
|
return 'agent-full-access';
|
|
56
57
|
return undefined;
|
|
57
58
|
}
|
|
59
|
+
function acpModePermissions(mode) {
|
|
60
|
+
if (mode === 'read-only')
|
|
61
|
+
return { approval: 'on-request', sandbox: 'read-only' };
|
|
62
|
+
if (mode === 'agent-full-access')
|
|
63
|
+
return { approval: 'never', sandbox: 'danger-full-access' };
|
|
64
|
+
return { approval: 'on-request', sandbox: 'workspace-write' };
|
|
65
|
+
}
|
|
66
|
+
function fleetModeForApproval(nativeMode) {
|
|
67
|
+
if (nativeMode === 'never')
|
|
68
|
+
return 'allow';
|
|
69
|
+
if (nativeMode === 'on-request')
|
|
70
|
+
return 'auto';
|
|
71
|
+
if (nativeMode === 'untrusted')
|
|
72
|
+
return 'ask';
|
|
73
|
+
throw new Error(`unsupported Codex approval policy '${nativeMode}'`);
|
|
74
|
+
}
|
|
58
75
|
/** Resolve & validate the per-role approval policy, throwing on an unknown value. */
|
|
59
76
|
function approvalPolicy(role) {
|
|
60
77
|
const o = role.harness_options;
|
|
@@ -282,6 +299,22 @@ export function makeCodexAdapter(exec = realExec) {
|
|
|
282
299
|
return translated;
|
|
283
300
|
const approval = approvalPolicy(role) ?? 'on-request';
|
|
284
301
|
const sandbox = sandboxMode(role) ?? 'workspace-write';
|
|
302
|
+
if (role.session === 'acp') {
|
|
303
|
+
const mode = acpAgentMode(role) ?? 'agent';
|
|
304
|
+
const actual = acpModePermissions(mode);
|
|
305
|
+
const exact = actual.approval === approval && actual.sandbox === sandbox;
|
|
306
|
+
return {
|
|
307
|
+
...translated,
|
|
308
|
+
native: { mode, ...actual },
|
|
309
|
+
exact,
|
|
310
|
+
warnings: exact ? [] : [
|
|
311
|
+
`bundled codex-acp ${BUNDLED_CODEX_ACP_VERSION} mode '${mode}' actually uses `
|
|
312
|
+
+ `approval=${actual.approval} sandbox=${actual.sandbox}; this does not exactly `
|
|
313
|
+
+ `represent approval=${approval} sandbox=${sandbox}`,
|
|
314
|
+
],
|
|
315
|
+
capabilities: codexCapabilities(actual.approval, actual.sandbox),
|
|
316
|
+
};
|
|
317
|
+
}
|
|
285
318
|
return {
|
|
286
319
|
...translated,
|
|
287
320
|
native: { approval, sandbox },
|
|
@@ -289,13 +322,17 @@ export function makeCodexAdapter(exec = realExec) {
|
|
|
289
322
|
};
|
|
290
323
|
},
|
|
291
324
|
effectivePermissionMode(role) {
|
|
325
|
+
if (role.session === 'acp') {
|
|
326
|
+
const nativeMode = acpAgentMode(role) ?? 'agent';
|
|
327
|
+
return {
|
|
328
|
+
fleetMode: fleetModeForApproval(acpModePermissions(nativeMode).approval), nativeMode,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
292
331
|
const nativeMode = approvalPolicy(role) ?? 'untrusted';
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
throw new Error(`unsupported Codex approval policy '${nativeMode}'`);
|
|
298
|
-
return { fleetMode, nativeMode };
|
|
332
|
+
return { fleetMode: fleetModeForApproval(nativeMode), nativeMode };
|
|
333
|
+
},
|
|
334
|
+
inheritedPermissionMode(role) {
|
|
335
|
+
return fleetModeForApproval(approvalPolicy(role) ?? 'untrusted');
|
|
299
336
|
},
|
|
300
337
|
vocabulary: {
|
|
301
338
|
bindTool: 'choose_identity',
|
package/dist/harness/types.d.ts
CHANGED
|
@@ -111,6 +111,8 @@ export interface HarnessAdapter {
|
|
|
111
111
|
fleetMode: FleetPermissionMode;
|
|
112
112
|
nativeMode: string;
|
|
113
113
|
};
|
|
114
|
+
/** Configured portable intent to inherit when a live runtime preset is narrower. */
|
|
115
|
+
inheritedPermissionMode?(role: ResolvedRole): FleetPermissionMode;
|
|
114
116
|
/**
|
|
115
117
|
* REQUIRED. Every adapter must either translate neutral permissions or
|
|
116
118
|
* explicitly declare that it cannot. Enforced at registration.
|
package/dist/monitor.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { MonitorConfig, NotifyEventType } from './config.js';
|
|
1
|
+
import type { MonitorConfig, MonitorInterrupt, NotifyEventType } from './config.js';
|
|
2
2
|
import { type FailureEvidence } from './model-recovery.js';
|
|
3
3
|
/** A content-free arrival event as the daemon serves it over the notifications API. */
|
|
4
4
|
export interface NotifyEvent {
|
|
@@ -47,11 +47,12 @@ export interface MonitorDeps {
|
|
|
47
47
|
*/
|
|
48
48
|
delivery?: {
|
|
49
49
|
submit(text: string, options?: {
|
|
50
|
-
interrupt?:
|
|
50
|
+
interrupt?: MonitorInterrupt;
|
|
51
51
|
}): Promise<{
|
|
52
52
|
succeeded: boolean;
|
|
53
53
|
outcome: string;
|
|
54
54
|
detail?: string;
|
|
55
|
+
safeBoundary?: 'direct' | 'after_tool' | 'timeout' | 'unsupported';
|
|
55
56
|
}>;
|
|
56
57
|
};
|
|
57
58
|
/** Body-free, typed evidence for runner-owned model recovery. */
|
|
@@ -63,7 +64,7 @@ export interface MonitorDeps {
|
|
|
63
64
|
* nothing whatsoever about whether wakes are being delivered or whether the
|
|
64
65
|
* turns they trigger keep dying.
|
|
65
66
|
*/
|
|
66
|
-
export type StatusCause = 'connectivity' | 'delivery' | 'modal' | 'offline' | 'turns-failing' | 'auth';
|
|
67
|
+
export type StatusCause = 'connectivity' | 'delivery' | 'modal' | 'offline' | 'turns-failing' | 'safe-boundary' | 'auth';
|
|
67
68
|
/** Best-effort daemon config (issue #17): the fields the MCP client reads. */
|
|
68
69
|
interface DaemonConfig {
|
|
69
70
|
apiToken?: string;
|
package/dist/monitor.js
CHANGED
|
@@ -489,13 +489,22 @@ export class Monitor {
|
|
|
489
489
|
this.degrade('delivery', `wake ${result.outcome}${result.detail ? ` (${result.detail})` : ''}`);
|
|
490
490
|
return false;
|
|
491
491
|
}
|
|
492
|
+
if (result.safeBoundary === 'timeout' || result.safeBoundary === 'unsupported')
|
|
493
|
+
this.degrade('safe-boundary', result.detail ?? `after_tool ${result.safeBoundary}`);
|
|
494
|
+
else
|
|
495
|
+
this.recover('safe-boundary');
|
|
492
496
|
this.recover('delivery', 'modal');
|
|
493
|
-
if (result.
|
|
497
|
+
if (result.outcome !== 'injected' && result.outcome !== 'startedNewTurn')
|
|
494
498
|
this.recordTurn('completed');
|
|
495
499
|
return true;
|
|
496
500
|
}
|
|
497
|
-
|
|
501
|
+
// Tmux exposes no authenticated tool lifecycle. `after_tool` therefore
|
|
502
|
+
// degrades to the existing non-cancelling injection path; never guess a
|
|
503
|
+
// boundary from pane text and never send C-c for this mode.
|
|
504
|
+
if (this.cfg.interrupt === true)
|
|
498
505
|
await this.deps.tmux.sendKey(this.name, 'C-c');
|
|
506
|
+
if (this.cfg.interrupt === 'after_tool')
|
|
507
|
+
this.degrade('safe-boundary', 'after_tool unsupported by tmux; using non-cancelling delivery');
|
|
499
508
|
const state = await this.awaitInjectable(pid);
|
|
500
509
|
if (state !== 'ready') {
|
|
501
510
|
if (state === 'offline')
|
|
@@ -179,7 +179,10 @@ export class OwnerChannel {
|
|
|
179
179
|
if (!this.ready || this.stopping)
|
|
180
180
|
throw new Error('owner-channel MCP client is unavailable');
|
|
181
181
|
const model = event.model ? `, model ${event.model}` : '';
|
|
182
|
-
const
|
|
182
|
+
const monitorPolicy = event.monitor.interrupt === true
|
|
183
|
+
? ' with interruption'
|
|
184
|
+
: event.monitor.interrupt === 'after_tool' ? ' with after-tool steering' : '';
|
|
185
|
+
const monitor = `${event.monitor.mode} monitor${monitorPolicy}`;
|
|
183
186
|
const inherited = event.inherited.length
|
|
184
187
|
? ` Supervisor inherited omitted defaults: ${event.inherited.join(', ')}.` : '';
|
|
185
188
|
await this.sendProactiveMessage(`🧑💻 ${event.caller} spawned ${event.lifetime} agent ${event.role} `
|
package/dist/permissions.d.ts
CHANGED
|
@@ -67,6 +67,8 @@ export declare function effectivePermissionMode(role: ResolvedRole): {
|
|
|
67
67
|
fleetMode: import('./config.js').FleetPermissionMode;
|
|
68
68
|
nativeMode: string;
|
|
69
69
|
};
|
|
70
|
+
/** Preserve configured intent when a managed child inherits from its caller. */
|
|
71
|
+
export declare function inheritedPermissionMode(role: ResolvedRole): import('./config.js').FleetPermissionMode;
|
|
70
72
|
/** Resolve one role's permissions through its adapter. Never throws. */
|
|
71
73
|
export declare function analyzeRolePermissions(role: ResolvedRole): RolePermissionAnalysis;
|
|
72
74
|
/** Every line a command should show for a role: translation, conflicts, floor. */
|
package/dist/permissions.js
CHANGED
|
@@ -30,6 +30,11 @@ export function effectivePermissionMode(role) {
|
|
|
30
30
|
throw new Error(`harness '${role.harness}' cannot report an effective ask|auto|allow permission mode`);
|
|
31
31
|
return adapter.effectivePermissionMode(role);
|
|
32
32
|
}
|
|
33
|
+
/** Preserve configured intent when a managed child inherits from its caller. */
|
|
34
|
+
export function inheritedPermissionMode(role) {
|
|
35
|
+
const adapter = getAdapter(role.harness);
|
|
36
|
+
return adapter.inheritedPermissionMode?.(role) ?? effectivePermissionMode(role).fleetMode;
|
|
37
|
+
}
|
|
33
38
|
/**
|
|
34
39
|
* Find native settings that contradict the neutral block. Only fires when the
|
|
35
40
|
* operator wrote BOTH — a role that states its intent once, neutrally or
|
package/dist/runner.js
CHANGED
|
@@ -585,18 +585,33 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
585
585
|
// and closes the session before the wake turn can run. During startup,
|
|
586
586
|
// steer into the live turn instead; after it completes, honor the
|
|
587
587
|
// configured interrupt policy normally.
|
|
588
|
-
const
|
|
589
|
-
const
|
|
590
|
-
|
|
588
|
+
const policy = options?.interrupt;
|
|
589
|
+
const interrupt = policy === true && acpStartupComplete;
|
|
590
|
+
const promptOptions = {
|
|
591
|
+
interrupt, steer: true,
|
|
591
592
|
...(interrupt ? { interruptSource: 'fleet-monitor' } : {}),
|
|
592
593
|
origin: { kind: 'fleet-monitor' },
|
|
593
|
-
}
|
|
594
|
+
};
|
|
595
|
+
// Startup is already a protected boundary: as with immediate mode,
|
|
596
|
+
// steer rather than waiting on/cancelling the runner-owned first turn.
|
|
597
|
+
const result = policy === 'after_tool' && acpStartupComplete
|
|
598
|
+
? await arbiter.submitPromptAfterTool(text, promptOptions)
|
|
599
|
+
: await arbiter.submitPrompt(text, promptOptions);
|
|
594
600
|
const steered = result.accepted
|
|
595
601
|
&& (result.detail === 'injected' || result.detail === 'startedNewTurn');
|
|
602
|
+
const boundary = result.safeBoundary;
|
|
603
|
+
const boundaryDetail = boundary
|
|
604
|
+
? boundary.state === 'timeout'
|
|
605
|
+
? `after_tool timed out after ${boundary.waitedMs}ms; steered without cancellation`
|
|
606
|
+
: boundary.state === 'unsupported'
|
|
607
|
+
? 'after_tool unsupported; used non-cancelling queued delivery'
|
|
608
|
+
: `after_tool ${boundary.state} delivery after ${boundary.waitedMs}ms`
|
|
609
|
+
: result.detail;
|
|
596
610
|
return {
|
|
597
611
|
succeeded: result.succeeded || steered,
|
|
598
612
|
outcome: steered ? result.detail : result.outcome,
|
|
599
|
-
detail:
|
|
613
|
+
detail: boundaryDetail,
|
|
614
|
+
...(boundary ? { safeBoundary: boundary.state } : {}),
|
|
600
615
|
};
|
|
601
616
|
},
|
|
602
617
|
};
|
package/dist/session/acp.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ import type { CommonPermissions } from '../config.js';
|
|
|
3
3
|
import { ConversationEventStore } from './conversation-store.js';
|
|
4
4
|
import type { ConversationSnapshot, PromptOrigin, PromptReceipt, SubmitPromptCommand } from './conversation-types.js';
|
|
5
5
|
import type { ConversationHandlePage, ExitRecord, QueuedPrompt, SessionEvent, RuntimeSelectorMetadata, SessionHandle, SessionSnapshot, SubmitPromptOptions, TurnCancellationSource, TurnOutcome, TurnResult } from './types.js';
|
|
6
|
+
/** Bound safe-boundary waiting without turning a hung tool into cancellation. */
|
|
7
|
+
export declare const AFTER_TOOL_BOUNDARY_TIMEOUT_MS = 120000;
|
|
6
8
|
/** Server-generated typed provenance followed by the exact human-authored body. */
|
|
7
9
|
export declare function promptContentBlocks(text: string, origin?: PromptOrigin): acp.ContentBlock[];
|
|
8
10
|
export declare function runtimeSelector(options: acp.SessionConfigOption[] | null | undefined, category: string): RuntimeSelectorMetadata | undefined;
|
|
@@ -25,6 +27,8 @@ export interface AcpSessionOptions {
|
|
|
25
27
|
permissionTimeoutMs?: number;
|
|
26
28
|
/** Grace after the last controller detaches before the unattended policy applies. */
|
|
27
29
|
controllerGraceMs?: number;
|
|
30
|
+
/** Test seam; production uses AFTER_TOOL_BOUNDARY_TIMEOUT_MS. */
|
|
31
|
+
afterToolBoundaryTimeoutMs?: number;
|
|
28
32
|
}
|
|
29
33
|
/**
|
|
30
34
|
* Classify an ACP `stopReason` into a terminal outcome. A refusal and a
|
|
@@ -61,9 +65,13 @@ export declare class AcpSession implements SessionHandle {
|
|
|
61
65
|
private runtimeModel?;
|
|
62
66
|
private reasoningEffort?;
|
|
63
67
|
private controllerCount;
|
|
68
|
+
private closing;
|
|
64
69
|
/** Armed when the last controller detaches; unattended policy applies on fire. */
|
|
65
70
|
private controllerGrace?;
|
|
66
71
|
private cancelEscalation?;
|
|
72
|
+
/** ACP-authenticated in-flight calls, including independently reserved permissions. */
|
|
73
|
+
private readonly activeToolCalls;
|
|
74
|
+
private readonly toolBoundaryWaiters;
|
|
67
75
|
private activeTurn?;
|
|
68
76
|
private constructor();
|
|
69
77
|
static start(options: AcpSessionOptions): Promise<AcpSession>;
|
|
@@ -76,6 +84,21 @@ export declare class AcpSession implements SessionHandle {
|
|
|
76
84
|
private recoverOpenPrompts;
|
|
77
85
|
isAlive(): boolean;
|
|
78
86
|
snapshot(): SessionSnapshot;
|
|
87
|
+
private toolCall;
|
|
88
|
+
private reserveTool;
|
|
89
|
+
private reservePermission;
|
|
90
|
+
private allowPermission;
|
|
91
|
+
private releasePermission;
|
|
92
|
+
private releaseTool;
|
|
93
|
+
private releaseToolIfIdle;
|
|
94
|
+
private releaseAllTools;
|
|
95
|
+
private waitForToolBoundary;
|
|
96
|
+
private recordAfterToolDelivery;
|
|
97
|
+
/**
|
|
98
|
+
* Monitor-only safe-boundary delivery. Steering is the interruption: this
|
|
99
|
+
* path never calls session/cancel and never resolves a pending permission.
|
|
100
|
+
*/
|
|
101
|
+
submitPromptAfterTool(text: string, options?: SubmitPromptOptions): Promise<TurnResult>;
|
|
79
102
|
/**
|
|
80
103
|
* Accept responsibility for a prompt, then return. The turn itself may run
|
|
81
104
|
* for minutes behind other queued turns; making an interactive caller wait
|