@ours.network/fleet 1.1.5 → 1.2.0-nightly.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/README.md +163 -21
  2. package/dist/agent-ours/bridge.d.ts +3 -0
  3. package/dist/agent-ours/bridge.js +178 -0
  4. package/dist/agent-ours/controller.d.ts +14 -0
  5. package/dist/agent-ours/controller.js +60 -0
  6. package/dist/agent-ours/file-rpc.d.ts +23 -0
  7. package/dist/agent-ours/file-rpc.js +123 -0
  8. package/dist/agent-ours/harness.d.ts +14 -0
  9. package/dist/agent-ours/harness.js +64 -0
  10. package/dist/agent-ours/mcp-endpoint.d.ts +15 -0
  11. package/dist/agent-ours/mcp-endpoint.js +111 -0
  12. package/dist/agent-ours/runtime.d.ts +53 -0
  13. package/dist/agent-ours/runtime.js +247 -0
  14. package/dist/agent-ours/service.d.ts +23 -0
  15. package/dist/agent-ours/service.js +364 -0
  16. package/dist/agent-ours/state.d.ts +45 -0
  17. package/dist/agent-ours/state.js +95 -0
  18. package/dist/agent-ours/wire.d.ts +14 -0
  19. package/dist/agent-ours/wire.js +55 -0
  20. package/dist/agent-recovery-gate.js +1 -1
  21. package/dist/application/legacy-supervisor-identity.d.ts +7 -0
  22. package/dist/application/legacy-supervisor-identity.js +43 -0
  23. package/dist/application/supervisor-ours-tools.d.ts +159 -0
  24. package/dist/application/supervisor-ours-tools.js +121 -0
  25. package/dist/application/task-room-service.js +55 -19
  26. package/dist/briefing.js +10 -59
  27. package/dist/build-info.json +6 -5
  28. package/dist/capabilities.d.ts +2 -1
  29. package/dist/capabilities.js +2 -0
  30. package/dist/cli.js +75 -8
  31. package/dist/client-profile.d.ts +18 -0
  32. package/dist/client-profile.js +130 -0
  33. package/dist/config.d.ts +1 -0
  34. package/dist/creation.d.ts +1 -1
  35. package/dist/creation.js +13 -21
  36. package/dist/daemon-recovery.d.ts +2 -0
  37. package/dist/daemon-recovery.js +53 -2
  38. package/dist/docs.d.ts +3 -3
  39. package/dist/docs.js +32 -23
  40. package/dist/doctor.d.ts +1 -1
  41. package/dist/doctor.js +70 -14
  42. package/dist/fleet-command-audit.js +3 -2
  43. package/dist/harness/agent-session.d.ts +2 -0
  44. package/dist/harness/claude-code-session.js +4 -2
  45. package/dist/harness/codex-session.js +9 -2
  46. package/dist/harness/codex.d.ts +3 -2
  47. package/dist/harness/codex.js +8 -3
  48. package/dist/harness/hermes-session.js +7 -1
  49. package/dist/init-wizard.d.ts +11 -6
  50. package/dist/init-wizard.js +33 -0
  51. package/dist/monitor.d.ts +34 -3
  52. package/dist/monitor.js +174 -74
  53. package/dist/owner-channel/channel.d.ts +3 -0
  54. package/dist/owner-channel/channel.js +28 -6
  55. package/dist/owner-channel/ours-client.d.ts +12 -5
  56. package/dist/owner-channel/ours-client.js +48 -12
  57. package/dist/rooms-tasks/archived-absence.d.ts +8 -0
  58. package/dist/rooms-tasks/archived-absence.js +48 -0
  59. package/dist/rooms-tasks/cli.js +1 -1
  60. package/dist/rooms-tasks/close.d.ts +3 -0
  61. package/dist/rooms-tasks/close.js +95 -17
  62. package/dist/rooms-tasks/cowork-adapter.d.ts +4 -0
  63. package/dist/rooms-tasks/cowork-adapter.js +10 -2
  64. package/dist/rooms-tasks/cowork-http.d.ts +3 -0
  65. package/dist/rooms-tasks/cowork-http.js +70 -0
  66. package/dist/rooms-tasks/deletion.js +40 -3
  67. package/dist/rooms-tasks/member-startup.d.ts +1 -0
  68. package/dist/rooms-tasks/member-startup.js +2 -0
  69. package/dist/rooms-tasks/provision.d.ts +11 -0
  70. package/dist/rooms-tasks/provision.js +138 -26
  71. package/dist/rooms-tasks/room-state.js +13 -0
  72. package/dist/rooms-tasks/task-state.d.ts +13 -3
  73. package/dist/rooms-tasks/task-state.js +42 -6
  74. package/dist/rooms-tasks/types.d.ts +12 -0
  75. package/dist/rooms-tasks/workspace-artifacts.d.ts +6 -0
  76. package/dist/rooms-tasks/workspace-artifacts.js +93 -0
  77. package/dist/rooms-tasks/workspace.d.ts +26 -0
  78. package/dist/rooms-tasks/workspace.js +195 -0
  79. package/dist/runner.d.ts +14 -0
  80. package/dist/runner.js +540 -494
  81. package/dist/session/codex-app-server-transport.js +1 -1
  82. package/dist/session/codex-app-server.js +31 -5
  83. package/dist/spawn.js +19 -16
  84. package/dist/startup-readiness.d.ts +13 -0
  85. package/dist/startup-readiness.js +38 -0
  86. package/dist/supervisor/launchd.js +8 -1
  87. package/dist/supervisor/systemd.js +5 -0
  88. package/dist/temp-supervisor-recovery.d.ts +3 -0
  89. package/dist/temp-supervisor-recovery.js +46 -0
  90. package/dist/watchdog/briefing.js +3 -16
  91. package/dist/watchdog/run.js +19 -5
  92. package/dist/web/server.d.ts +2 -0
  93. package/dist/web/server.js +18 -1
  94. package/docs/task-workspaces.md +98 -0
  95. package/package.json +8 -4
  96. package/presets/fleet/roles/Coordinator.yaml +22 -0
