@ours.network/fleet 1.2.0-nightly.2 → 1.2.0-nightly.20
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 +152 -29
- package/dist/agent-ours/bridge.d.ts +3 -0
- package/dist/agent-ours/bridge.js +178 -0
- package/dist/agent-ours/controller.d.ts +14 -0
- package/dist/agent-ours/controller.js +60 -0
- package/dist/agent-ours/correspondence.d.ts +10 -0
- package/dist/agent-ours/correspondence.js +1 -0
- package/dist/agent-ours/file-rpc.d.ts +23 -0
- package/dist/agent-ours/file-rpc.js +123 -0
- package/dist/agent-ours/harness.d.ts +14 -0
- package/dist/agent-ours/harness.js +72 -0
- package/dist/agent-ours/mcp-endpoint.d.ts +15 -0
- package/dist/agent-ours/mcp-endpoint.js +111 -0
- package/dist/agent-ours/runtime.d.ts +59 -0
- package/dist/agent-ours/runtime.js +283 -0
- package/dist/agent-ours/service.d.ts +23 -0
- package/dist/agent-ours/service.js +372 -0
- package/dist/agent-ours/state.d.ts +45 -0
- package/dist/agent-ours/state.js +95 -0
- package/dist/agent-ours/wire.d.ts +14 -0
- package/dist/agent-ours/wire.js +55 -0
- package/dist/agent-recovery-gate.js +1 -1
- package/dist/application/conversation-history.d.ts +6 -0
- package/dist/application/conversation-history.js +65 -0
- package/dist/application/fleet-query-service.d.ts +1 -1
- package/dist/application/fleet-query-service.js +2 -2
- package/dist/application/legacy-supervisor-identity.d.ts +7 -0
- package/dist/application/legacy-supervisor-identity.js +43 -0
- package/dist/application/model-catalog.d.ts +1 -1
- package/dist/application/model-catalog.js +5 -2
- package/dist/application/preset-provenance.d.ts +19 -0
- package/dist/application/preset-provenance.js +59 -0
- package/dist/application/room-layout-definitions.d.ts +45 -0
- package/dist/application/room-layout-definitions.js +130 -0
- package/dist/application/session-control.d.ts +8 -0
- package/dist/application/session-control.js +21 -3
- package/dist/application/supervisor-ours-tools.d.ts +159 -0
- package/dist/application/supervisor-ours-tools.js +113 -0
- package/dist/application/task-layouts.d.ts +89 -0
- package/dist/application/task-layouts.js +248 -0
- package/dist/application/task-room-service.d.ts +99 -3
- package/dist/application/task-room-service.js +296 -31
- package/dist/atomic-file.d.ts +5 -0
- package/dist/atomic-file.js +62 -1
- package/dist/briefing.js +10 -59
- package/dist/build-info.json +5 -4
- package/dist/capabilities.d.ts +2 -1
- package/dist/capabilities.js +2 -0
- package/dist/cli.js +65 -2
- package/dist/client-profile.d.ts +5 -16
- package/dist/client-profile.js +4 -112
- package/dist/config.d.ts +4 -0
- package/dist/config.js +7 -0
- package/dist/creation.d.ts +1 -1
- package/dist/creation.js +5 -16
- package/dist/daemon-recovery.d.ts +1 -8
- package/dist/daemon-recovery.js +8 -113
- package/dist/docs.d.ts +3 -3
- package/dist/docs.js +43 -23
- package/dist/doctor.d.ts +1 -1
- package/dist/doctor.js +46 -59
- package/dist/erased-resources.d.ts +9 -0
- package/dist/erased-resources.js +87 -0
- package/dist/fleet-command-audit.d.ts +1 -0
- package/dist/fleet-command-audit.js +12 -7
- package/dist/harness/agent-session.d.ts +2 -0
- package/dist/harness/claude-code-session.js +4 -2
- package/dist/harness/codex-app-server-proxy.d.ts +4 -1
- package/dist/harness/codex-app-server-proxy.js +41 -5
- package/dist/harness/codex-session.js +9 -2
- package/dist/harness/codex.d.ts +3 -2
- package/dist/harness/codex.js +4 -3
- package/dist/harness/hermes-session.js +7 -1
- package/dist/init-wizard.js +19 -9
- package/dist/monitor.d.ts +1 -34
- package/dist/monitor.js +24 -151
- package/dist/owner-channel/channel.d.ts +3 -0
- package/dist/owner-channel/channel.js +34 -6
- package/dist/owner-channel/commands.d.ts +4 -0
- package/dist/owner-channel/commands.js +8 -0
- package/dist/owner-channel/lifecycle-outbox.js +4 -3
- package/dist/owner-channel/ours-client.js +2 -17
- package/dist/preset-migration.js +15 -0
- package/dist/rooms-tasks/add-member.d.ts +48 -0
- package/dist/rooms-tasks/add-member.js +83 -0
- package/dist/rooms-tasks/archived-absence.d.ts +8 -0
- package/dist/rooms-tasks/archived-absence.js +48 -0
- package/dist/rooms-tasks/cli.js +34 -5
- package/dist/rooms-tasks/close.d.ts +5 -2
- package/dist/rooms-tasks/close.js +208 -28
- package/dist/rooms-tasks/config.js +3 -1
- package/dist/rooms-tasks/cowork-adapter.d.ts +6 -4
- package/dist/rooms-tasks/cowork-adapter.js +11 -117
- package/dist/rooms-tasks/cowork-http.d.ts +3 -0
- package/dist/rooms-tasks/cowork-http.js +70 -0
- package/dist/rooms-tasks/deletion.d.ts +1 -1
- package/dist/rooms-tasks/deletion.js +74 -5
- package/dist/rooms-tasks/erasure.d.ts +6 -0
- package/dist/rooms-tasks/erasure.js +169 -0
- package/dist/rooms-tasks/external-worker.d.ts +1 -1
- package/dist/rooms-tasks/external-worker.js +3 -4
- package/dist/rooms-tasks/layout-cli.d.ts +2 -0
- package/dist/rooms-tasks/layout-cli.js +81 -0
- package/dist/rooms-tasks/layout-config.d.ts +11 -0
- package/dist/rooms-tasks/layout-config.js +89 -0
- package/dist/rooms-tasks/layout-control.d.ts +21 -0
- package/dist/rooms-tasks/layout-control.js +52 -0
- package/dist/rooms-tasks/layout-service.d.ts +39 -0
- package/dist/rooms-tasks/layout-service.js +173 -0
- package/dist/rooms-tasks/layout.d.ts +102 -0
- package/dist/rooms-tasks/layout.js +227 -0
- package/dist/rooms-tasks/live-readiness.d.ts +18 -0
- package/dist/rooms-tasks/live-readiness.js +85 -0
- package/dist/rooms-tasks/member-startup.d.ts +1 -0
- package/dist/rooms-tasks/member-startup.js +2 -0
- package/dist/rooms-tasks/provision.d.ts +14 -0
- package/dist/rooms-tasks/provision.js +182 -48
- package/dist/rooms-tasks/room-state.d.ts +4 -2
- package/dist/rooms-tasks/room-state.js +53 -8
- package/dist/rooms-tasks/task-state.d.ts +42 -7
- package/dist/rooms-tasks/task-state.js +153 -23
- package/dist/rooms-tasks/types.d.ts +28 -0
- package/dist/rooms-tasks/workspace-artifacts.d.ts +6 -0
- package/dist/rooms-tasks/workspace-artifacts.js +93 -0
- package/dist/rooms-tasks/workspace.d.ts +26 -0
- package/dist/rooms-tasks/workspace.js +195 -0
- package/dist/runner.d.ts +23 -3
- package/dist/runner.js +574 -494
- package/dist/session/acp.d.ts +3 -0
- package/dist/session/acp.js +7 -0
- package/dist/session/arbiter.d.ts +3 -0
- package/dist/session/arbiter.js +20 -2
- package/dist/session/codex-app-server-transport.js +1 -1
- package/dist/session/codex-app-server.d.ts +3 -0
- package/dist/session/codex-app-server.js +25 -1
- package/dist/session/control.d.ts +10 -1
- package/dist/session/control.js +36 -2
- package/dist/session/conversation-normalizer.d.ts +2 -1
- package/dist/session/conversation-normalizer.js +1 -1
- package/dist/session/conversation-types.d.ts +5 -3
- package/dist/session/task-notice.d.ts +14 -0
- package/dist/session/task-notice.js +13 -0
- package/dist/session/types.d.ts +3 -0
- package/dist/socket-path.d.ts +2 -0
- package/dist/socket-path.js +22 -0
- package/dist/spawn.js +19 -16
- package/dist/startup-readiness.d.ts +13 -0
- package/dist/startup-readiness.js +38 -0
- package/dist/supervisor/launchd.js +2 -1
- package/dist/supervisor/systemd.js +9 -23
- package/dist/temp-idle.d.ts +17 -0
- package/dist/temp-idle.js +42 -0
- package/dist/temp-lifecycle.d.ts +6 -1
- package/dist/temp-lifecycle.js +29 -13
- package/dist/temp-supervisor-recovery.d.ts +3 -0
- package/dist/temp-supervisor-recovery.js +46 -0
- package/dist/watchdog/briefing.js +3 -16
- package/dist/watchdog/run.js +19 -5
- package/dist/web/agent-attachments.d.ts +29 -0
- package/dist/web/agent-attachments.js +162 -0
- package/dist/web/auth.d.ts +4 -1
- package/dist/web/auth.js +23 -2
- package/dist/web/control.js +2 -1
- package/dist/web/fleet-config-service.d.ts +4 -1
- package/dist/web/fleet-config-service.js +46 -15
- package/dist/web/prefix-gateway.d.ts +20 -0
- package/dist/web/prefix-gateway.js +150 -0
- package/dist/web/runtime.d.ts +4 -0
- package/dist/web/runtime.js +19 -14
- package/dist/web/server.d.ts +7 -0
- package/dist/web/server.js +293 -25
- package/docs/room-layouts.md +92 -0
- package/docs/task-workspaces.md +130 -0
- package/examples/room-layouts/fresh.yaml +27 -0
- package/examples/room-layouts/shared.yaml +19 -0
- package/package.json +17 -13
- package/presets/README.md +66 -0
- package/presets/brain-catalog.json +7 -2
- package/presets/fleet/agent_templates/Engineer.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-1-high.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-1-low.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-1-max.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-1-medium.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-1-xhigh.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-high.yaml +1 -1
- package/presets/fleet/brains/claude-fable-5-low.yaml +1 -1
- package/presets/fleet/brains/claude-fable-5-max.yaml +1 -1
- package/presets/fleet/brains/claude-fable-5-medium.yaml +1 -1
- package/presets/fleet/brains/claude-fable-5-xhigh.yaml +1 -1
- package/presets/fleet/brains/claude-haiku-4-5-20251001-default.yaml +4 -0
- package/presets/fleet/brains/claude-opus-5-5-high.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-5-low.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-5-max.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-5-medium.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-5-xhigh.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-high.yaml +1 -1
- package/presets/fleet/brains/claude-opus-5-low.yaml +1 -1
- package/presets/fleet/brains/claude-opus-5-max.yaml +1 -1
- package/presets/fleet/brains/claude-opus-5-medium.yaml +1 -1
- package/presets/fleet/brains/claude-opus-5-xhigh.yaml +1 -1
- package/presets/fleet/brains/claude-sonnet-5-high.yaml +1 -1
- package/presets/fleet/brains/claude-sonnet-5-low.yaml +1 -1
- package/presets/fleet/brains/claude-sonnet-5-max.yaml +1 -1
- package/presets/fleet/brains/claude-sonnet-5-medium.yaml +1 -1
- package/presets/fleet/brains/claude-sonnet-5-xhigh.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-3-codex-spark-high.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-3-codex-spark-low.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-3-codex-spark-medium.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-3-codex-spark-xhigh.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-4-high.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-4-low.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-4-medium.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-4-mini-high.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-4-mini-low.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-4-mini-medium.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-4-mini-xhigh.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-4-xhigh.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-5-high.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-5-low.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-5-medium.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-5-xhigh.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-luna-high.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-luna-low.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-luna-max.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-luna-medium.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-luna-xhigh.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-sol-high.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-sol-low.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-sol-max.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-sol-medium.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-sol-ultra.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-sol-xhigh.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-terra-high.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-terra-low.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-terra-max.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-terra-medium.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-terra-ultra.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-terra-xhigh.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-6-astra-high.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-6-astra-low.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-6-astra-max.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-6-astra-medium.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-6-astra-ultra.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-6-astra-xhigh.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-6-luna-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-luna-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-luna-max.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-luna-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-luna-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-sol-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-sol-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-sol-max.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-sol-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-sol-ultra.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-sol-xhigh.yaml +5 -0
- package/presets/fleet/roles/Coordinator.yaml +22 -0
- package/presets/fleet/roles/Engineer.yaml +40 -0
- package/presets/fleet/room_layouts/engineering.yaml +25 -0
- package/presets/fleet/room_layouts/pair.yaml +24 -0
- package/presets/fleet/room_layouts/single.yaml +17 -0
- package/presets/fleet/room_layouts/team.yaml +30 -0
- package/presets/fleet/room_templates/engineering.yaml +19 -0
- package/presets/manifest.json +2 -2
- package/dist/web-app/assets/index-59GF-gsZ.css +0 -1
- package/dist/web-app/assets/index-C0h3ALvs.js +0 -10
- package/dist/web-app/icons/ours-fleet-maskable.svg +0 -4
- package/dist/web-app/icons/ours-fleet.svg +0 -4
- package/dist/web-app/index.html +0 -17
- package/dist/web-app/manifest.webmanifest +0 -15
- package/dist/web-app/offline.html +0 -18
- package/dist/web-app/sw.js +0 -51
|
@@ -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>;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
/** Do not pass the supervisor's ours connection credentials to harness children. */
|
|
4
|
+
export function managedChildEnvironment(input) {
|
|
5
|
+
const out = {};
|
|
6
|
+
for (const [key, value] of Object.entries(input)) {
|
|
7
|
+
if (value === undefined || (/^OURS_/i.test(key) && !key.startsWith('OURS_FLEET_')))
|
|
8
|
+
continue;
|
|
9
|
+
out[key] = value;
|
|
10
|
+
}
|
|
11
|
+
out.FLEET_OURS_MANAGED = '1';
|
|
12
|
+
return out;
|
|
13
|
+
}
|
|
14
|
+
/** Replace ours only; retain the user's ordinary harness configuration and plugins. */
|
|
15
|
+
export function prepareManagedHarness(role, stateDir, cwd, descriptor, env) {
|
|
16
|
+
const bridge = fileURLToPath(new URL('./bridge.js', import.meta.url));
|
|
17
|
+
const server = {
|
|
18
|
+
name: 'ours',
|
|
19
|
+
command: process.execPath,
|
|
20
|
+
args: [bridge],
|
|
21
|
+
env: [{ name: 'FLEET_OURS_BRIDGE_DESCRIPTOR', value: descriptor }],
|
|
22
|
+
};
|
|
23
|
+
// Portable allow covers bound identity messaging, not all tools on the server.
|
|
24
|
+
const tools = role.harness === 'codex' && role.permissions?.approval === 'allow'
|
|
25
|
+
? Object.fromEntries([
|
|
26
|
+
'current_identity', 'get_messages', 'list_history', 'list_contacts', 'send_message',
|
|
27
|
+
].map(name => [name, { approval_mode: 'approve' }]))
|
|
28
|
+
: undefined;
|
|
29
|
+
const native = {
|
|
30
|
+
mcp_servers: {
|
|
31
|
+
ours: {
|
|
32
|
+
command: process.execPath,
|
|
33
|
+
args: [bridge],
|
|
34
|
+
env: { FLEET_OURS_BRIDGE_DESCRIPTOR: descriptor },
|
|
35
|
+
...(tools ? { tools } : {}),
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
plugins: { 'ours@ours-codex-marketplace': { enabled: false } },
|
|
39
|
+
};
|
|
40
|
+
const child = managedChildEnvironment({ ...process.env, ...env });
|
|
41
|
+
if (role.harness === 'codex') {
|
|
42
|
+
const config = child.CODEX_CONFIG ? JSON.parse(child.CODEX_CONFIG) : {};
|
|
43
|
+
child.CODEX_CONFIG = JSON.stringify({
|
|
44
|
+
...config,
|
|
45
|
+
mcp_servers: { ...config.mcp_servers, ...native.mcp_servers },
|
|
46
|
+
plugins: { ...config.plugins, ...native.plugins },
|
|
47
|
+
});
|
|
48
|
+
// codex-acp otherwise prefers a same-name connector from the user's config.
|
|
49
|
+
child.DISABLE_MCP_CONFIG_FILTERING = 'true';
|
|
50
|
+
}
|
|
51
|
+
return { ours: { server, native }, env: child };
|
|
52
|
+
}
|
|
53
|
+
export function managedClaudeMeta(base) {
|
|
54
|
+
const meta = base ?? {}, claude = (meta.claudeCode ?? {});
|
|
55
|
+
const options = (claude.options ?? {});
|
|
56
|
+
const settings = typeof options.settings === 'string'
|
|
57
|
+
? JSON.parse(readFileSync(options.settings, 'utf8'))
|
|
58
|
+
: (options.settings ?? {});
|
|
59
|
+
return {
|
|
60
|
+
...meta,
|
|
61
|
+
claudeCode: {
|
|
62
|
+
...claude,
|
|
63
|
+
options: {
|
|
64
|
+
...options,
|
|
65
|
+
settings: {
|
|
66
|
+
...settings,
|
|
67
|
+
enabledPlugins: { ...settings.enabledPlugins, 'ours@ours.network': false },
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ApplicationIdentityStore } from '@ours.network/mcp/application-identities';
|
|
2
|
+
import type { OursClient } from '@ours.network/sdk/client';
|
|
3
|
+
import type { AgentOursRuntime } from './runtime.js';
|
|
4
|
+
export interface EndpointOptions {
|
|
5
|
+
socket: string;
|
|
6
|
+
capability: string;
|
|
7
|
+
generation: number;
|
|
8
|
+
runtime: AgentOursRuntime;
|
|
9
|
+
client: OursClient;
|
|
10
|
+
identities: ApplicationIdentityStore;
|
|
11
|
+
remoteDaemonFiles: boolean;
|
|
12
|
+
}
|
|
13
|
+
export declare function startMcpEndpoint(options: EndpointOptions): Promise<{
|
|
14
|
+
close(): Promise<void>;
|
|
15
|
+
}>;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { createServer } from 'node:net';
|
|
2
|
+
import { chmod, unlink } from 'node:fs/promises';
|
|
3
|
+
import { timingSafeEqual } from 'node:crypto';
|
|
4
|
+
import { createManagedOursMcpServer } from '@ours.network/mcp/server';
|
|
5
|
+
import { JSONRPCMessageSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
6
|
+
import { FileCallbacks } from './file-rpc.js';
|
|
7
|
+
import { Wire } from './wire.js';
|
|
8
|
+
export async function startMcpEndpoint(options) {
|
|
9
|
+
const sockets = new Set();
|
|
10
|
+
const server = createServer((socket) => {
|
|
11
|
+
if (sockets.size >= 4) {
|
|
12
|
+
socket.destroy();
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const wire = new Wire(socket);
|
|
16
|
+
sockets.add(wire);
|
|
17
|
+
const files = new FileCallbacks(wire);
|
|
18
|
+
let authenticated = false;
|
|
19
|
+
let inFlight = 0;
|
|
20
|
+
const ids = new Set();
|
|
21
|
+
const deadline = setTimeout(() => wire.close(), 5000);
|
|
22
|
+
deadline.unref();
|
|
23
|
+
const transport = {
|
|
24
|
+
start: async () => { },
|
|
25
|
+
send: async (message) => {
|
|
26
|
+
if ('id' in message &&
|
|
27
|
+
message.id !== undefined &&
|
|
28
|
+
('result' in message || 'error' in message))
|
|
29
|
+
ids.delete(message.id);
|
|
30
|
+
await wire.send({ kind: 'mcp', value: message });
|
|
31
|
+
},
|
|
32
|
+
close: async () => wire.close(),
|
|
33
|
+
};
|
|
34
|
+
const mcp = createManagedOursMcpServer(options.client, 'managed-v1', options.identities, {
|
|
35
|
+
remoteDaemonFiles: options.remoteDaemonFiles,
|
|
36
|
+
fileContext: files,
|
|
37
|
+
admit: async () => {
|
|
38
|
+
if (!authenticated || inFlight >= 32)
|
|
39
|
+
throw Error('MCP_ADMISSION_LIMIT');
|
|
40
|
+
inFlight++;
|
|
41
|
+
try {
|
|
42
|
+
const release = await options.runtime.admit();
|
|
43
|
+
return () => {
|
|
44
|
+
inFlight--;
|
|
45
|
+
release();
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
inFlight--;
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
wire.onClose = () => {
|
|
55
|
+
clearTimeout(deadline);
|
|
56
|
+
sockets.delete(wire);
|
|
57
|
+
files.close();
|
|
58
|
+
transport.onclose?.();
|
|
59
|
+
};
|
|
60
|
+
wire.onFrame = (frame) => {
|
|
61
|
+
if (!authenticated) {
|
|
62
|
+
const actual = typeof frame?.capability === 'string' ? Buffer.from(frame.capability) : Buffer.alloc(0);
|
|
63
|
+
const expected = Buffer.from(options.capability);
|
|
64
|
+
if (frame?.kind !== 'hello' ||
|
|
65
|
+
frame.generation !== options.generation ||
|
|
66
|
+
actual.length !== expected.length ||
|
|
67
|
+
!timingSafeEqual(actual, expected))
|
|
68
|
+
throw Error('UNAUTHORIZED_BRIDGE');
|
|
69
|
+
authenticated = true;
|
|
70
|
+
clearTimeout(deadline);
|
|
71
|
+
void mcp
|
|
72
|
+
.connect(transport)
|
|
73
|
+
.then(() => wire.send({ kind: 'ready' }))
|
|
74
|
+
.catch(() => wire.close());
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (frame?.kind === 'fs-result') {
|
|
78
|
+
files.receive(frame);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (frame?.kind !== 'mcp')
|
|
82
|
+
throw Error('UNKNOWN_BRIDGE_OPERATION');
|
|
83
|
+
const message = JSONRPCMessageSchema.parse(frame.value);
|
|
84
|
+
if ('method' in message && 'id' in message) {
|
|
85
|
+
if (ids.has(message.id) || ids.size >= 64)
|
|
86
|
+
throw Error('MCP_REQUEST_LIMIT');
|
|
87
|
+
ids.add(message.id);
|
|
88
|
+
}
|
|
89
|
+
transport.onmessage?.(message);
|
|
90
|
+
};
|
|
91
|
+
});
|
|
92
|
+
await new Promise((resolve, reject) => {
|
|
93
|
+
server.once('error', reject);
|
|
94
|
+
server.listen(options.socket, () => {
|
|
95
|
+
server.removeListener('error', reject);
|
|
96
|
+
resolve();
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
await chmod(options.socket, 0o600);
|
|
100
|
+
return {
|
|
101
|
+
close: async () => {
|
|
102
|
+
for (const socket of sockets)
|
|
103
|
+
socket.close();
|
|
104
|
+
await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
|
|
105
|
+
await unlink(options.socket).catch((error) => {
|
|
106
|
+
if (error.code !== 'ENOENT')
|
|
107
|
+
throw error;
|
|
108
|
+
});
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { AgentHistoryQuery } from './correspondence.js';
|
|
2
|
+
import type { OursClient } from '@ours.network/sdk/client';
|
|
3
|
+
import { RuntimeJournal, type RuntimeState } from './state.js';
|
|
4
|
+
export interface AssignedIdentity {
|
|
5
|
+
instance: string;
|
|
6
|
+
generation: number;
|
|
7
|
+
daemon: string;
|
|
8
|
+
name: string;
|
|
9
|
+
lifetime: 'permanent' | 'temporary';
|
|
10
|
+
action: string;
|
|
11
|
+
expectedCid?: string;
|
|
12
|
+
allowCreate: boolean;
|
|
13
|
+
bio: string;
|
|
14
|
+
}
|
|
15
|
+
export interface RoomAdmission {
|
|
16
|
+
id: string;
|
|
17
|
+
cid: string;
|
|
18
|
+
seat: string;
|
|
19
|
+
action: string;
|
|
20
|
+
/** The secret stays in trusted storage and is never returned to the child. */
|
|
21
|
+
redeem(client: OursClient): Promise<{
|
|
22
|
+
cid: string;
|
|
23
|
+
}>;
|
|
24
|
+
observe(agentCid: string): Promise<'pending' | 'established' | 'mismatch'>;
|
|
25
|
+
discardSecret(): Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
export interface RuntimeDependencies {
|
|
28
|
+
journal: RuntimeJournal;
|
|
29
|
+
/** Client is already attached to the pinned daemon with its durable external owner. */
|
|
30
|
+
client: OursClient;
|
|
31
|
+
/** Checked before every operation; tied to the cross-process controller lock. */
|
|
32
|
+
assertFence(): void;
|
|
33
|
+
sleep(ms: number): Promise<void>;
|
|
34
|
+
now(): number;
|
|
35
|
+
}
|
|
36
|
+
/** A bridge never owns this object or its client; EOF never releases its lease. */
|
|
37
|
+
export declare class AgentOursRuntime {
|
|
38
|
+
readonly assignment: AssignedIdentity;
|
|
39
|
+
private readonly deps;
|
|
40
|
+
private inFlight;
|
|
41
|
+
private drain?;
|
|
42
|
+
private accepting;
|
|
43
|
+
private state;
|
|
44
|
+
constructor(assignment: AssignedIdentity, deps: RuntimeDependencies);
|
|
45
|
+
get snapshot(): Readonly<RuntimeState>;
|
|
46
|
+
private transition;
|
|
47
|
+
private verify;
|
|
48
|
+
prepare(room?: RoomAdmission, timeoutMs?: number): Promise<void>;
|
|
49
|
+
/** Admission covers SDK calls, response rendering and agent filesystem callbacks. */
|
|
50
|
+
admit(): Promise<() => void>;
|
|
51
|
+
/** Trusted supervisor-only admission for a later room; keeps the live harness
|
|
52
|
+
* and original startup assignment. Not exposed as an agent lifecycle tool. */
|
|
53
|
+
joinAdditionalRoom(invite: string, expectedRoomCid: string): Promise<void>;
|
|
54
|
+
readContacts(): Promise<unknown>;
|
|
55
|
+
readHistory(request: AgentHistoryQuery): Promise<unknown>;
|
|
56
|
+
startHarness<T>(start: () => Promise<T>): Promise<T>;
|
|
57
|
+
suspend(): Promise<void>;
|
|
58
|
+
terminal(): Promise<void>;
|
|
59
|
+
}
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/** A bridge never owns this object or its client; EOF never releases its lease. */
|
|
2
|
+
export class AgentOursRuntime {
|
|
3
|
+
assignment;
|
|
4
|
+
deps;
|
|
5
|
+
inFlight = 0;
|
|
6
|
+
drain;
|
|
7
|
+
accepting = false;
|
|
8
|
+
state;
|
|
9
|
+
constructor(assignment, deps) {
|
|
10
|
+
this.assignment = assignment;
|
|
11
|
+
this.deps = deps;
|
|
12
|
+
const existing = deps.journal.read();
|
|
13
|
+
if (existing) {
|
|
14
|
+
if (existing.instance !== assignment.instance ||
|
|
15
|
+
existing.daemon !== assignment.daemon ||
|
|
16
|
+
existing.name !== assignment.name ||
|
|
17
|
+
existing.lifetime !== assignment.lifetime ||
|
|
18
|
+
existing.generation !== assignment.generation ||
|
|
19
|
+
existing.action !== assignment.action ||
|
|
20
|
+
(assignment.expectedCid !== undefined &&
|
|
21
|
+
existing.cid !== undefined &&
|
|
22
|
+
existing.cid !== assignment.expectedCid))
|
|
23
|
+
throw Error('RUNTIME_ASSIGNMENT_MISMATCH');
|
|
24
|
+
this.state = existing;
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
// Deliberately serialize only the public ownership facts, never credentials.
|
|
28
|
+
const { instance, generation, daemon, name, lifetime, action } = assignment;
|
|
29
|
+
this.state = {
|
|
30
|
+
version: 1,
|
|
31
|
+
instance,
|
|
32
|
+
generation,
|
|
33
|
+
daemon,
|
|
34
|
+
name,
|
|
35
|
+
lifetime,
|
|
36
|
+
action,
|
|
37
|
+
phase: 'PREPARING',
|
|
38
|
+
revision: 0,
|
|
39
|
+
updatedAt: '',
|
|
40
|
+
};
|
|
41
|
+
deps.assertFence();
|
|
42
|
+
deps.journal.commit(this.state);
|
|
43
|
+
this.state = deps.journal.read();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
get snapshot() {
|
|
47
|
+
return structuredClone(this.state);
|
|
48
|
+
}
|
|
49
|
+
transition(phase, patch = {}) {
|
|
50
|
+
this.deps.assertFence();
|
|
51
|
+
this.deps.journal.commit({ ...this.state, ...patch, phase }, this.state.revision);
|
|
52
|
+
this.state = this.deps.journal.read();
|
|
53
|
+
}
|
|
54
|
+
async verify() {
|
|
55
|
+
this.deps.assertFence();
|
|
56
|
+
if (this.assignment.expectedCid && this.state.cid !== this.assignment.expectedCid)
|
|
57
|
+
throw Error('PINNED_CID_MISMATCH');
|
|
58
|
+
const actual = await this.deps.client.currentIdentity();
|
|
59
|
+
if (actual.cid !== this.state.cid ||
|
|
60
|
+
actual.name !== this.state.name ||
|
|
61
|
+
actual.temporary !== (this.state.lifetime === 'temporary') ||
|
|
62
|
+
actual.isRoot)
|
|
63
|
+
throw Error('IDENTITY_PROOF_MISMATCH');
|
|
64
|
+
this.deps.assertFence();
|
|
65
|
+
}
|
|
66
|
+
async prepare(room, timeoutMs = 30_000) {
|
|
67
|
+
if (['TERMINAL_INTENT', 'CLEANUP_PENDING', 'RELEASED', 'FAILED', 'QUIESCING'].includes(this.state.phase))
|
|
68
|
+
throw Error('RUNTIME_NOT_RESUMABLE');
|
|
69
|
+
this.deps.assertFence();
|
|
70
|
+
if (!this.state.cid) {
|
|
71
|
+
// An existing PREPARING journal is an uncertain create, never evidence of absence.
|
|
72
|
+
if (this.state.phase === 'RECOVERING')
|
|
73
|
+
throw Error('UNCERTAIN_PROVISIONING');
|
|
74
|
+
const rows = await this.deps.client.listIdentities();
|
|
75
|
+
const row = rows.find((r) => r.name === this.state.name);
|
|
76
|
+
if (row) {
|
|
77
|
+
if (!('cid' in row) ||
|
|
78
|
+
this.state.lifetime === 'temporary' ||
|
|
79
|
+
!this.assignment.expectedCid ||
|
|
80
|
+
row.cid !== this.assignment.expectedCid ||
|
|
81
|
+
row.kind === 'root' ||
|
|
82
|
+
row.temp)
|
|
83
|
+
throw Error('IDENTITY_COLLISION');
|
|
84
|
+
this.transition('RECOVERING');
|
|
85
|
+
await this.deps.client.chooseIdentity({ name: this.state.name, force: false });
|
|
86
|
+
this.transition('OWNED', { cid: row.cid });
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
if (!this.assignment.allowCreate || this.assignment.expectedCid)
|
|
90
|
+
throw Error('IDENTITY_ABSENT');
|
|
91
|
+
// Persist uncertainty before the non-idempotent call. Crash recovery requires owner proof.
|
|
92
|
+
this.transition('RECOVERING');
|
|
93
|
+
const args = {
|
|
94
|
+
name: this.state.name,
|
|
95
|
+
bio: this.assignment.bio,
|
|
96
|
+
exposeLocal: false,
|
|
97
|
+
localAutoAccept: true,
|
|
98
|
+
};
|
|
99
|
+
const created = this.state.lifetime === 'temporary'
|
|
100
|
+
? await this.deps.client.createTemporaryIdentity(args)
|
|
101
|
+
: await this.deps.client.createIdentity(args);
|
|
102
|
+
if (created.hierarchy !== 'role')
|
|
103
|
+
throw Error('ROOT_PROVISIONING_FORBIDDEN');
|
|
104
|
+
this.transition('OWNED', { cid: created.info.cid });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
await this.verify();
|
|
108
|
+
if (!room && (this.state.room || this.state.admissionIntent))
|
|
109
|
+
throw Error('ROOM_DESCRIPTOR_REQUIRED');
|
|
110
|
+
if (room) {
|
|
111
|
+
if (this.state.room &&
|
|
112
|
+
(this.state.room.id !== room.id ||
|
|
113
|
+
this.state.room.cid !== room.cid ||
|
|
114
|
+
this.state.room.seat !== room.seat ||
|
|
115
|
+
this.state.room.agentCid !== this.state.cid ||
|
|
116
|
+
this.state.room.action !== room.action))
|
|
117
|
+
throw Error('ROOM_ASSIGNMENT_MISMATCH');
|
|
118
|
+
const intent = this.state.admissionIntent;
|
|
119
|
+
if (intent &&
|
|
120
|
+
(intent.id !== room.id ||
|
|
121
|
+
intent.cid !== room.cid ||
|
|
122
|
+
intent.seat !== room.seat ||
|
|
123
|
+
intent.action !== room.action ||
|
|
124
|
+
intent.agentCid !== this.state.cid))
|
|
125
|
+
throw Error('ROOM_INTENT_MISMATCH');
|
|
126
|
+
if (!this.state.room && !intent) {
|
|
127
|
+
// A permanent identity may already occupy its seat after a clean supervisor stop.
|
|
128
|
+
const existing = await room.observe(this.state.cid);
|
|
129
|
+
if (existing === 'mismatch') {
|
|
130
|
+
this.transition('FAILED');
|
|
131
|
+
throw Error('WRONG_ROOM_SEAT');
|
|
132
|
+
}
|
|
133
|
+
this.transition('ROOM_PENDING', {
|
|
134
|
+
admissionIntent: {
|
|
135
|
+
id: room.id,
|
|
136
|
+
cid: room.cid,
|
|
137
|
+
seat: room.seat,
|
|
138
|
+
action: room.action,
|
|
139
|
+
agentCid: this.state.cid,
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
if (existing !== 'established') {
|
|
143
|
+
const result = await room.redeem(this.deps.client);
|
|
144
|
+
if (result.cid !== room.cid) {
|
|
145
|
+
this.transition('FAILED');
|
|
146
|
+
throw Error('WRONG_ROOM_CID');
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const deadline = this.deps.now() + timeoutMs;
|
|
151
|
+
for (;;) {
|
|
152
|
+
this.deps.assertFence();
|
|
153
|
+
const observation = await room.observe(this.state.cid);
|
|
154
|
+
if (observation === 'mismatch') {
|
|
155
|
+
this.transition('FAILED');
|
|
156
|
+
throw Error('WRONG_ROOM_SEAT');
|
|
157
|
+
}
|
|
158
|
+
if (observation === 'established')
|
|
159
|
+
break;
|
|
160
|
+
if (this.deps.now() >= deadline)
|
|
161
|
+
throw Error('ROOM_PENDING');
|
|
162
|
+
await this.deps.sleep(Math.min(250, deadline - this.deps.now()));
|
|
163
|
+
}
|
|
164
|
+
this.transition('READY', {
|
|
165
|
+
room: {
|
|
166
|
+
id: room.id,
|
|
167
|
+
cid: room.cid,
|
|
168
|
+
seat: room.seat,
|
|
169
|
+
agentCid: this.state.cid,
|
|
170
|
+
action: room.action,
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
await room.discardSecret();
|
|
174
|
+
}
|
|
175
|
+
else
|
|
176
|
+
this.transition('READY');
|
|
177
|
+
this.accepting = true;
|
|
178
|
+
}
|
|
179
|
+
/** Admission covers SDK calls, response rendering and agent filesystem callbacks. */
|
|
180
|
+
async admit() {
|
|
181
|
+
this.deps.assertFence();
|
|
182
|
+
if (!this.accepting || !['READY', 'SERVING'].includes(this.state.phase))
|
|
183
|
+
throw Error('RUNTIME_NOT_READY');
|
|
184
|
+
this.inFlight++;
|
|
185
|
+
let released = false;
|
|
186
|
+
const release = () => {
|
|
187
|
+
if (released)
|
|
188
|
+
return;
|
|
189
|
+
released = true;
|
|
190
|
+
if (--this.inFlight === 0)
|
|
191
|
+
this.drain?.();
|
|
192
|
+
};
|
|
193
|
+
try {
|
|
194
|
+
await this.verify();
|
|
195
|
+
if (!this.accepting)
|
|
196
|
+
throw Error('RUNTIME_QUIESCING');
|
|
197
|
+
return release;
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
release();
|
|
201
|
+
throw error;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
/** Trusted supervisor-only admission for a later room; keeps the live harness
|
|
205
|
+
* and original startup assignment. Not exposed as an agent lifecycle tool. */
|
|
206
|
+
async joinAdditionalRoom(invite, expectedRoomCid) {
|
|
207
|
+
const release = await this.admit();
|
|
208
|
+
try {
|
|
209
|
+
const contact = await this.deps.client.addContact({ invite });
|
|
210
|
+
if (contact.cid !== expectedRoomCid)
|
|
211
|
+
throw Error('ADDITIONAL_ROOM_CID_MISMATCH');
|
|
212
|
+
await this.verify();
|
|
213
|
+
}
|
|
214
|
+
finally {
|
|
215
|
+
release();
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
async readContacts() {
|
|
219
|
+
const release = await this.admit();
|
|
220
|
+
try {
|
|
221
|
+
const data = await this.deps.client.listContacts();
|
|
222
|
+
await this.verify();
|
|
223
|
+
return { identity: { name: this.state.name, cid: this.state.cid, generation: this.state.generation }, ...data };
|
|
224
|
+
}
|
|
225
|
+
finally {
|
|
226
|
+
release();
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
async readHistory(request) {
|
|
230
|
+
const release = await this.admit();
|
|
231
|
+
try {
|
|
232
|
+
const data = await this.deps.client.listHistory(request);
|
|
233
|
+
await this.verify();
|
|
234
|
+
return { identity: { name: this.state.name, cid: this.state.cid, generation: this.state.generation }, ...data };
|
|
235
|
+
}
|
|
236
|
+
finally {
|
|
237
|
+
release();
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
async startHarness(start) {
|
|
241
|
+
if (!this.accepting || this.state.phase !== 'READY')
|
|
242
|
+
throw Error('RUNTIME_NOT_READY');
|
|
243
|
+
await this.verify();
|
|
244
|
+
this.transition('SERVING');
|
|
245
|
+
return start();
|
|
246
|
+
}
|
|
247
|
+
async suspend() {
|
|
248
|
+
this.accepting = false;
|
|
249
|
+
if (this.inFlight)
|
|
250
|
+
await new Promise((resolve) => {
|
|
251
|
+
this.drain = resolve;
|
|
252
|
+
});
|
|
253
|
+
this.transition('RECOVERING');
|
|
254
|
+
}
|
|
255
|
+
async terminal() {
|
|
256
|
+
this.accepting = false;
|
|
257
|
+
if (this.state.phase === 'RELEASED')
|
|
258
|
+
return;
|
|
259
|
+
if (!['TERMINAL_INTENT', 'CLEANUP_PENDING'].includes(this.state.phase))
|
|
260
|
+
this.transition('QUIESCING');
|
|
261
|
+
if (this.inFlight)
|
|
262
|
+
await new Promise((resolve) => {
|
|
263
|
+
this.drain = resolve;
|
|
264
|
+
});
|
|
265
|
+
this.transition('TERMINAL_INTENT');
|
|
266
|
+
try {
|
|
267
|
+
this.deps.assertFence();
|
|
268
|
+
const ack = await this.deps.client.releaseLease();
|
|
269
|
+
if (!ack ||
|
|
270
|
+
!Array.isArray(ack.released) ||
|
|
271
|
+
!Array.isArray(ack.closed) ||
|
|
272
|
+
[...ack.released, ...ack.closed].some((name) => typeof name !== 'string') ||
|
|
273
|
+
[ack.attempted, ack.notified, ack.failed].some((n) => !Number.isSafeInteger(n) || n < 0) ||
|
|
274
|
+
ack.notified + ack.failed !== ack.attempted)
|
|
275
|
+
throw Error('INVALID_RELEASE_ACK');
|
|
276
|
+
this.transition('RELEASED', { releaseAck: ack });
|
|
277
|
+
}
|
|
278
|
+
catch (error) {
|
|
279
|
+
this.transition('CLEANUP_PENDING');
|
|
280
|
+
throw error;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|