@neta-art/cohub-cli 7.1.2 → 8.0.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 +40 -2
- package/dist/auth.js +38 -5
- package/dist/client.js +5 -2
- package/dist/commands/runtime.d.ts +1 -2
- package/dist/commands/runtime.js +169 -293
- package/dist/commands/sandboxd-binary.d.ts +1 -1
- package/dist/commands/sandboxd-binary.js +13 -5
- package/dist/runtime/archive-store.d.ts +5 -1
- package/dist/runtime/archive-store.js +22 -6
- package/dist/runtime/connection.d.ts +4 -2
- package/dist/runtime/connection.js +113 -44
- package/dist/runtime/diagnostics.d.ts +3 -0
- package/dist/runtime/diagnostics.js +3 -0
- package/dist/runtime/harness.d.ts +7 -0
- package/dist/runtime/harness.js +63 -17
- package/dist/runtime/instance.d.ts +5 -0
- package/dist/runtime/instance.js +159 -0
- package/dist/runtime/json-rpc.d.ts +2 -0
- package/dist/runtime/json-rpc.js +2 -0
- package/dist/runtime/launch.d.ts +20 -0
- package/dist/runtime/launch.js +176 -0
- package/dist/runtime/native-codex-hook.d.ts +1 -0
- package/dist/runtime/native-codex-hook.js +28 -0
- package/dist/runtime/native-install.d.ts +21 -0
- package/dist/runtime/native-install.js +130 -0
- package/dist/runtime/native-ipc.d.ts +26 -0
- package/dist/runtime/native-ipc.js +101 -0
- package/dist/runtime/native-pi-extension.d.ts +20 -0
- package/dist/runtime/native-pi-extension.js +47 -0
- package/dist/runtime/native-sync-store.d.ts +97 -0
- package/dist/runtime/native-sync-store.js +365 -0
- package/dist/runtime/native-sync.d.ts +25 -0
- package/dist/runtime/native-sync.js +128 -0
- package/dist/runtime/native-transcript.d.ts +27 -0
- package/dist/runtime/native-transcript.js +281 -0
- package/dist/runtime/presentation.d.ts +21 -0
- package/dist/runtime/presentation.js +78 -0
- package/dist/runtime/process-group.d.ts +2 -0
- package/dist/runtime/process-group.js +124 -31
- package/dist/runtime/session-store.d.ts +17 -2
- package/dist/runtime/session-store.js +118 -9
- package/dist/runtime/space-binding.d.ts +3 -0
- package/dist/runtime/space-binding.js +43 -6
- package/dist/runtime/supervisor.d.ts +16 -0
- package/dist/runtime/supervisor.js +277 -0
- package/dist/runtime/worker.d.ts +1 -0
- package/dist/runtime/worker.js +20 -0
- package/package.json +3 -2
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { createInterface } from "node:readline";
|
|
4
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
5
|
+
import { resolveWebsocketUrl } from "@neta-art/cohub";
|
|
6
|
+
import { AuthRequiredError, resolveAccessToken } from "../auth.js";
|
|
7
|
+
import { createClient } from "../client.js";
|
|
8
|
+
import { currentIdentityKey } from "../space.js";
|
|
9
|
+
import { ensureSandboxdBinary } from "../commands/sandboxd-binary.js";
|
|
10
|
+
import { serveRuntime } from "./connection.js";
|
|
11
|
+
import { discoverHarnesses } from "./harness.js";
|
|
12
|
+
import { RuntimeDiagnostics, serializeDiagnosticError } from "./diagnostics.js";
|
|
13
|
+
import { ownRuntimeInstance, runtimeInstanceDirectory } from "./instance.js";
|
|
14
|
+
import { createDiagnosticConsole } from "./presentation.js";
|
|
15
|
+
import { RuntimeSessionStore } from "./session-store.js";
|
|
16
|
+
import { captureNativeSession, flushNativeSessions, nativeWebSocketTransport } from "./native-sync.js";
|
|
17
|
+
import { serveNativeDaemon } from "./native-ipc.js";
|
|
18
|
+
export function sandboxOutputLevel(value, stream) {
|
|
19
|
+
const level = typeof value === "string" ? value.toLowerCase() : "";
|
|
20
|
+
if (["debug", "info", "warn", "error"].includes(level))
|
|
21
|
+
return level;
|
|
22
|
+
return stream === "stderr" ? "warn" : "debug";
|
|
23
|
+
}
|
|
24
|
+
export async function runRuntime(config, onState, externalSignal, onDiagnostic) {
|
|
25
|
+
const controller = new AbortController();
|
|
26
|
+
const signal = externalSignal ? AbortSignal.any([externalSignal, controller.signal]) : controller.signal;
|
|
27
|
+
const stop = () => controller.abort();
|
|
28
|
+
process.once("SIGINT", stop);
|
|
29
|
+
process.once("SIGTERM", stop);
|
|
30
|
+
const client = createClient();
|
|
31
|
+
const space = client.space(config.spaceId);
|
|
32
|
+
const store = new RuntimeSessionStore(config.spaceId, { projectionSource: space, archiveTransport: space });
|
|
33
|
+
const consoleSink = config.background ? undefined : createDiagnosticConsole(config.verbose);
|
|
34
|
+
const diagnostics = new RuntimeDiagnostics({
|
|
35
|
+
root: store.root, spaceId: config.spaceId, runtimeId: randomUUID(),
|
|
36
|
+
onEvent: (event) => { consoleSink?.(event); onDiagnostic?.(event); },
|
|
37
|
+
});
|
|
38
|
+
store.setDiagnostics(diagnostics);
|
|
39
|
+
let status = {
|
|
40
|
+
spaceId: config.spaceId, root: config.root, runtimeId: diagnostics.runtimeId,
|
|
41
|
+
pid: process.pid, harnesses: config.harnesses, background: config.background,
|
|
42
|
+
state: "starting", harnessConnected: false, workspaceConnected: false,
|
|
43
|
+
diagnosticsPath: diagnostics.directory,
|
|
44
|
+
nativeSync: true,
|
|
45
|
+
};
|
|
46
|
+
let hasBeenReady = false;
|
|
47
|
+
const update = (patch) => {
|
|
48
|
+
const next = { ...status, ...patch };
|
|
49
|
+
if (next.state !== "stopping" && next.state !== "attention")
|
|
50
|
+
next.state = next.harnessConnected && next.workspaceConnected ? "ready" : hasBeenReady ? "reconnecting" : "starting";
|
|
51
|
+
if (JSON.stringify(status) === JSON.stringify(next))
|
|
52
|
+
return;
|
|
53
|
+
const becameReady = next.state === "ready" && status.state !== "ready";
|
|
54
|
+
status = next;
|
|
55
|
+
if (becameReady) {
|
|
56
|
+
hasBeenReady = true;
|
|
57
|
+
diagnostics.log("info", "runtime.available");
|
|
58
|
+
}
|
|
59
|
+
onState({ ...status });
|
|
60
|
+
};
|
|
61
|
+
let closeInstance;
|
|
62
|
+
let bridgeTask;
|
|
63
|
+
let nativeSyncTask;
|
|
64
|
+
let closeNativeDaemon;
|
|
65
|
+
let nativeSend = null;
|
|
66
|
+
let tokenInFlight = null;
|
|
67
|
+
const token = (forceRefresh = false) => {
|
|
68
|
+
tokenInFlight ??= (async () => {
|
|
69
|
+
if (currentIdentityKey() !== config.identity)
|
|
70
|
+
throw new AuthRequiredError("Runtime account changed; sign in to the original account");
|
|
71
|
+
const value = await resolveAccessToken({ forceRefresh });
|
|
72
|
+
if (!value)
|
|
73
|
+
throw new AuthRequiredError();
|
|
74
|
+
if (currentIdentityKey() !== config.identity)
|
|
75
|
+
throw new AuthRequiredError("Runtime account changed");
|
|
76
|
+
return value;
|
|
77
|
+
})().finally(() => { tokenInFlight = null; });
|
|
78
|
+
return tokenInFlight;
|
|
79
|
+
};
|
|
80
|
+
try {
|
|
81
|
+
closeInstance = await ownRuntimeInstance(runtimeInstanceDirectory(config.identity, config.spaceId), () => status, async (force) => {
|
|
82
|
+
if (!force)
|
|
83
|
+
for await (const batch of store.pendingExecutionBatches()) {
|
|
84
|
+
if (batch.length)
|
|
85
|
+
throw new Error("Unconfirmed executions remain. Use down --yes to stop; results and files are retained");
|
|
86
|
+
}
|
|
87
|
+
update({ state: "stopping" });
|
|
88
|
+
setTimeout(stop, 30);
|
|
89
|
+
});
|
|
90
|
+
onState({ ...status });
|
|
91
|
+
diagnostics.log("info", "runtime.cli_started", { platform: process.platform, node: process.versions.node, harnesses: config.harnesses });
|
|
92
|
+
const [binary, capabilities] = await Promise.all([
|
|
93
|
+
ensureSandboxdBinary({ onStatus: (message) => diagnostics.log("info", "sandboxd.download", { message }) }),
|
|
94
|
+
config.capabilities ?? discoverHarnesses(config.harnesses, config.executables, config.root),
|
|
95
|
+
]);
|
|
96
|
+
signal.throwIfAborted();
|
|
97
|
+
const url = new URL(resolveWebsocketUrl({ url: process.env.COHUB_WS_URL }));
|
|
98
|
+
url.pathname = "/runtime/relay";
|
|
99
|
+
const relay = new URL(url);
|
|
100
|
+
relay.pathname = "/sandbox/relay";
|
|
101
|
+
const runBridge = async () => {
|
|
102
|
+
let attempts = 0;
|
|
103
|
+
while (!signal.aborted) {
|
|
104
|
+
// A bridge may only register while this process owns the Harness lease.
|
|
105
|
+
if (!status.harnessConnected) {
|
|
106
|
+
await delay(500, undefined, { signal }).catch(() => undefined);
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
const startedAt = Date.now();
|
|
110
|
+
let refreshTimer;
|
|
111
|
+
let probeTimer;
|
|
112
|
+
let managed = false;
|
|
113
|
+
let childStopped = false;
|
|
114
|
+
try {
|
|
115
|
+
const initialToken = await token();
|
|
116
|
+
signal.throwIfAborted();
|
|
117
|
+
const bridge = spawn(binary, ["--local", "--space", config.spaceId, "--root", config.root, "--relay", process.env.COHUB_RELAY_URL?.trim() || relay.toString()], {
|
|
118
|
+
stdio: ["pipe", "pipe", "pipe", "pipe"],
|
|
119
|
+
env: { ...process.env, COHUB_RELAY_TOKEN: initialToken, COHUB_RUNTIME_ID: status.runtimeId, COHUB_LOG_FORMAT: "json", COHUB_RUNTIME_MANAGED: "1" },
|
|
120
|
+
});
|
|
121
|
+
bridge.stdin?.on("error", () => undefined);
|
|
122
|
+
const closed = new Promise((resolve) => {
|
|
123
|
+
bridge.once("error", (error) => diagnostics.log("error", "sandboxd.process_error", { error: serializeDiagnosticError(error) }));
|
|
124
|
+
bridge.once("close", (code, exitSignal) => {
|
|
125
|
+
childStopped = true;
|
|
126
|
+
if (!signal.aborted)
|
|
127
|
+
diagnostics.log("warn", "sandboxd.process_exit", { code, signal: exitSignal });
|
|
128
|
+
resolve();
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
for (const streamName of ["stdout", "stderr"]) {
|
|
132
|
+
const stream = bridge[streamName];
|
|
133
|
+
if (!stream)
|
|
134
|
+
continue;
|
|
135
|
+
const lines = createInterface({ input: stream });
|
|
136
|
+
lines.on("line", (line) => {
|
|
137
|
+
let parsed = null;
|
|
138
|
+
try {
|
|
139
|
+
const value = JSON.parse(line);
|
|
140
|
+
if (value && typeof value === "object" && !Array.isArray(value))
|
|
141
|
+
parsed = value;
|
|
142
|
+
}
|
|
143
|
+
catch { /* Compatibility with text loggers. */ }
|
|
144
|
+
const data = parsed ? Object.fromEntries(Object.entries(parsed).filter(([key]) => !["msg", "level", "time"].includes(key))) : {};
|
|
145
|
+
diagnostics.log(sandboxOutputLevel(parsed?.level, streamName), "sandboxd.log", { ...data, message: parsed?.msg ?? line }, { component: "sandboxd" });
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
const control = bridge.stdio[3];
|
|
149
|
+
const events = createInterface({ input: control });
|
|
150
|
+
events.on("line", (line) => {
|
|
151
|
+
try {
|
|
152
|
+
const event = JSON.parse(line);
|
|
153
|
+
if (event.type === "hello")
|
|
154
|
+
managed = true;
|
|
155
|
+
if (event.type === "connected" || event.type === "disconnected") {
|
|
156
|
+
const connected = event.type === "connected";
|
|
157
|
+
update({ workspaceConnected: connected });
|
|
158
|
+
diagnostics.log(connected ? "info" : "warn", `sandboxd.${event.type}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
diagnostics.log("warn", "sandboxd.control_invalid");
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
const refresh = async () => {
|
|
166
|
+
try {
|
|
167
|
+
const next = await token();
|
|
168
|
+
if (!childStopped && managed && !signal.aborted)
|
|
169
|
+
bridge.stdin?.write(`${JSON.stringify({ type: "auth", token: next })}\n`);
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
diagnostics.log("warn", error instanceof AuthRequiredError ? "runtime.auth_required" : "runtime.auth_token_failed", { error: serializeDiagnosticError(error) });
|
|
173
|
+
}
|
|
174
|
+
if (!childStopped && !signal.aborted)
|
|
175
|
+
refreshTimer = setTimeout(() => void refresh(), 10_000);
|
|
176
|
+
};
|
|
177
|
+
void refresh();
|
|
178
|
+
// Released older sandboxd binaries do not have the private control pipe.
|
|
179
|
+
// Ask the authoritative API instead of parsing human-readable log lines.
|
|
180
|
+
const probe = async () => {
|
|
181
|
+
if (!managed && !childStopped && !signal.aborted) {
|
|
182
|
+
try {
|
|
183
|
+
const remote = await space.getRuntime(undefined, { signal: AbortSignal.any([signal, AbortSignal.timeout(5000)]) });
|
|
184
|
+
if (!childStopped && !managed)
|
|
185
|
+
update({ workspaceConnected: remote.runtimeId === status.runtimeId && remote.workspace?.online === true });
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
if (!childStopped && !managed)
|
|
189
|
+
update({ workspaceConnected: false });
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (!childStopped && !signal.aborted && !managed)
|
|
193
|
+
probeTimer = setTimeout(() => void probe(), 5000);
|
|
194
|
+
};
|
|
195
|
+
probeTimer = setTimeout(() => void probe(), 1000);
|
|
196
|
+
const terminate = () => {
|
|
197
|
+
bridge.kill("SIGTERM");
|
|
198
|
+
const timeout = setTimeout(() => bridge.kill("SIGKILL"), 3000);
|
|
199
|
+
void closed.finally(() => clearTimeout(timeout));
|
|
200
|
+
};
|
|
201
|
+
signal.addEventListener("abort", terminate, { once: true });
|
|
202
|
+
if (signal.aborted)
|
|
203
|
+
terminate();
|
|
204
|
+
await closed;
|
|
205
|
+
signal.removeEventListener("abort", terminate);
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
if (!signal.aborted)
|
|
209
|
+
diagnostics.log("warn", "sandboxd.restart_failed", { error: serializeDiagnosticError(error) });
|
|
210
|
+
}
|
|
211
|
+
finally {
|
|
212
|
+
childStopped = true;
|
|
213
|
+
clearTimeout(refreshTimer);
|
|
214
|
+
clearTimeout(probeTimer);
|
|
215
|
+
update({ workspaceConnected: false });
|
|
216
|
+
}
|
|
217
|
+
if (Date.now() - startedAt > 60_000)
|
|
218
|
+
attempts = 0;
|
|
219
|
+
const backoff = Math.min(30_000, 500 * 2 ** Math.min(attempts++, 6));
|
|
220
|
+
await delay(backoff / 2 + Math.random() * backoff / 2, undefined, { signal }).catch(() => undefined);
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
bridgeTask = runBridge();
|
|
224
|
+
closeNativeDaemon = await serveNativeDaemon({ runtimeRoot: store.root, handle: async (request) => ({
|
|
225
|
+
store: await captureNativeSession(request),
|
|
226
|
+
}) });
|
|
227
|
+
nativeSyncTask = (async () => {
|
|
228
|
+
const report = (error) => diagnostics.log("warn", "native.sync_pending", { error: serializeDiagnosticError(error) });
|
|
229
|
+
while (!signal.aborted) {
|
|
230
|
+
try {
|
|
231
|
+
if (nativeSend)
|
|
232
|
+
await flushNativeSessions(config.spaceId, config.identity, signal, report, nativeWebSocketTransport(config.spaceId, config.identity, nativeSend));
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
if (!signal.aborted)
|
|
236
|
+
report(error);
|
|
237
|
+
}
|
|
238
|
+
await delay(5000, undefined, { signal }).catch(() => undefined);
|
|
239
|
+
}
|
|
240
|
+
})();
|
|
241
|
+
await serveRuntime({
|
|
242
|
+
spaceId: config.spaceId, cwd: config.root, url: url.toString(), capabilities,
|
|
243
|
+
harnesses: config.executables, runtimeId: status.runtimeId, diagnostics, token, signal, store,
|
|
244
|
+
onReady: () => update({ harnessConnected: true }),
|
|
245
|
+
onDisconnected: () => { nativeSend = null; update({ harnessConnected: false }); },
|
|
246
|
+
onNativeChannel: (send) => { nativeSend = send; },
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
catch (error) {
|
|
250
|
+
if (!signal.aborted) {
|
|
251
|
+
update({ state: "attention" });
|
|
252
|
+
diagnostics.log("error", "runtime.failed", { error: serializeDiagnosticError(error) });
|
|
253
|
+
throw error;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
finally {
|
|
257
|
+
controller.abort();
|
|
258
|
+
try {
|
|
259
|
+
await Promise.all([bridgeTask, nativeSyncTask]);
|
|
260
|
+
await closeNativeDaemon?.();
|
|
261
|
+
}
|
|
262
|
+
finally {
|
|
263
|
+
try {
|
|
264
|
+
await diagnostics.close();
|
|
265
|
+
}
|
|
266
|
+
finally {
|
|
267
|
+
try {
|
|
268
|
+
await closeInstance?.();
|
|
269
|
+
}
|
|
270
|
+
finally {
|
|
271
|
+
process.removeListener("SIGINT", stop);
|
|
272
|
+
process.removeListener("SIGTERM", stop);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { runRuntime } from "./supervisor.js";
|
|
2
|
+
import { serializeDiagnosticError } from "./diagnostics.js";
|
|
3
|
+
const notify = (message) => {
|
|
4
|
+
// The launcher deliberately disconnects after its one-shot summary.
|
|
5
|
+
if (process.connected)
|
|
6
|
+
process.send?.(message, () => undefined);
|
|
7
|
+
};
|
|
8
|
+
process.once("message", (config) => {
|
|
9
|
+
void runRuntime(config, (status) => {
|
|
10
|
+
notify({ type: "status", status });
|
|
11
|
+
}, undefined, (event) => {
|
|
12
|
+
notify({ type: "diagnostic", event });
|
|
13
|
+
}).catch((error) => {
|
|
14
|
+
notify({ type: "failed", error: serializeDiagnosticError(error).message });
|
|
15
|
+
process.exitCode = 1;
|
|
16
|
+
}).finally(() => {
|
|
17
|
+
if (process.connected)
|
|
18
|
+
process.disconnect?.();
|
|
19
|
+
});
|
|
20
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@neta-art/cohub-cli",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "8.0.1",
|
|
4
4
|
"description": "CLI for Cohub — spaces, sessions, and agent collaboration.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"commander": "^15.0.0",
|
|
23
23
|
"pixi.js": "^8.20.1",
|
|
24
24
|
"sharp": "^0.35.4",
|
|
25
|
-
"@neta-art/cohub": "8.
|
|
25
|
+
"@neta-art/cohub": "8.21.0"
|
|
26
26
|
},
|
|
27
27
|
"publishConfig": {
|
|
28
28
|
"access": "public"
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
"test": "node ../../scripts/test/run.mjs 'tests/**/*.test.ts'",
|
|
43
43
|
"test:runtime": "node --import tsx --test tests/runtime-*.integration.ts",
|
|
44
44
|
"test:runtime:native": "node --import tsx tests/runtime-native.smoke.ts",
|
|
45
|
+
"test:runtime:plugins": "node scripts/test-native-plugins.mjs",
|
|
45
46
|
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
46
47
|
}
|
|
47
48
|
}
|