package/README.md CHANGED
@@ -52,11 +52,10 @@ briefing.md per role ──► agent session adapter ──► native app-se
52
52
  ```
53
53
 
54
54
  Each role gets a state dir (`~/.ours-fleet/agents/<Name>/`) holding its briefing,
55
- logs, routines, and session markers. On boot the agent reads its briefing: bind
56
- identity, publish bio/persona, announce to its coordinator, work — while the
55
+ logs, routines, and session markers. Before boot, the supervisor binds the assigned identity and prepares its room.
56
+ The agent then reads its briefing and starts work — while the
57
57
  supervisor delivers its mail wakes as `[fleet-monitor]` console lines (see
58
- [Mail monitor](#mail-monitor)). Set `monitor.mode: native` when the harness
59
- should own mail wake instead. On crash the supervisor relaunches it and the
58
+ [Mail monitor](#mail-monitor)). Managed ours sessions use supervisor-owned mail wakes. On crash the supervisor relaunches it and the
60
59
  harness resumes the same session.
61
60
 
62
61
  The state dir contract:
@@ -80,7 +79,7 @@ The state dir contract:
80
79
  |---|---|---|
81
80
  | Node ≥ 22 | runs `ours-fleet` and its maintained adapters | nodejs.org, `apt`, or `brew` |
82
81
  | a harness CLI, logged in | the agent itself | e.g. Claude Code (`claude`) or Codex CLI (`codex`) |
83
- | `ours` CLI + shared daemon | identity + agent-to-agent messaging | `npm i -g @ours.network/cli && ours daemon start` |
82
+ | `ours` CLI + shared daemon | identity + agent-to-agent messaging | `npm i -g @ours.network/cli@2.8.1-nightly.7 @ours.network/daemon@3.8.1-nightly.1 && ours-daemon start` |
84
83
 
85
84
  Linux only: `ours-fleet init` enables *linger* so roles run without a login session
86
85
  and survive reboots. macOS: launchd agents start **at login** (no linger
@@ -231,15 +230,11 @@ from the current runner generation and excludes adapter `session/load` replay;
231
230
  those replay events remain in the durable ledger with `agent_replay` provenance
232
231
  for diagnosis and recovery rather than appearing as current work.
233
232
 
234
- For every temporary role, the first-boot instructions call ours MCP
235
- `create_temporary_identity` with the exact assigned name, so the new identity
236
- is owned and cleaned up by that connector session lifecycle. Temporary roles
237
- never bind pre-existing identities or fall back to permanent `create_identity`.
238
- Fleet does not inspect, preserve, or provision an ours identity for temp spawn.
239
- A collision, missing tool, or creation error stops for operator review; fleet
240
- never force-adopts or deletes identity state. Permanent roles are
241
- provisioned by fleet before launch and never delegate normal identity creation
242
- to the harness.
233
+ The Fleet supervisor creates and binds the assigned identity before the harness
234
+ starts. Temporary identities survive harness reconnects and are released only when
235
+ the logical run ends. Permanent identities remain available after the supervisor
236
+ releases ownership. Agents receive ready tools; no model-owned identity bootstrap
237
+ or onboarding skill is required.
243
238
 
244
239
  The web console uses structured ACP activity rather than exposing a raw agent terminal.
245
240
 
@@ -510,6 +505,23 @@ obsolete, presence-sensitive `OURS_AUTOSTART` variable from ACP harness
510
505
  processes. `ours-mcp proxy` is client-only and never starts a daemon; operators and explicit
511
506
  installer/setup flows remain responsible for starting it.
512
507
 
508
+ Fleet selects explicit `OURS_CONFIG` first, otherwise the managed
509
+ `~/.ours-client/profile.json` when present. With a managed profile, ordinary
510
+ `ours-fleet doctor` and `ours-fleet up` need no environment override. Invalid or
511
+ unreadable managed profiles fail rather than select another daemon. If neither
512
+ selection exists, the existing unmanaged behavior remains.
513
+
514
+ The private profile names the daemon endpoint, retained instance UUID and host
515
+ credential path; Fleet does not need the daemon state directory. An explicit
516
+ override selects a new invocation without rewriting already installed service
517
+ definitions. To select another profile explicitly:
518
+
519
+ ```sh
520
+ export OURS_CONFIG=/absolute/private/ours/client.json
521
+ ours-fleet doctor
522
+ ours-fleet up
523
+ ```
524
+
513
525
  ### Rooms and tasks
514
526
 
515
527
  Init installs editable `single`, `pair`, and `team` definitions under
@@ -548,7 +560,26 @@ N, Enter, Escape, Ctrl-C, Ctrl-D, or EOF cancels. In a picker, Escape, Ctrl-C, C
548
560
  or EOF cancels; Enter records the highlighted choice (or continues a non-empty multi-select),
549
561
  N is ignored, and an empty subscription selection remains blocked. Every cancellation
550
562
  before the final Yes performs no host setup or config publication. Non-TTY use stops with
551
- the same zero-mutation guarantee. After the final Yes, host integration runs before locked
563
+ the same zero-mutation guarantee unless `--settings PATH` supplies complete JSON answers.
564
+ Prepared settings run without questions and use this strict shape (catalog model capabilities
565
+ must match exactly):
566
+
567
+ ```json
568
+ {
569
+ "subscriptions": ["codex"],
570
+ "assignmentStrategy": "one-model",
571
+ "models": {
572
+ "development": { "harness": "codex", "session": "acp", "model": "gpt-5.6-sol", "efforts": ["low", "medium", "high", "xhigh", "max", "ultra"] },
573
+ "review": { "harness": "codex", "session": "acp", "model": "gpt-5.6-sol", "efforts": ["low", "medium", "high", "xhigh", "max", "ultra"] },
574
+ "coordination": { "harness": "codex", "session": "acp", "model": "gpt-5.6-sol", "efforts": ["low", "medium", "high", "xhigh", "max", "ultra"] }
575
+ },
576
+ "reasoning": "balanced"
577
+ }
578
+ ```
579
+
580
+ Run `ours-fleet init --settings settings.json -c /path/custom.yaml`. Unreadable,
581
+ malformed, incomplete, extra, or unsupported values fail before host setup and publication;
582
+ Fleet never falls back to questions. After the final Yes or validated settings, host integration runs before locked
552
583
  publication; a hard kill, power loss, or host crash can therefore leave host integration
553
584
  or private stage/recovery evidence to inspect.
554
585
  Existing targets and their tree must be owner-private regular files/directories on the
@@ -613,10 +644,10 @@ originating task. Existing active rooms are not retroactively migrated, and room
613
644
  with Owner attachment disabled receive no Owner grant.
614
645
 
615
646
  Fleet launches each template member with a dedicated one-time Cowork invite.
616
- The generated temporary-agent briefing contains the exact identity name, invite,
617
- Cowork role, and task. The agent creates that identity itself with ours MCP
618
- `create_temporary_identity`, accepts the invite with `add_contact`, and starts
619
- work immediately. A room is ready only after the Cowork room (and its task, when
647
+ The supervisor accepts the startup invite and verifies the provisioned identity CID
648
+ in the authenticated room seat before starting the model. The generated briefing
649
+ contains the assigned identity, room, role and task, without invite material.
650
+ A room is ready only after the Cowork room (and its task, when
620
651
  task-bound) is durably active, every configured member seat is authenticated and active with its matching
621
652
  live Fleet launch, and the configured Owner seat is active when owner attachment is
622
653
  enabled. There is no briefing hash, startup ACK, or separate role-briefing readiness
@@ -639,8 +670,8 @@ Set `room.anonymous: true` on a room template, or pass `--anonymous` to
639
670
  `task create`, `task start`, `task work`, or `room create`, to create an
640
671
  anonymous Cowork room. `--no-anonymous` explicitly overrides an anonymous
641
672
  template. Fleet records the resolved value before room creation so retries keep
642
- the same choice. Temporary members of an anonymous room are instructed to call
643
- `create_temporary_identity` with `expose_local=false`. Their generated briefings
673
+ the same choice. The supervisor creates anonymous room members with local contact-book exposure
674
+ disabled. Their generated briefings
644
675
  do not disclose or compare an Owner participant CID. A participant-originated
645
676
  instruction has Owner authority only when the authenticated Cowork room envelope
646
677
  attributes that participant seat the exact `Owner` role. Literal text, display
@@ -1610,3 +1641,114 @@ any interpreter symlink path. Declare these installation-specific paths in
1610
1641
  `isolation.fs.read`; do not expose an entire operator home. The tested policy
1611
1642
  allowed a project write and refused a write to an outside read-only directory.
1612
1643
  The `broker` network mode retains host networking; it is not network isolation.
1644
+
1645
+ ## Container-daemon recovery checks
1646
+
1647
+ After building Fleet with the selected SDK and CLI artifacts installed, run
1648
+ `npm run test:v1-recovery` in an isolated Docker container with external networking
1649
+ disabled. The existing test covers retry and terminal failure cases using a
1650
+ temporary daemon and no authenticated AI harness. By default both daemon and
1651
+ token-update commands use `node_modules/@ours.network/daemon/dist/cli.js`;
1652
+ `FLEET_DAEMON_CLI` and `FLEET_TOKEN_CLI` can select explicitly prepared CLI entries.
1653
+ These package checks do not replace native Fleet/harness or platform acceptance.
1654
+
1655
+ ### Build with selected SDK and CLI archives
1656
+
1657
+ Run in the build container with Node 22+, npm and tar available:
1658
+
1659
+ ```sh
1660
+ node scripts/build-selected.mjs --sdk /artifacts/ours.network-sdk-3.7.2.tgz --cli /artifacts/ours.network-cli-2.7.2.tgz --out-dir /artifacts/consumer
1661
+ ```
1662
+
1663
+ The recipe validates package names, installs and builds in disposable staging,
1664
+ then writes one complete portable npm archive. Stdout is a JSON object with its
1665
+ actual `filename`; build/npm logs go to stderr. Normal source manifests, locks
1666
+ and installed dependencies are preserved. The installer must install the same
1667
+ selected SDK and CLI archives alongside this package; its final dependency
1668
+ versions come from those archives. Existing bundling choices are unchanged.
1669
+
1670
+ Focused build/install verification (two real builds, including changed bytes
1671
+ under identical input names and versions, plus a missing-vendor negative check):
1672
+
1673
+ ```sh
1674
+ node scripts/check-build-selected.mjs --sdk /artifacts/ours.network-sdk-3.7.2.tgz --cli /artifacts/ours.network-cli-2.7.2.tgz
1675
+ ```
1676
+
1677
+ ### Remote clients over HTTPS
1678
+
1679
+ A client may select an `https://` origin backed by a TLS reverse proxy. Keep the
1680
+ daemon listener private and configure the proxy separately with a certificate
1681
+ trusted by the client and matching the hostname. Node uses its normal trust
1682
+ store; a private CA may be supplied through `NODE_EXTRA_CA_CERTS` before starting
1683
+ the client. Certificate verification must remain enabled.
1684
+
1685
+ The existing issued client credential works over either transport; changing the
1686
+ URL scheme does not require a new credential. Keep the server's master on the
1687
+ server. Forward `x-ours-api-token` and the `x-ours-*` session headers unchanged,
1688
+ and support streamed request/response bodies and long polling. Client requests
1689
+ refuse redirects, including HTTPS-to-HTTP redirects: configure the final HTTPS
1690
+ origin directly. UUID/capability checks still precede credential-bearing calls.
1691
+
1692
+ This adds client HTTPS support, not an HTTPS daemon listener, certificate
1693
+ provisioning or automatic reverse-proxy configuration. Existing local HTTP and
1694
+ SSH-tunnel profiles continue to work.
1695
+
1696
+ See [supervisor-owned ours and migration](docs/supervisor-ours.md) for the runtime contract and review build instructions.
1697
+
1698
+ ### One-server gateway profile
1699
+
1700
+ A private client profile may include `serverUrl`, for example
1701
+ `https://ours.example/base`, together with `endpoint` set to
1702
+ `https://ours.example/base/daemon`, the existing `expectedInstanceId`, and the
1703
+ absolute private `credentialPath`. Fleet preserves that daemon prefix for doctor
1704
+ and monitoring and derives `/cowork/management/rpc` from `serverUrl` for room
1705
+ management. Requests use the issued server credential, reject redirects, have
1706
+ bounded bodies/deadlines, and never replay mutations. This capability is declared
1707
+ as `cowork.http-management-v1` in the built artifact.
1708
+
1709
+ Explicit Cowork socket, config or state-directory overrides continue to select
1710
+ local Unix management. Profiles without `serverUrl` retain legacy behavior. The
1711
+ HTTP API grants operator room authority; use the installer's supported private
1712
+ or authenticated external gateway entry, and keep its backend ports private.
1713
+
1714
+ ### Agent ours tools through the supervisor
1715
+
1716
+ Operators can invoke the running agent's ordinary ours MCP tools without sending
1717
+ an LLM prompt or binding another session to its identity:
1718
+
1719
+ ```sh
1720
+ ours-fleet ours tools Critic1
1721
+ ours-fleet ours call Critic1 current_identity
1722
+ ours-fleet ours call Critic1 list_contacts
1723
+ ours-fleet ours call Critic1 generate_invite --args-file /private/invite-options.json
1724
+ ours-fleet ours call Critic1 add_contact --args-file /private/contact.json
1725
+ ```
1726
+
1727
+ `tools` returns the managed MCP tool names and argument schemas, plus the selected
1728
+ agent's identity name, CID, and supervisor generation. `call` returns the same
1729
+ identity metadata and the MCP result. The private JSON file contains the tool's
1730
+ arguments (for example `{"invite":"…"}` for `add_contact`); omitted arguments mean
1731
+ `{}`. Protect files containing invites and protect command output containing a
1732
+ new invite. Tool errors set a nonzero CLI exit status.
1733
+
1734
+ The authenticated REST equivalents are `GET /api/v1/roles/:id/ours/tools` and
1735
+ `POST /api/v1/roles/:id/ours/call`, with the normal Fleet session and CSRF token.
1736
+ The POST body is `{"tool":"list_contacts","arguments":{}}`. MCP tool errors
1737
+ remain in `result.isError`; transport failures use the normal Fleet error envelope.
1738
+ Request arguments and results are excluded from the Fleet audit log.
1739
+
1740
+ Both interfaces use the supervisor's existing fixed-identity MCP server and tool
1741
+ policy. Identity creation, removal, switching, and binding are not exposed. The
1742
+ supervisor must be running. Older descriptors are supported only when a unique
1743
+ existing runtime journal, instance record, and identity pin prove the selected
1744
+ agent and generation, and the same MCP connection confirms its identity and
1745
+ lifetime before the operation. This reads existing state without rewriting the
1746
+ descriptor or restarting the agent. Missing, ambiguous, stale, or mismatched
1747
+ proofs fail closed. Unknown identity descriptions also fail closed. Closing an operator call
1748
+ retains the supervisor's identity. An interrupted mutation can have an unknown
1749
+ outcome: inspect state before retrying, because there is no automatic retry or
1750
+ exactly-once guarantee. Contact acceptance alone does not prove peer verification
1751
+ or message delivery. File tools resolve paths in the invoking CLI process or
1752
+ Fleet web server's filesystem context, with that process's access permissions.
1753
+
1754
+ Task workspace ownership, retention, safe deletion and legacy migration are documented in [Task workspaces](docs/task-workspaces.md).
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ /** Runs inside exactly the harness OS sandbox. Only this process opens agent paths. */
3
+ export declare function runBridge(descriptorPath: string): Promise<void>;
@@ -0,0 +1,178 @@
1
+ #!/usr/bin/env node
2
+ import { createHash } from 'node:crypto';
3
+ import { connect } from 'node:net';
4
+ import { access, mkdir, open, readFile } from 'node:fs/promises';
5
+ import { constants } from 'node:fs';
6
+ import { basename, dirname, resolve } from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+ import { MAX_CHUNK_BYTES, MAX_FRAME_BYTES, Wire } from './wire.js';
9
+ /** Runs inside exactly the harness OS sandbox. Only this process opens agent paths. */
10
+ export async function runBridge(descriptorPath) {
11
+ const descriptor = JSON.parse(await readFile(descriptorPath, 'utf8'));
12
+ if (typeof descriptor.socket !== 'string' ||
13
+ typeof descriptor.capability !== 'string' ||
14
+ !Number.isInteger(descriptor.generation))
15
+ throw Error('INVALID_BRIDGE_DESCRIPTOR');
16
+ if (process.env.FLEET_OURS_BRIDGE_EXPECTED) {
17
+ const expected = JSON.parse(process.env.FLEET_OURS_BRIDGE_EXPECTED);
18
+ const digest = createHash('sha256').update(JSON.stringify([descriptor.socket, descriptor.capability, descriptor.generation])).digest('hex');
19
+ if (expected.transportDigest !== digest)
20
+ throw Error('SUPERVISOR_TRANSPORT_CHANGED');
21
+ if (['role', 'identity', 'cid', 'generation'].some(key => descriptor[key] !== expected[key]))
22
+ throw Error('SUPERVISOR_SELECTION_CHANGED');
23
+ }
24
+ const wire = new Wire(connect(descriptor.socket));
25
+ const handles = new Map();
26
+ const cleanup = async () => {
27
+ for (const row of handles.values())
28
+ await row.file.close().catch(() => { });
29
+ handles.clear();
30
+ };
31
+ let admitted = false;
32
+ let stdin = Buffer.alloc(0);
33
+ let chain = Promise.resolve();
34
+ const fail = () => {
35
+ wire.close();
36
+ process.stdin.pause();
37
+ void cleanup();
38
+ };
39
+ wire.onClose = fail;
40
+ wire.onFrame = (raw) => {
41
+ if (raw?.kind === 'ready') {
42
+ if (admitted)
43
+ throw Error('DUPLICATE_READY');
44
+ admitted = true;
45
+ process.stdin.resume();
46
+ return;
47
+ }
48
+ if (!admitted)
49
+ throw Error('BRIDGE_NOT_READY');
50
+ if (raw?.kind === 'mcp') {
51
+ const line = JSON.stringify(raw.value) + '\n';
52
+ if (!process.stdout.write(line)) {
53
+ wire.socket.pause();
54
+ process.stdout.once('drain', () => wire.socket.resume());
55
+ }
56
+ return;
57
+ }
58
+ if (raw?.kind !== 'fs' || typeof raw.id !== 'string' || typeof raw.call !== 'string')
59
+ throw Error('INVALID_BRIDGE_FRAME');
60
+ chain = chain
61
+ .then(async () => {
62
+ try {
63
+ const { op, args, call } = raw;
64
+ let value;
65
+ if (op === 'probe') {
66
+ try {
67
+ await access(args.path, constants.R_OK);
68
+ value = true;
69
+ }
70
+ catch {
71
+ value = false;
72
+ }
73
+ }
74
+ else if (op === 'read-open' || op === 'write-open') {
75
+ if (handles.size >= 32 ||
76
+ typeof args.handle !== 'string' ||
77
+ handles.has(args.handle) ||
78
+ typeof args.path !== 'string')
79
+ throw Error('FILE_LIMIT');
80
+ const path = resolve(args.path);
81
+ if (op === 'write-open')
82
+ await mkdir(dirname(path), { recursive: true });
83
+ const file = await open(path, op === 'write-open' ? 'w' : 'r');
84
+ handles.set(args.handle, { file, call, write: op === 'write-open' });
85
+ value =
86
+ op === 'write-open'
87
+ ? path
88
+ : { filename: basename(path), size: (await file.stat()).size };
89
+ }
90
+ else {
91
+ const row = handles.get(args.handle);
92
+ if (!row) {
93
+ if (op === 'close') {
94
+ value = null;
95
+ }
96
+ else
97
+ throw Error('UNKNOWN_FILE_HANDLE');
98
+ }
99
+ else {
100
+ if (row.call !== call)
101
+ throw Error('FOREIGN_FILE_CALL');
102
+ if (op === 'close') {
103
+ handles.delete(args.handle);
104
+ await row.file.close();
105
+ value = null;
106
+ }
107
+ else if (op === 'read' && !row.write) {
108
+ const buffer = Buffer.alloc(MAX_CHUNK_BYTES);
109
+ const { bytesRead } = await row.file.read(buffer);
110
+ value = bytesRead ? buffer.subarray(0, bytesRead).toString('base64') : null;
111
+ }
112
+ else if (op === 'write' && row.write) {
113
+ if (typeof args.data !== 'string' ||
114
+ args.data.length > Math.ceil(MAX_CHUNK_BYTES / 3) * 4)
115
+ throw Error('FILE_CHUNK_LIMIT');
116
+ const buffer = Buffer.from(args.data, 'base64');
117
+ let offset = 0;
118
+ while (offset < buffer.length) {
119
+ const r = await row.file.write(buffer, offset, buffer.length - offset);
120
+ if (!r.bytesWritten)
121
+ throw Error('SHORT_WRITE');
122
+ offset += r.bytesWritten;
123
+ }
124
+ value = null;
125
+ }
126
+ else
127
+ throw Error('INVALID_FILE_OPERATION');
128
+ }
129
+ }
130
+ await wire.send({ kind: 'fs-result', id: raw.id, value });
131
+ }
132
+ catch {
133
+ await wire.send({ kind: 'fs-result', id: raw.id, error: 'FILE_OPERATION_FAILED' });
134
+ }
135
+ })
136
+ .catch(fail);
137
+ };
138
+ process.stdin.pause();
139
+ process.stdin.on('data', (chunk) => {
140
+ stdin = Buffer.concat([stdin, chunk]);
141
+ for (;;) {
142
+ const end = stdin.indexOf(10);
143
+ if (end < 0) {
144
+ if (stdin.length > MAX_FRAME_BYTES)
145
+ fail();
146
+ return;
147
+ }
148
+ if (end > MAX_FRAME_BYTES) {
149
+ fail();
150
+ return;
151
+ }
152
+ const line = stdin.subarray(0, end);
153
+ stdin = stdin.subarray(end + 1);
154
+ try {
155
+ const value = JSON.parse(line.toString('utf8'));
156
+ process.stdin.pause();
157
+ void wire.send({ kind: 'mcp', value }).then(() => process.stdin.resume(), fail);
158
+ }
159
+ catch {
160
+ fail();
161
+ return;
162
+ }
163
+ }
164
+ });
165
+ process.stdin.on('end', fail);
166
+ process.stdout.on('error', fail);
167
+ await wire.send({
168
+ kind: 'hello',
169
+ capability: descriptor.capability,
170
+ generation: descriptor.generation,
171
+ });
172
+ }
173
+ if (process.argv[1] === fileURLToPath(import.meta.url)) {
174
+ runBridge(process.env.FLEET_OURS_BRIDGE_DESCRIPTOR ?? '').catch(() => {
175
+ process.stderr.write('Fleet ours bridge unavailable\n');
176
+ process.exitCode = 1;
177
+ });
178
+ }
@@ -0,0 +1,14 @@
1
+ import { RuntimeJournal } from './state.js';
2
+ /** Reuse Fleet's process-lifetime binder to serialize startup and cleanup. */
3
+ export declare class RuntimeController {
4
+ private readonly lease;
5
+ readonly privateDir: string;
6
+ readonly journal: RuntimeJournal;
7
+ private active;
8
+ private constructor();
9
+ static acquire(root: string, daemon: string, name: string, instance: string): Promise<RuntimeController>;
10
+ assertFence: () => void;
11
+ successor(): string;
12
+ resumeGeneration(): number;
13
+ unlock(): void;
14
+ }
@@ -0,0 +1,60 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { mkdirSync, renameSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { acquireOwnerBinderLease } from '../owner-channel/binder.js';
5
+ import { binderKey, RuntimeJournal } from './state.js';
6
+ /** Reuse Fleet's process-lifetime binder to serialize startup and cleanup. */
7
+ export class RuntimeController {
8
+ lease;
9
+ privateDir;
10
+ journal;
11
+ active = true;
12
+ constructor(lease, privateDir) {
13
+ this.lease = lease;
14
+ this.privateDir = privateDir;
15
+ this.journal = new RuntimeJournal(privateDir);
16
+ }
17
+ static async acquire(root, daemon, name, instance) {
18
+ const dir = join(root, binderKey(daemon, name));
19
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
20
+ const lease = await acquireOwnerBinderLease(dir, 'agent-ours', name);
21
+ const controller = new RuntimeController(lease, dir);
22
+ if (controller.journal.read()?.instance && controller.journal.read().instance !== instance) {
23
+ lease.release();
24
+ throw Error('RUNTIME_INSTANCE_MISMATCH');
25
+ }
26
+ return controller;
27
+ }
28
+ assertFence = () => {
29
+ if (!this.active)
30
+ throw Error('STALE_RUNTIME_CONTROLLER');
31
+ };
32
+ successor() {
33
+ this.assertFence();
34
+ const state = this.journal.read();
35
+ if (state?.phase !== 'RELEASED')
36
+ throw Error('PREDECESSOR_NOT_RELEASED');
37
+ const archive = join(this.privateDir, `retired-${state.instance}`);
38
+ mkdirSync(archive, { mode: 0o700 });
39
+ for (const file of ['state.json', 'owner.json', 'instance.json'])
40
+ renameSync(join(this.privateDir, file), join(archive, file));
41
+ return randomUUID();
42
+ }
43
+ resumeGeneration() {
44
+ this.assertFence();
45
+ const s = this.journal.read();
46
+ if (!s)
47
+ return 1;
48
+ if (['TERMINAL_INTENT', 'CLEANUP_PENDING', 'RELEASED', 'FAILED'].includes(s.phase))
49
+ throw Error('RUNTIME_RETIRED_OR_FAILED');
50
+ if (!s.cid)
51
+ throw Error('UNCERTAIN_PROVISIONING');
52
+ this.journal.commit({ ...s, generation: s.generation + 1, phase: 'RECOVERING' }, s.revision);
53
+ return s.generation + 1;
54
+ }
55
+ unlock() {
56
+ this.assertFence();
57
+ this.lease.release();
58
+ this.active = false;
59
+ }
60
+ }
@@ -0,0 +1,23 @@
1
+ import type { FileExecutionContext } from '@ours.network/mcp/server';
2
+ import type { ToolRequestExtra } from '@ours.network/mcp/dist/types/mcp/tool.js';
3
+ import { type Wire } from './wire.js';
4
+ /** Call-scoped callbacks carry no daemon credentials or upload receipts. */
5
+ export declare class FileCallbacks implements FileExecutionContext {
6
+ private readonly wire;
7
+ private pending;
8
+ constructor(wire: Wire);
9
+ receive(frame: any): void;
10
+ close(): void;
11
+ private rpc;
12
+ canRead(path: string, extra: ToolRequestExtra): Promise<boolean>;
13
+ read(path: string, extra: ToolRequestExtra): Promise<{
14
+ filename: any;
15
+ size: any;
16
+ body: ReadableStream<Uint8Array<ArrayBufferLike>>;
17
+ close: () => Promise<void>;
18
+ }>;
19
+ write(path: string, body: ReadableStream<Uint8Array>, extra: ToolRequestExtra): Promise<{
20
+ path: string;
21
+ size: number;
22
+ }>;
23
+ }
@@ -0,0 +1,123 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { MAX_CHUNK_BYTES } from './wire.js';
3
+ /** Call-scoped callbacks carry no daemon credentials or upload receipts. */
4
+ export class FileCallbacks {
5
+ wire;
6
+ pending = new Map();
7
+ constructor(wire) {
8
+ this.wire = wire;
9
+ }
10
+ receive(frame) {
11
+ if (frame?.kind !== 'fs-result' || typeof frame.id !== 'string')
12
+ throw Error('INVALID_FILE_RESPONSE');
13
+ const p = this.pending.get(frame.id);
14
+ if (!p)
15
+ throw Error('UNSOLICITED_FILE_RESPONSE');
16
+ this.pending.delete(frame.id);
17
+ clearTimeout(p.timer);
18
+ if (frame.error)
19
+ p.reject(Error('AGENT_FILE_OPERATION_FAILED'));
20
+ else
21
+ p.resolve(frame.value);
22
+ }
23
+ close() {
24
+ for (const p of this.pending.values()) {
25
+ clearTimeout(p.timer);
26
+ p.reject(Error('FILE_BRIDGE_DISCONNECTED'));
27
+ }
28
+ this.pending.clear();
29
+ }
30
+ async rpc(call, op, args, signal) {
31
+ signal.throwIfAborted();
32
+ if (this.pending.size >= 64)
33
+ throw Error('FILE_CALLBACK_LIMIT');
34
+ const id = randomUUID();
35
+ return new Promise((resolve, reject) => {
36
+ const finish = (f) => (value) => {
37
+ signal.removeEventListener('abort', abort);
38
+ f(value);
39
+ };
40
+ const fail = (reason) => {
41
+ const p = this.pending.get(id);
42
+ if (!p)
43
+ return;
44
+ this.pending.delete(id);
45
+ clearTimeout(p.timer);
46
+ finish(reject)(reason);
47
+ };
48
+ const abort = () => fail(Error('FILE_CALLBACK_ABORTED'));
49
+ const timer = setTimeout(() => fail(Error('FILE_CALLBACK_TIMEOUT')), 30_000);
50
+ timer.unref();
51
+ this.pending.set(id, { resolve: finish(resolve), reject: finish(reject), timer });
52
+ signal.addEventListener('abort', abort, { once: true });
53
+ void this.wire
54
+ .send({ kind: 'fs', id, call, op, args })
55
+ .catch(() => fail(Error('FILE_BRIDGE_DISCONNECTED')));
56
+ });
57
+ }
58
+ async canRead(path, extra) {
59
+ return (await this.rpc(String(extra.requestId), 'probe', { path }, extra.signal)) === true;
60
+ }
61
+ async read(path, extra) {
62
+ const call = String(extra.requestId), handle = randomUUID();
63
+ const meta = await this.rpc(call, 'read-open', { path, handle }, extra.signal);
64
+ if (!meta ||
65
+ typeof meta.filename !== 'string' ||
66
+ !Number.isSafeInteger(meta.size) ||
67
+ meta.size < 0)
68
+ throw Error('INVALID_FILE_METADATA');
69
+ const close = async () => {
70
+ await this.rpc(call, 'close', { handle }, new AbortController().signal).catch(() => { });
71
+ };
72
+ const body = new ReadableStream({
73
+ pull: async (controller) => {
74
+ try {
75
+ const chunk = await this.rpc(call, 'read', { handle }, extra.signal);
76
+ if (chunk === null) {
77
+ controller.close();
78
+ return;
79
+ }
80
+ if (typeof chunk !== 'string' || chunk.length > Math.ceil(MAX_CHUNK_BYTES / 3) * 4)
81
+ throw Error('INVALID_FILE_CHUNK');
82
+ const bytes = Buffer.from(chunk, 'base64');
83
+ if (bytes.length > MAX_CHUNK_BYTES)
84
+ throw Error('FILE_CHUNK_TOO_LARGE');
85
+ controller.enqueue(bytes);
86
+ }
87
+ catch (error) {
88
+ controller.error(error);
89
+ await close();
90
+ }
91
+ },
92
+ cancel: close,
93
+ });
94
+ return { filename: meta.filename, size: meta.size, body, close };
95
+ }
96
+ async write(path, body, extra) {
97
+ const call = String(extra.requestId), handle = randomUUID();
98
+ const reader = body.getReader();
99
+ try {
100
+ const resolved = await this.rpc(call, 'write-open', { path, handle }, extra.signal);
101
+ if (typeof resolved !== 'string')
102
+ throw Error('INVALID_FILE_PATH');
103
+ let size = 0;
104
+ for (;;) {
105
+ extra.signal.throwIfAborted();
106
+ const chunk = await reader.read();
107
+ if (chunk.done)
108
+ break;
109
+ for (let offset = 0; offset < chunk.value.length; offset += MAX_CHUNK_BYTES) {
110
+ const bytes = chunk.value.subarray(offset, offset + MAX_CHUNK_BYTES);
111
+ size += bytes.length;
112
+ await this.rpc(call, 'write', { handle, data: Buffer.from(bytes).toString('base64') }, extra.signal);
113
+ }
114
+ }
115
+ await this.rpc(call, 'close', { handle }, extra.signal);
116
+ return { path: resolved, size };
117
+ }
118
+ finally {
119
+ await reader.cancel().catch(() => { });
120
+ await this.rpc(call, 'close', { handle }, new AbortController().signal).catch(() => { });
121
+ }
122
+ }
123
+ }
@@ -0,0 +1,14 @@
1
+ import type { AcpMcpServer } from '../harness/types.js';
2
+ import type { ResolvedRole } from '../config.js';
3
+ export interface ManagedHarnessOurs {
4
+ server: AcpMcpServer;
5
+ native: Record<string, unknown>;
6
+ }
7
+ /** Do not pass the supervisor's ours connection credentials to harness children. */
8
+ export declare function managedChildEnvironment(input: NodeJS.ProcessEnv): Record<string, string>;
9
+ /** Replace ours only; retain the user's ordinary harness configuration and plugins. */
10
+ export declare function prepareManagedHarness(role: ResolvedRole, stateDir: string, cwd: string, descriptor: string, env: Record<string, string>): {
11
+ ours: ManagedHarnessOurs;
12
+ env: Record<string, string>;
13
+ };
14
+ export declare function managedClaudeMeta(base: Record<string, unknown> | undefined): Record<string, unknown>;