@synap-core/cli 1.12.2 → 1.13.1-next.20261102.ec3d166
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/LICENSE +21 -0
- package/README.md +38 -11
- package/dist/build-info.json +2 -3
- package/dist/commands/agents.d.ts +0 -1
- package/dist/commands/agents.js +13 -3
- package/dist/commands/agents.js.map +1 -1
- package/dist/commands/app.js +2 -1
- package/dist/commands/app.js.map +1 -1
- package/dist/commands/automation.js +46 -41
- package/dist/commands/automation.js.map +1 -1
- package/dist/commands/box.d.ts +42 -0
- package/dist/commands/box.js +89 -0
- package/dist/commands/box.js.map +1 -0
- package/dist/commands/bridge-setup.js +3 -1
- package/dist/commands/bridge-setup.js.map +1 -1
- package/dist/commands/capability.d.ts +87 -1
- package/dist/commands/capability.js +174 -64
- package/dist/commands/capability.js.map +1 -1
- package/dist/commands/close-questions.d.ts +27 -0
- package/dist/commands/close-questions.js +49 -0
- package/dist/commands/close-questions.js.map +1 -0
- package/dist/commands/close-questions.test.d.ts +1 -0
- package/dist/commands/close-questions.test.js +52 -0
- package/dist/commands/close-questions.test.js.map +1 -0
- package/dist/commands/connect.d.ts +1 -3
- package/dist/commands/connect.js +2 -2
- package/dist/commands/connect.js.map +1 -1
- package/dist/commands/context.js +72 -43
- package/dist/commands/context.js.map +1 -1
- package/dist/commands/data-extra.d.ts +13 -0
- package/dist/commands/data-extra.js +35 -6
- package/dist/commands/data-extra.js.map +1 -1
- package/dist/commands/data.js +2 -1
- package/dist/commands/data.js.map +1 -1
- package/dist/commands/doctor.d.ts +39 -0
- package/dist/commands/doctor.js +136 -14
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/hooks.d.ts +19 -0
- package/dist/commands/hooks.js +70 -0
- package/dist/commands/hooks.js.map +1 -0
- package/dist/commands/import.d.ts +25 -0
- package/dist/commands/import.js +98 -12
- package/dist/commands/import.js.map +1 -1
- package/dist/commands/init.d.ts +14 -0
- package/dist/commands/init.js +63 -21
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/keys-request.d.ts +48 -0
- package/dist/commands/keys-request.js +236 -0
- package/dist/commands/keys-request.js.map +1 -0
- package/dist/commands/keys-request.test.d.ts +1 -0
- package/dist/commands/keys-request.test.js +176 -0
- package/dist/commands/keys-request.test.js.map +1 -0
- package/dist/commands/knowledge.d.ts +6 -0
- package/dist/commands/knowledge.js +13 -3
- package/dist/commands/knowledge.js.map +1 -1
- package/dist/commands/lens.js +2 -1
- package/dist/commands/lens.js.map +1 -1
- package/dist/commands/market-authoring.js +1 -1
- package/dist/commands/market-authoring.js.map +1 -1
- package/dist/commands/market.d.ts +61 -4
- package/dist/commands/market.js +106 -48
- package/dist/commands/market.js.map +1 -1
- package/dist/commands/mcp.d.ts +1 -1
- package/dist/commands/mcp.js +14 -9
- package/dist/commands/mcp.js.map +1 -1
- package/dist/commands/observe.category.test.d.ts +1 -0
- package/dist/commands/observe.category.test.js +59 -0
- package/dist/commands/observe.category.test.js.map +1 -0
- package/dist/commands/observe.d.ts +3 -0
- package/dist/commands/observe.js +7 -5
- package/dist/commands/observe.js.map +1 -1
- package/dist/commands/observe.title.test.d.ts +1 -0
- package/dist/commands/observe.title.test.js +37 -0
- package/dist/commands/observe.title.test.js.map +1 -0
- package/dist/commands/playbook.js +9 -0
- package/dist/commands/playbook.js.map +1 -1
- package/dist/commands/pods.d.ts +6 -1
- package/dist/commands/pods.js +38 -17
- package/dist/commands/pods.js.map +1 -1
- package/dist/commands/proposals-actions.js +7 -1
- package/dist/commands/proposals-actions.js.map +1 -1
- package/dist/commands/proton-setup.js +3 -1
- package/dist/commands/proton-setup.js.map +1 -1
- package/dist/commands/providers.d.ts +22 -6
- package/dist/commands/providers.js +39 -32
- package/dist/commands/providers.js.map +1 -1
- package/dist/commands/raycast.d.ts +15 -0
- package/dist/commands/raycast.js +36 -10
- package/dist/commands/raycast.js.map +1 -1
- package/dist/commands/session-feed-wait.d.ts +48 -0
- package/dist/commands/session-feed-wait.js +261 -0
- package/dist/commands/session-feed-wait.js.map +1 -0
- package/dist/commands/session-post.d.ts +40 -0
- package/dist/commands/session-post.js +96 -0
- package/dist/commands/session-post.js.map +1 -0
- package/dist/commands/session-wait.d.ts +9 -4
- package/dist/commands/session-wait.js +20 -5
- package/dist/commands/session-wait.js.map +1 -1
- package/dist/commands/sessions.d.ts +2 -0
- package/dist/commands/sessions.js +23 -0
- package/dist/commands/sessions.js.map +1 -1
- package/dist/commands/skills-commands.d.ts +2 -0
- package/dist/commands/skills-commands.js +9 -1
- package/dist/commands/skills-commands.js.map +1 -1
- package/dist/commands/skills-commands.test.d.ts +1 -0
- package/dist/commands/skills-commands.test.js +57 -0
- package/dist/commands/skills-commands.test.js.map +1 -0
- package/dist/commands/status.js +13 -7
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/statusline.backoff.test.js +13 -3
- package/dist/commands/statusline.backoff.test.js.map +1 -1
- package/dist/commands/statusline.d.ts +104 -9
- package/dist/commands/statusline.js +392 -148
- package/dist/commands/statusline.js.map +1 -1
- package/dist/commands/statusline.line2.test.d.ts +15 -0
- package/dist/commands/statusline.line2.test.js +226 -0
- package/dist/commands/statusline.line2.test.js.map +1 -0
- package/dist/commands/statusline.needs-you.test.d.ts +1 -0
- package/dist/commands/statusline.needs-you.test.js +17 -0
- package/dist/commands/statusline.needs-you.test.js.map +1 -0
- package/dist/commands/statusline.now.test.d.ts +10 -0
- package/dist/commands/statusline.now.test.js +250 -0
- package/dist/commands/statusline.now.test.js.map +1 -0
- package/dist/commands/tracks.d.ts +68 -6
- package/dist/commands/tracks.js +272 -16
- package/dist/commands/tracks.js.map +1 -1
- package/dist/commands/upload.js +12 -0
- package/dist/commands/upload.js.map +1 -1
- package/dist/commands/wait.d.ts +124 -0
- package/dist/commands/wait.js +384 -0
- package/dist/commands/wait.js.map +1 -0
- package/dist/commands/wait.test.d.ts +8 -0
- package/dist/commands/wait.test.js +308 -0
- package/dist/commands/wait.test.js.map +1 -0
- package/dist/commands/whoami.d.ts +7 -0
- package/dist/commands/whoami.js +17 -0
- package/dist/commands/whoami.js.map +1 -1
- package/dist/commands/worker-accounts.d.ts +149 -0
- package/dist/commands/worker-accounts.js +339 -0
- package/dist/commands/worker-accounts.js.map +1 -0
- package/dist/commands/worker-accounts.test.d.ts +1 -0
- package/dist/commands/worker-accounts.test.js +224 -0
- package/dist/commands/worker-accounts.test.js.map +1 -0
- package/dist/commands/worker-github-app.d.ts +63 -0
- package/dist/commands/worker-github-app.js +161 -0
- package/dist/commands/worker-github-app.js.map +1 -0
- package/dist/commands/worker-github-app.test.d.ts +1 -0
- package/dist/commands/worker-github-app.test.js +102 -0
- package/dist/commands/worker-github-app.test.js.map +1 -0
- package/dist/commands/worker-github-broker.d.ts +39 -0
- package/dist/commands/worker-github-broker.js +106 -0
- package/dist/commands/worker-github-broker.js.map +1 -0
- package/dist/commands/worker-github-broker.test.d.ts +1 -0
- package/dist/commands/worker-github-broker.test.js +90 -0
- package/dist/commands/worker-github-broker.test.js.map +1 -0
- package/dist/commands/worker-runs.test.d.ts +1 -0
- package/dist/commands/worker-runs.test.js +150 -0
- package/dist/commands/worker-runs.test.js.map +1 -0
- package/dist/commands/worker-worktrees.d.ts +34 -0
- package/dist/commands/worker-worktrees.js +87 -0
- package/dist/commands/worker-worktrees.js.map +1 -0
- package/dist/commands/worker.d.ts +406 -10
- package/dist/commands/worker.js +1180 -126
- package/dist/commands/worker.js.map +1 -1
- package/dist/commands/worker.test.js +588 -11
- package/dist/commands/worker.test.js.map +1 -1
- package/dist/index.js +220 -28
- package/dist/index.js.map +1 -1
- package/dist/lib/agent-door.d.ts +1 -3
- package/dist/lib/agent-door.js +10 -7
- package/dist/lib/agent-door.js.map +1 -1
- package/dist/lib/agents-config.d.ts +9 -0
- package/dist/lib/agents-config.js +20 -0
- package/dist/lib/agents-config.js.map +1 -1
- package/dist/lib/atomic-file.d.ts +20 -1
- package/dist/lib/atomic-file.js +100 -2
- package/dist/lib/atomic-file.js.map +1 -1
- package/dist/lib/auth.d.ts +28 -2
- package/dist/lib/auth.js +52 -12
- package/dist/lib/auth.js.map +1 -1
- package/dist/lib/box/egress.d.ts +82 -0
- package/dist/lib/box/egress.js +264 -0
- package/dist/lib/box/egress.js.map +1 -0
- package/dist/lib/box/evidence.d.ts +86 -0
- package/dist/lib/box/evidence.js +121 -0
- package/dist/lib/box/evidence.js.map +1 -0
- package/dist/lib/box/job-server.d.ts +72 -0
- package/dist/lib/box/job-server.js +250 -0
- package/dist/lib/box/job-server.js.map +1 -0
- package/dist/lib/box/provisioner.d.ts +83 -0
- package/dist/lib/box/provisioner.js +312 -0
- package/dist/lib/box/provisioner.js.map +1 -0
- package/dist/lib/box/proxmox.d.ts +112 -0
- package/dist/lib/box/proxmox.js +211 -0
- package/dist/lib/box/proxmox.js.map +1 -0
- package/dist/lib/box/wire.d.ts +98 -0
- package/dist/lib/box/wire.js +70 -0
- package/dist/lib/box/wire.js.map +1 -0
- package/dist/lib/bridge-invocation.test.d.ts +1 -0
- package/dist/lib/bridge-invocation.test.js +75 -0
- package/dist/lib/bridge-invocation.test.js.map +1 -0
- package/dist/lib/bridge-status.d.ts +144 -0
- package/dist/lib/bridge-status.js +227 -0
- package/dist/lib/bridge-status.js.map +1 -0
- package/dist/lib/bridge-status.test.d.ts +6 -0
- package/dist/lib/bridge-status.test.js +197 -0
- package/dist/lib/bridge-status.test.js.map +1 -0
- package/dist/lib/client-header.test.d.ts +1 -0
- package/dist/lib/client-header.test.js +103 -0
- package/dist/lib/client-header.test.js.map +1 -0
- package/dist/lib/conversation-headers.d.ts +89 -9
- package/dist/lib/conversation-headers.js +156 -3
- package/dist/lib/conversation-headers.js.map +1 -1
- package/dist/lib/conversation-headers.test.d.ts +1 -0
- package/dist/lib/conversation-headers.test.js +78 -0
- package/dist/lib/conversation-headers.test.js.map +1 -0
- package/dist/lib/credential-class.d.ts +40 -2
- package/dist/lib/credential-class.js +48 -6
- package/dist/lib/credential-class.js.map +1 -1
- package/dist/lib/device-connect.d.ts +45 -0
- package/dist/lib/device-connect.js +60 -0
- package/dist/lib/device-connect.js.map +1 -1
- package/dist/lib/exporter-coverage.d.ts +1 -1
- package/dist/lib/exporter-coverage.js +1 -1
- package/dist/lib/goal-words.d.ts +38 -0
- package/dist/lib/goal-words.js +115 -0
- package/dist/lib/goal-words.js.map +1 -0
- package/dist/lib/hub-client.d.ts +87 -8
- package/dist/lib/hub-client.js +205 -70
- package/dist/lib/hub-client.js.map +1 -1
- package/dist/lib/init-handoff.d.ts +8 -0
- package/dist/lib/init-handoff.js +9 -2
- package/dist/lib/init-handoff.js.map +1 -1
- package/dist/lib/key-locations.d.ts +3 -3
- package/dist/lib/key-locations.js +3 -39
- package/dist/lib/key-locations.js.map +1 -1
- package/dist/lib/key-source.d.ts +3 -1
- package/dist/lib/key-source.js +7 -4
- package/dist/lib/key-source.js.map +1 -1
- package/dist/lib/knowledge-title.d.ts +3 -2
- package/dist/lib/knowledge-title.js +3 -2
- package/dist/lib/knowledge-title.js.map +1 -1
- package/dist/lib/machine-instance.d.ts +7 -5
- package/dist/lib/machine-instance.js +7 -5
- package/dist/lib/machine-instance.js.map +1 -1
- package/dist/lib/mcp-bridge.d.ts +50 -0
- package/dist/lib/mcp-bridge.js +232 -11
- package/dist/lib/mcp-bridge.js.map +1 -1
- package/dist/lib/mcp-bridge.test.js +224 -4
- package/dist/lib/mcp-bridge.test.js.map +1 -1
- package/dist/lib/mcp-call.d.ts +5 -1
- package/dist/lib/mcp-call.js +7 -6
- package/dist/lib/mcp-call.js.map +1 -1
- package/dist/lib/mcp-connector-url.test.d.ts +1 -0
- package/dist/lib/mcp-connector-url.test.js +13 -0
- package/dist/lib/mcp-connector-url.test.js.map +1 -0
- package/dist/lib/mcp-snippets.d.ts +16 -0
- package/dist/lib/mcp-snippets.js +20 -2
- package/dist/lib/mcp-snippets.js.map +1 -1
- package/dist/lib/now-line.d.ts +66 -0
- package/dist/lib/now-line.js +193 -0
- package/dist/lib/now-line.js.map +1 -0
- package/dist/lib/now-line.test.d.ts +5 -0
- package/dist/lib/now-line.test.js +177 -0
- package/dist/lib/now-line.test.js.map +1 -0
- package/dist/lib/package-updates.d.ts +66 -0
- package/dist/lib/package-updates.js +76 -0
- package/dist/lib/package-updates.js.map +1 -0
- package/dist/lib/pod-moved.d.ts +70 -0
- package/dist/lib/pod-moved.js +146 -0
- package/dist/lib/pod-moved.js.map +1 -0
- package/dist/lib/pod-reach.d.ts +39 -0
- package/dist/lib/pod-reach.js +117 -0
- package/dist/lib/pod-reach.js.map +1 -0
- package/dist/lib/pod-reach.test.d.ts +1 -0
- package/dist/lib/pod-reach.test.js +67 -0
- package/dist/lib/pod-reach.test.js.map +1 -0
- package/dist/lib/pod.d.ts +8 -4
- package/dist/lib/pod.js +12 -10
- package/dist/lib/pod.js.map +1 -1
- package/dist/lib/register-claude-code-mcp.test.d.ts +1 -0
- package/dist/lib/register-claude-code-mcp.test.js +97 -0
- package/dist/lib/register-claude-code-mcp.test.js.map +1 -0
- package/dist/lib/review-credential.js +3 -1
- package/dist/lib/review-credential.js.map +1 -1
- package/dist/lib/safe-open.d.ts +5 -3
- package/dist/lib/safe-open.js +9 -8
- package/dist/lib/safe-open.js.map +1 -1
- package/dist/lib/self-host.d.ts +17 -0
- package/dist/lib/self-host.js +24 -0
- package/dist/lib/self-host.js.map +1 -0
- package/dist/lib/session-feed.d.ts +185 -0
- package/dist/lib/session-feed.js +257 -0
- package/dist/lib/session-feed.js.map +1 -0
- package/dist/lib/targets.d.ts +52 -3
- package/dist/lib/targets.js +150 -209
- package/dist/lib/targets.js.map +1 -1
- package/dist/lib/term.d.ts +24 -0
- package/dist/lib/term.js +52 -0
- package/dist/lib/term.js.map +1 -0
- package/dist/lib/term.test.d.ts +1 -0
- package/dist/lib/term.test.js +29 -0
- package/dist/lib/term.test.js.map +1 -0
- package/dist/lib/transcript-index.d.ts +94 -0
- package/dist/lib/transcript-index.js +321 -0
- package/dist/lib/transcript-index.js.map +1 -0
- package/dist/lib/transcript-index.test.d.ts +10 -0
- package/dist/lib/transcript-index.test.js +200 -0
- package/dist/lib/transcript-index.test.js.map +1 -0
- package/dist/lib/wait-help.d.ts +11 -0
- package/dist/lib/wait-help.js +25 -0
- package/dist/lib/wait-help.js.map +1 -0
- package/dist/lib/wait-state.d.ts +57 -0
- package/dist/lib/wait-state.js +114 -0
- package/dist/lib/wait-state.js.map +1 -0
- package/dist/lib/wake-hook-settings.d.ts +57 -0
- package/dist/lib/wake-hook-settings.js +153 -0
- package/dist/lib/wake-hook-settings.js.map +1 -0
- package/dist/lib/wake-state.d.ts +65 -0
- package/dist/lib/wake-state.js +122 -0
- package/dist/lib/wake-state.js.map +1 -0
- package/package.json +5 -2
- package/skills/README.md +1 -1
- package/skills/agent-os/SKILL.md +8 -8
- package/skills/synap/SKILL.md +178 -88
- package/skills/synap/code-skills.md +84 -0
- package/skills/synap/concepts.md +38 -20
- package/skills/synap/creative-loop.md +1 -1
- package/skills/synap/document-embeds.md +17 -1
- package/skills/synap/focus-sessions.md +17 -8
- package/skills/synap/from-intent.md +33 -17
- package/skills/synap/governance-rules.md +3 -1
- package/skills/synap/lenses.md +4 -4
- package/skills/synap/linking.md +2 -2
- package/skills/synap/mental-model.md +1 -1
- package/skills/synap/operating-mode-rules.md +1 -1
- package/skills/synap/quick-reference.md +2 -1
- package/skills/synap/reflexes.md +8 -8
- package/skills/synap/viewframe-cells.md +1 -1
- package/skills/synap/work-model.md +24 -10
- package/skills/synap/workspace-design.md +5 -5
- package/skills/synap/writes.md +16 -9
- package/skills/synap/writing-titles.md +7 -0
- package/skills/synap-app/SKILL.md +148 -66
- package/skills/synap-app/creating-the-base-app.md +12 -7
- package/skills/synap-app/governance.md +27 -16
- package/skills/synap-app/install-commands.md +18 -9
- package/skills/synap-app/overview.md +16 -9
- package/skills/synap-app/using-the-sdk.md +75 -25
- package/skills/synap-market/SKILL.md +1 -1
- package/skills/synap-market/governance-and-catalog.md +1 -1
- package/skills/synap-schema/SKILL.md +24 -21
- package/skills/synap-schema/intent-vocabulary-ssot.md +24 -21
- package/templates/box/runner.py +206 -0
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the box talks to: a small HTTP server on the provisioner's bridge
|
|
3
|
+
* address. The box holds no token: its identity IS its address on the isolated
|
|
4
|
+
* bridge (the provisioner assigned it), so a request is answered only for the
|
|
5
|
+
* run that address was given to.
|
|
6
|
+
*
|
|
7
|
+
* GET /job the script, its packages, parameters and wall clock
|
|
8
|
+
* POST /phase {"phase":"run"}: packages are in, egress narrows to the
|
|
9
|
+
* declared hosts (one way: never back to setup)
|
|
10
|
+
* POST /log a chunk of the run's stdout+stderr
|
|
11
|
+
* PUT /out/<path> one /out file, streamed to staging under the caps
|
|
12
|
+
* POST /done {"exitCode", "installedPackages"}
|
|
13
|
+
*/
|
|
14
|
+
import { createHash } from "node:crypto";
|
|
15
|
+
import fs from "node:fs";
|
|
16
|
+
import http from "node:http";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
import { LogCapture } from "./evidence.js";
|
|
19
|
+
/** BOX-CONTRACT v1: at most 20 files per run. */
|
|
20
|
+
export const MAX_OUT_FILES = 20;
|
|
21
|
+
const MAX_BODY_BYTES = 1024 * 1024;
|
|
22
|
+
/**
|
|
23
|
+
* A safe relative path for an /out file, or null. Each segment is decoded
|
|
24
|
+
* once; no empty, `.`, `..`, NUL or separator inside a segment.
|
|
25
|
+
*/
|
|
26
|
+
export function safeOutPath(urlPath) {
|
|
27
|
+
const raw = urlPath.replace(/^\/out\//, "");
|
|
28
|
+
// 500 = the pod's finish `files[].path` limit.
|
|
29
|
+
if (!raw || raw.length > 500)
|
|
30
|
+
return null;
|
|
31
|
+
const segments = [];
|
|
32
|
+
for (const enc of raw.split("/")) {
|
|
33
|
+
let seg;
|
|
34
|
+
try {
|
|
35
|
+
seg = decodeURIComponent(enc);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
if (!seg || seg === "." || seg === ".." || /[\0/\\]/.test(seg) || seg.length > 255)
|
|
41
|
+
return null;
|
|
42
|
+
segments.push(seg);
|
|
43
|
+
}
|
|
44
|
+
return segments.length <= 16 ? segments.join("/") : null;
|
|
45
|
+
}
|
|
46
|
+
/** The job document the box runner reads. */
|
|
47
|
+
export function jobFor(run) {
|
|
48
|
+
const s = run.script;
|
|
49
|
+
return {
|
|
50
|
+
runId: run.id,
|
|
51
|
+
language: s.language,
|
|
52
|
+
entrypoint: s.entrypoint,
|
|
53
|
+
code: s.code,
|
|
54
|
+
packages: s.packages,
|
|
55
|
+
parameters: run.parameters ?? null,
|
|
56
|
+
wallSeconds: s.limits.wallSeconds,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
export class BoxRegistry {
|
|
60
|
+
stagingRoot;
|
|
61
|
+
boxes = new Map();
|
|
62
|
+
/** Called once per box and event (the provisioner sets it per run: its clocks hang on it). */
|
|
63
|
+
onEvent = () => { };
|
|
64
|
+
constructor(stagingRoot) {
|
|
65
|
+
this.stagingRoot = stagingRoot;
|
|
66
|
+
}
|
|
67
|
+
register(ip, run) {
|
|
68
|
+
let finish;
|
|
69
|
+
const done = new Promise((r) => (finish = r));
|
|
70
|
+
const outDir = path.join(this.stagingRoot, run.id);
|
|
71
|
+
fs.rmSync(outDir, { recursive: true, force: true });
|
|
72
|
+
fs.mkdirSync(outDir, { recursive: true, mode: 0o700 });
|
|
73
|
+
const box = {
|
|
74
|
+
run,
|
|
75
|
+
phase: "setup",
|
|
76
|
+
log: new LogCapture(),
|
|
77
|
+
blocked: [],
|
|
78
|
+
outDir,
|
|
79
|
+
outBytes: 0,
|
|
80
|
+
outFiles: [],
|
|
81
|
+
jobFetched: false,
|
|
82
|
+
done,
|
|
83
|
+
finish,
|
|
84
|
+
};
|
|
85
|
+
this.boxes.set(ip, box);
|
|
86
|
+
return box;
|
|
87
|
+
}
|
|
88
|
+
/** Forget the box and drop its staged files. */
|
|
89
|
+
unregister(ip) {
|
|
90
|
+
const box = this.boxes.get(ip);
|
|
91
|
+
this.boxes.delete(ip);
|
|
92
|
+
if (box)
|
|
93
|
+
fs.rmSync(box.outDir, { recursive: true, force: true });
|
|
94
|
+
}
|
|
95
|
+
get(ip) {
|
|
96
|
+
return this.boxes.get(ip);
|
|
97
|
+
}
|
|
98
|
+
policyFor(ip) {
|
|
99
|
+
const b = this.boxes.get(ip);
|
|
100
|
+
return b ? { phase: b.phase, runHosts: b.run.script.hosts } : null;
|
|
101
|
+
}
|
|
102
|
+
recordBlock(ip, block) {
|
|
103
|
+
this.boxes.get(ip)?.blocked.push(block);
|
|
104
|
+
}
|
|
105
|
+
markJobFetched(ip) {
|
|
106
|
+
const b = this.boxes.get(ip);
|
|
107
|
+
if (!b || b.jobFetched)
|
|
108
|
+
return;
|
|
109
|
+
b.jobFetched = true;
|
|
110
|
+
this.onEvent(b.run, "job");
|
|
111
|
+
}
|
|
112
|
+
enterRunPhase(ip) {
|
|
113
|
+
const b = this.boxes.get(ip);
|
|
114
|
+
if (!b || b.phase === "run")
|
|
115
|
+
return;
|
|
116
|
+
b.phase = "run";
|
|
117
|
+
this.onEvent(b.run, "run");
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function readBody(req, cap = MAX_BODY_BYTES) {
|
|
121
|
+
return new Promise((resolve, reject) => {
|
|
122
|
+
const chunks = [];
|
|
123
|
+
let n = 0;
|
|
124
|
+
req.on("data", (c) => {
|
|
125
|
+
n += c.length;
|
|
126
|
+
if (n > cap) {
|
|
127
|
+
reject(new Error("body too large"));
|
|
128
|
+
req.destroy();
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
chunks.push(c);
|
|
132
|
+
});
|
|
133
|
+
req.on("end", () => resolve(Buffer.concat(chunks)));
|
|
134
|
+
req.on("error", reject);
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
function json(res, status, body) {
|
|
138
|
+
if (status === 204) {
|
|
139
|
+
res.writeHead(204).end();
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
res.writeHead(status, { "content-type": "application/json" }).end(JSON.stringify(body));
|
|
143
|
+
}
|
|
144
|
+
export function parseReport(raw) {
|
|
145
|
+
const body = (raw && typeof raw === "object" ? raw : {});
|
|
146
|
+
const exitCode = typeof body.exitCode === "number" && Number.isInteger(body.exitCode) ? body.exitCode : null;
|
|
147
|
+
const installedPackages = Array.isArray(body.installedPackages)
|
|
148
|
+
? body.installedPackages
|
|
149
|
+
.filter((p) => p && typeof p.name === "string" && typeof p.version === "string" && p.name && p.version)
|
|
150
|
+
.slice(0, 2000)
|
|
151
|
+
.map((p) => ({
|
|
152
|
+
name: p.name,
|
|
153
|
+
version: p.version,
|
|
154
|
+
manager: p.manager === "npm" ? "npm" : "pip",
|
|
155
|
+
}))
|
|
156
|
+
: [];
|
|
157
|
+
return { exitCode, installedPackages };
|
|
158
|
+
}
|
|
159
|
+
export function createJobServer(registry) {
|
|
160
|
+
return http.createServer(async (req, res) => {
|
|
161
|
+
const ip = (req.socket.remoteAddress ?? "").replace(/^::ffff:/, "");
|
|
162
|
+
const box = registry.get(ip);
|
|
163
|
+
if (!box)
|
|
164
|
+
return json(res, 403, { error: "not a running box" });
|
|
165
|
+
const url = new URL(req.url ?? "/", "http://box");
|
|
166
|
+
try {
|
|
167
|
+
if (req.method === "GET" && url.pathname === "/job") {
|
|
168
|
+
registry.markJobFetched(ip);
|
|
169
|
+
return json(res, 200, jobFor(box.run));
|
|
170
|
+
}
|
|
171
|
+
if (req.method === "POST" && url.pathname === "/phase") {
|
|
172
|
+
const body = JSON.parse((await readBody(req)).toString("utf8") || "{}");
|
|
173
|
+
if (body.phase !== "run")
|
|
174
|
+
return json(res, 400, { error: "the only move is to the run phase" });
|
|
175
|
+
registry.enterRunPhase(ip);
|
|
176
|
+
return json(res, 200, { phase: "run" });
|
|
177
|
+
}
|
|
178
|
+
if (req.method === "POST" && url.pathname === "/log") {
|
|
179
|
+
req.on("data", (c) => box.log.append(c));
|
|
180
|
+
req.on("end", () => json(res, 204));
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (req.method === "PUT" && url.pathname.startsWith("/out/")) {
|
|
184
|
+
const rel = safeOutPath(url.pathname);
|
|
185
|
+
if (!rel)
|
|
186
|
+
return json(res, 400, { error: "bad out path" });
|
|
187
|
+
if (box.outFiles.length >= MAX_OUT_FILES)
|
|
188
|
+
return json(res, 413, { error: `more than ${MAX_OUT_FILES} files` });
|
|
189
|
+
const cap = box.run.script.limits.outMb * 1024 * 1024;
|
|
190
|
+
const dest = path.join(box.outDir, rel);
|
|
191
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
192
|
+
const hash = createHash("sha256");
|
|
193
|
+
const file = fs.createWriteStream(dest, { mode: 0o600 });
|
|
194
|
+
let size = 0;
|
|
195
|
+
let over = false;
|
|
196
|
+
// Over the cap or cut off mid-upload: no partial file is kept, and the
|
|
197
|
+
// answer goes out only once it is gone.
|
|
198
|
+
const abort = (status, error) => {
|
|
199
|
+
if (over)
|
|
200
|
+
return;
|
|
201
|
+
over = true;
|
|
202
|
+
file.destroy();
|
|
203
|
+
file.once("close", () => {
|
|
204
|
+
fs.rmSync(dest, { force: true });
|
|
205
|
+
if (!res.headersSent)
|
|
206
|
+
json(res, status, { error });
|
|
207
|
+
req.destroy();
|
|
208
|
+
});
|
|
209
|
+
};
|
|
210
|
+
// A write that fails (disk full), or a pending write cut short by the
|
|
211
|
+
// destroy above: one answer either way, never an unhandled 'error'
|
|
212
|
+
// (which would crash the provisioner and leave the box unanswered).
|
|
213
|
+
file.on("error", () => abort(500, "could not stage the file"));
|
|
214
|
+
req.on("data", (c) => {
|
|
215
|
+
if (over)
|
|
216
|
+
return;
|
|
217
|
+
size += c.length;
|
|
218
|
+
if (box.outBytes + size > cap)
|
|
219
|
+
return abort(413, `/out exceeds ${box.run.script.limits.outMb} MB`);
|
|
220
|
+
hash.update(c);
|
|
221
|
+
file.write(c);
|
|
222
|
+
});
|
|
223
|
+
req.on("end", () => {
|
|
224
|
+
if (over)
|
|
225
|
+
return;
|
|
226
|
+
file.end(() => {
|
|
227
|
+
box.outBytes += size;
|
|
228
|
+
box.outFiles.push({ path: rel, size, sha256: hash.digest("hex") });
|
|
229
|
+
json(res, 201, { path: rel, size });
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
req.on("close", () => {
|
|
233
|
+
if (!req.complete)
|
|
234
|
+
abort(400, "upload cut off");
|
|
235
|
+
});
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
if (req.method === "POST" && url.pathname === "/done") {
|
|
239
|
+
box.finish(parseReport(JSON.parse((await readBody(req)).toString("utf8"))));
|
|
240
|
+
return json(res, 200, { ok: true });
|
|
241
|
+
}
|
|
242
|
+
return json(res, 404, { error: "no such door" });
|
|
243
|
+
}
|
|
244
|
+
catch (err) {
|
|
245
|
+
if (!res.headersSent)
|
|
246
|
+
json(res, 400, { error: err.message });
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
//# sourceMappingURL=job-server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"job-server.js","sourceRoot":"","sources":["../../../src/lib/box/job-server.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The provisioner's loop: one box per run, one run at a time (phase 0).
|
|
3
|
+
*
|
|
4
|
+
* guards ok? ─no─▶ wait (the run keeps its turn: nothing is leased)
|
|
5
|
+
* lease ─▶ clone + start a box ─▶ the box fetches its job, installs, runs
|
|
6
|
+
* ─▶ done | a clock ran out | Stop | lease lost | provisioner stopping
|
|
7
|
+
* destroy the box, ALWAYS ─▶ upload /out ─▶ finish with the evidence
|
|
8
|
+
*
|
|
9
|
+
* Three clocks, one after the other: BOOT (the box must ask for its job, else
|
|
10
|
+
* INFRA: it never started), SETUP (the declared packages must install, else
|
|
11
|
+
* timeout), and the script's own wall clock from the moment it starts.
|
|
12
|
+
*
|
|
13
|
+
* A lost lease (404/409) ends the work without a finish: the pod already gave
|
|
14
|
+
* the run up. A reaper destroys every tagged box no run holds, at start and
|
|
15
|
+
* after every run, so a crash never leaves a box behind for long.
|
|
16
|
+
*/
|
|
17
|
+
import type { BoxRegistry } from "./job-server.js";
|
|
18
|
+
import type { BoxBackend, HostReading } from "./proxmox.js";
|
|
19
|
+
import { type BoxHub, type BoxLimits, type FinishBody, type LeasedRun } from "./wire.js";
|
|
20
|
+
export interface HostGuards {
|
|
21
|
+
/** Wait unless the host has at least this much MemAvailable. */
|
|
22
|
+
minMemAvailableMb: number;
|
|
23
|
+
/** Wait while the box storage (thin pool data) is fuller than this. */
|
|
24
|
+
maxStorageUsedPct: number;
|
|
25
|
+
/** Wait while the thin pool's metadata is fuller than this. */
|
|
26
|
+
maxMetaUsedPct: number;
|
|
27
|
+
}
|
|
28
|
+
/** Phase 0 on the founder's host: swap is full, so the margin is RAM only. */
|
|
29
|
+
export declare const DEFAULT_GUARDS: HostGuards;
|
|
30
|
+
/**
|
|
31
|
+
* BOX-CONTRACT "Phase 0 caps". The pod enforces them when a run is queued; the
|
|
32
|
+
* provisioner re-applies them because the host is the person's, not the pod's.
|
|
33
|
+
*/
|
|
34
|
+
export declare const PHASE0_CAPS: BoxLimits;
|
|
35
|
+
/** A box that has not asked for its job by then did not start: INFRA, not the script's verdict. */
|
|
36
|
+
export declare const BOOT_SECONDS = 180;
|
|
37
|
+
/** The declared packages must be installed by then; past it the run timed out in setup. */
|
|
38
|
+
export declare const SETUP_SECONDS = 900;
|
|
39
|
+
/** Limits cut to the phase 0 caps, and the names of the ones that were cut. */
|
|
40
|
+
export declare function clampLimits(l: BoxLimits): {
|
|
41
|
+
limits: BoxLimits;
|
|
42
|
+
clamped: string[];
|
|
43
|
+
};
|
|
44
|
+
export declare function checkGuards(r: HostReading, g: HostGuards): {
|
|
45
|
+
ok: true;
|
|
46
|
+
} | {
|
|
47
|
+
ok: false;
|
|
48
|
+
reason: string;
|
|
49
|
+
};
|
|
50
|
+
export interface ProvisionerDeps {
|
|
51
|
+
hub: BoxHub;
|
|
52
|
+
backend: BoxBackend;
|
|
53
|
+
registry: BoxRegistry;
|
|
54
|
+
log: (line: string) => void;
|
|
55
|
+
now?: () => number;
|
|
56
|
+
/** Destroy attempts before leaving a box to the reaper. */
|
|
57
|
+
destroyAttempts?: number;
|
|
58
|
+
/** The provisioner is stopping: end the current run (box destroyed, finish INFRA). */
|
|
59
|
+
signal?: AbortSignal;
|
|
60
|
+
/** Clock overrides (tests). */
|
|
61
|
+
bootMs?: number;
|
|
62
|
+
setupMs?: number;
|
|
63
|
+
}
|
|
64
|
+
export declare function mimeOf(file: string): string;
|
|
65
|
+
/** Destroy every tagged box no run holds. Returns the VMIDs destroyed. */
|
|
66
|
+
export declare function reap(backend: BoxBackend, held: ReadonlySet<number>, log: (l: string) => void): Promise<number[]>;
|
|
67
|
+
/**
|
|
68
|
+
* Work one leased run to its end (BOX-CONTRACT v2). Returns the finish body sent, or null when
|
|
69
|
+
* the lease was lost (no finish is sent then).
|
|
70
|
+
*/
|
|
71
|
+
export declare function runOne(leasedRun: LeasedRun, deps: ProvisionerDeps): Promise<FinishBody | null>;
|
|
72
|
+
export interface ServeOptions {
|
|
73
|
+
guards: HostGuards;
|
|
74
|
+
/** Wait between empty leases. */
|
|
75
|
+
idleMs: number;
|
|
76
|
+
/** Wait between guard readings while the host is short. */
|
|
77
|
+
guardWaitMs: number;
|
|
78
|
+
once?: boolean;
|
|
79
|
+
signal?: AbortSignal;
|
|
80
|
+
sleep?: (ms: number) => Promise<void>;
|
|
81
|
+
}
|
|
82
|
+
/** Lease and work runs until the signal aborts (or one pass with `once`). */
|
|
83
|
+
export declare function serve(deps: ProvisionerDeps, opts: ServeOptions): Promise<void>;
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The provisioner's loop: one box per run, one run at a time (phase 0).
|
|
3
|
+
*
|
|
4
|
+
* guards ok? ─no─▶ wait (the run keeps its turn: nothing is leased)
|
|
5
|
+
* lease ─▶ clone + start a box ─▶ the box fetches its job, installs, runs
|
|
6
|
+
* ─▶ done | a clock ran out | Stop | lease lost | provisioner stopping
|
|
7
|
+
* destroy the box, ALWAYS ─▶ upload /out ─▶ finish with the evidence
|
|
8
|
+
*
|
|
9
|
+
* Three clocks, one after the other: BOOT (the box must ask for its job, else
|
|
10
|
+
* INFRA: it never started), SETUP (the declared packages must install, else
|
|
11
|
+
* timeout), and the script's own wall clock from the moment it starts.
|
|
12
|
+
*
|
|
13
|
+
* A lost lease (404/409) ends the work without a finish: the pod already gave
|
|
14
|
+
* the run up. A reaper destroys every tagged box no run holds, at start and
|
|
15
|
+
* after every run, so a crash never leaves a box behind for long.
|
|
16
|
+
*/
|
|
17
|
+
import fs from "node:fs";
|
|
18
|
+
import path from "node:path";
|
|
19
|
+
import { assembleFinish, LogCapture } from "./evidence.js";
|
|
20
|
+
import { LeaseLostError, WorkspaceRequiredError, } from "./wire.js";
|
|
21
|
+
/** Phase 0 on the founder's host: swap is full, so the margin is RAM only. */
|
|
22
|
+
export const DEFAULT_GUARDS = { minMemAvailableMb: 6144, maxStorageUsedPct: 85, maxMetaUsedPct: 70 };
|
|
23
|
+
/**
|
|
24
|
+
* BOX-CONTRACT "Phase 0 caps". The pod enforces them when a run is queued; the
|
|
25
|
+
* provisioner re-applies them because the host is the person's, not the pod's.
|
|
26
|
+
*/
|
|
27
|
+
export const PHASE0_CAPS = { wallSeconds: 3600, cpus: 2, memoryMb: 2048, diskMb: 8192, outMb: 200 };
|
|
28
|
+
/** A box that has not asked for its job by then did not start: INFRA, not the script's verdict. */
|
|
29
|
+
export const BOOT_SECONDS = 180;
|
|
30
|
+
/** The declared packages must be installed by then; past it the run timed out in setup. */
|
|
31
|
+
export const SETUP_SECONDS = 900;
|
|
32
|
+
/** Limits cut to the phase 0 caps, and the names of the ones that were cut. */
|
|
33
|
+
export function clampLimits(l) {
|
|
34
|
+
const clamped = [];
|
|
35
|
+
const out = { ...l };
|
|
36
|
+
for (const k of Object.keys(PHASE0_CAPS)) {
|
|
37
|
+
const v = Number.isFinite(l[k]) && l[k] > 0 ? l[k] : PHASE0_CAPS[k];
|
|
38
|
+
out[k] = Math.min(v, PHASE0_CAPS[k]);
|
|
39
|
+
if (out[k] !== l[k])
|
|
40
|
+
clamped.push(`${k} ${l[k]} → ${out[k]}`);
|
|
41
|
+
}
|
|
42
|
+
return { limits: out, clamped };
|
|
43
|
+
}
|
|
44
|
+
export function checkGuards(r, g) {
|
|
45
|
+
if (r.memAvailableMb < g.minMemAvailableMb) {
|
|
46
|
+
return { ok: false, reason: `host MemAvailable ${r.memAvailableMb} MB < ${g.minMemAvailableMb} MB` };
|
|
47
|
+
}
|
|
48
|
+
if (r.storageUsedPct > g.maxStorageUsedPct) {
|
|
49
|
+
return { ok: false, reason: `box storage ${r.storageUsedPct.toFixed(1)}% full > ${g.maxStorageUsedPct}%` };
|
|
50
|
+
}
|
|
51
|
+
if (r.metaUsedPct !== null && r.metaUsedPct > g.maxMetaUsedPct) {
|
|
52
|
+
return { ok: false, reason: `thin pool metadata ${r.metaUsedPct.toFixed(1)}% full > ${g.maxMetaUsedPct}%` };
|
|
53
|
+
}
|
|
54
|
+
return { ok: true };
|
|
55
|
+
}
|
|
56
|
+
const MIME = {
|
|
57
|
+
".png": "image/png",
|
|
58
|
+
".jpg": "image/jpeg",
|
|
59
|
+
".jpeg": "image/jpeg",
|
|
60
|
+
".gif": "image/gif",
|
|
61
|
+
".svg": "image/svg+xml",
|
|
62
|
+
".webp": "image/webp",
|
|
63
|
+
".pdf": "application/pdf",
|
|
64
|
+
".csv": "text/csv",
|
|
65
|
+
".json": "application/json",
|
|
66
|
+
".txt": "text/plain",
|
|
67
|
+
".md": "text/markdown",
|
|
68
|
+
".html": "text/html",
|
|
69
|
+
".zip": "application/zip",
|
|
70
|
+
".mp4": "video/mp4",
|
|
71
|
+
".mp3": "audio/mpeg",
|
|
72
|
+
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
73
|
+
};
|
|
74
|
+
export function mimeOf(file) {
|
|
75
|
+
return MIME[path.extname(file).toLowerCase()] ?? "application/octet-stream";
|
|
76
|
+
}
|
|
77
|
+
/** Destroy every tagged box no run holds. Returns the VMIDs destroyed. */
|
|
78
|
+
export async function reap(backend, held, log) {
|
|
79
|
+
const gone = [];
|
|
80
|
+
for (const b of await backend.list()) {
|
|
81
|
+
if (held.has(b.vmid))
|
|
82
|
+
continue;
|
|
83
|
+
try {
|
|
84
|
+
await backend.destroy(b.vmid);
|
|
85
|
+
gone.push(b.vmid);
|
|
86
|
+
log(`reaper: destroyed stray box ${b.vmid} (${b.name})`);
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
log(`reaper: could not destroy box ${b.vmid}: ${err.message}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return gone;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Work one leased run to its end (BOX-CONTRACT v2). Returns the finish body sent, or null when
|
|
96
|
+
* the lease was lost (no finish is sent then).
|
|
97
|
+
*/
|
|
98
|
+
export async function runOne(leasedRun, deps) {
|
|
99
|
+
const now = deps.now ?? Date.now;
|
|
100
|
+
const startedAt = now();
|
|
101
|
+
const { limits, clamped } = clampLimits(leasedRun.script.limits);
|
|
102
|
+
const run = { ...leasedRun, script: { ...leasedRun.script, limits } };
|
|
103
|
+
const bootMs = deps.bootMs ?? BOOT_SECONDS * 1000;
|
|
104
|
+
const setupMs = deps.setupMs ?? SETUP_SECONDS * 1000;
|
|
105
|
+
let phase = "setup";
|
|
106
|
+
let stopped = false;
|
|
107
|
+
let lost = false;
|
|
108
|
+
let timedOut = false;
|
|
109
|
+
let infraReason;
|
|
110
|
+
/** Said in the run's log once a box exists (a clock that ran out, a cap that cut a limit). */
|
|
111
|
+
const notes = clamped.length ? [`limits cut to the phase 0 caps: ${clamped.join(", ")}`] : [];
|
|
112
|
+
let wake = () => { };
|
|
113
|
+
const interrupted = new Promise((r) => (wake = r));
|
|
114
|
+
const beat = async () => {
|
|
115
|
+
try {
|
|
116
|
+
const reply = await deps.hub.heartbeat(run, phase);
|
|
117
|
+
if (reply.stopRequested && !stopped) {
|
|
118
|
+
stopped = true;
|
|
119
|
+
deps.log(`run ${run.id}: Stop requested`);
|
|
120
|
+
wake();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
if (err instanceof LeaseLostError) {
|
|
125
|
+
lost = true;
|
|
126
|
+
deps.log(`run ${run.id}: ${err.message}`);
|
|
127
|
+
wake();
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
// A transient failure: the next beat retries before the lease runs out.
|
|
131
|
+
deps.log(`run ${run.id}: heartbeat failed: ${err.message}`);
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
const onAbort = () => {
|
|
135
|
+
infraReason ??= "the provisioner was stopped mid-run";
|
|
136
|
+
wake();
|
|
137
|
+
};
|
|
138
|
+
deps.signal?.addEventListener("abort", onAbort);
|
|
139
|
+
let clock;
|
|
140
|
+
const arm = (ms, fire) => {
|
|
141
|
+
clearTimeout(clock);
|
|
142
|
+
clock = setTimeout(() => {
|
|
143
|
+
fire();
|
|
144
|
+
wake();
|
|
145
|
+
}, ms);
|
|
146
|
+
};
|
|
147
|
+
await beat();
|
|
148
|
+
if (lost) {
|
|
149
|
+
deps.signal?.removeEventListener("abort", onAbort);
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
const timer = setInterval(() => void beat(), Math.max(5, run.heartbeatIntervalSeconds) * 1000);
|
|
153
|
+
deps.registry.onEvent = (r, event) => {
|
|
154
|
+
if (r.id !== run.id)
|
|
155
|
+
return;
|
|
156
|
+
if (event === "job") {
|
|
157
|
+
arm(setupMs, () => {
|
|
158
|
+
timedOut = true;
|
|
159
|
+
notes.push(`packages were still installing after ${Math.round(setupMs / 1000)} s: the box was destroyed`);
|
|
160
|
+
});
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
phase = "running";
|
|
164
|
+
void beat();
|
|
165
|
+
arm(limits.wallSeconds * 1000, () => {
|
|
166
|
+
timedOut = true;
|
|
167
|
+
notes.push(`the script ran past its ${limits.wallSeconds} s wall clock: the box was destroyed`);
|
|
168
|
+
});
|
|
169
|
+
};
|
|
170
|
+
let ip = null;
|
|
171
|
+
let vmid = null;
|
|
172
|
+
let report = { exitCode: null, installedPackages: [] };
|
|
173
|
+
try {
|
|
174
|
+
// A Stop, a lost lease or a stopping provisioner before the clone: no box at all.
|
|
175
|
+
if (!stopped && !lost && !deps.signal?.aborted) {
|
|
176
|
+
try {
|
|
177
|
+
const handle = await deps.backend.create(run.id, { cpus: limits.cpus, memoryMb: limits.memoryMb });
|
|
178
|
+
ip = handle.ip;
|
|
179
|
+
vmid = handle.vmid;
|
|
180
|
+
}
|
|
181
|
+
catch (err) {
|
|
182
|
+
infraReason = `clone failed: ${err.message}`;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (ip) {
|
|
186
|
+
const box = deps.registry.register(ip, run);
|
|
187
|
+
deps.log(`run ${run.id}: box ${vmid} at ${ip}`);
|
|
188
|
+
arm(bootMs, () => {
|
|
189
|
+
infraReason ??= `the box did not ask for its job within ${Math.round(bootMs / 1000)} s (boot or template problem)`;
|
|
190
|
+
});
|
|
191
|
+
const first = await Promise.race([box.done.then((d) => ({ d })), interrupted]);
|
|
192
|
+
if (first && "d" in first)
|
|
193
|
+
report = first.d;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
finally {
|
|
197
|
+
clearTimeout(clock);
|
|
198
|
+
deps.registry.onEvent = () => { };
|
|
199
|
+
deps.signal?.removeEventListener("abort", onAbort);
|
|
200
|
+
if (vmid !== null) {
|
|
201
|
+
const attempts = deps.destroyAttempts ?? 3;
|
|
202
|
+
for (let i = 1; i <= attempts; i++) {
|
|
203
|
+
try {
|
|
204
|
+
await deps.backend.destroy(vmid);
|
|
205
|
+
deps.log(`run ${run.id}: box ${vmid} destroyed`);
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
catch (err) {
|
|
209
|
+
deps.log(`run ${run.id}: destroy ${vmid} attempt ${i} failed: ${err.message}`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
const box = ip ? deps.registry.get(ip) : undefined;
|
|
215
|
+
const log = box?.log ?? new LogCapture();
|
|
216
|
+
for (const n of notes)
|
|
217
|
+
log.append(`\n[synap box] ${n}\n`);
|
|
218
|
+
try {
|
|
219
|
+
if (lost)
|
|
220
|
+
return null;
|
|
221
|
+
const files = [];
|
|
222
|
+
let storable = true;
|
|
223
|
+
for (const f of box?.outFiles ?? []) {
|
|
224
|
+
try {
|
|
225
|
+
if (!storable)
|
|
226
|
+
throw new WorkspaceRequiredError();
|
|
227
|
+
// The pod's upload door takes no empty file: listed in the evidence only.
|
|
228
|
+
if (f.size === 0)
|
|
229
|
+
throw new Error("the file is empty");
|
|
230
|
+
const bytes = fs.readFileSync(path.join(box.outDir, f.path));
|
|
231
|
+
const uploadToken = await deps.hub.upload(run, { filename: path.basename(f.path), mimeType: mimeOf(f.path), bytes });
|
|
232
|
+
files.push({ ...f, uploadToken, title: f.path });
|
|
233
|
+
}
|
|
234
|
+
catch (err) {
|
|
235
|
+
if (err instanceof LeaseLostError)
|
|
236
|
+
return null;
|
|
237
|
+
if (err instanceof WorkspaceRequiredError)
|
|
238
|
+
storable = false;
|
|
239
|
+
// Recorded without a token (evidence only) and said in the log the
|
|
240
|
+
// person reads, so a missing file is never silent.
|
|
241
|
+
files.push({ ...f });
|
|
242
|
+
log.append(`\n[synap box] /out/${f.path} (${f.size} bytes) was not stored: ${err.message}\n`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
const outcome = { exitCode: report.exitCode, timedOut, stopped, infraReason };
|
|
246
|
+
const body = assembleFinish({
|
|
247
|
+
leaseId: run.leaseId,
|
|
248
|
+
outcome,
|
|
249
|
+
startedAt,
|
|
250
|
+
finishedAt: now(),
|
|
251
|
+
log,
|
|
252
|
+
blockedEgress: box?.blocked ?? [],
|
|
253
|
+
installedPackages: report.installedPackages,
|
|
254
|
+
files,
|
|
255
|
+
});
|
|
256
|
+
try {
|
|
257
|
+
await deps.hub.finish(run, body);
|
|
258
|
+
deps.log(`run ${run.id}: finished ${body.status}`);
|
|
259
|
+
}
|
|
260
|
+
catch (err) {
|
|
261
|
+
deps.log(`run ${run.id}: finish failed: ${err.message}`);
|
|
262
|
+
if (err instanceof LeaseLostError)
|
|
263
|
+
return null;
|
|
264
|
+
throw err;
|
|
265
|
+
}
|
|
266
|
+
return body;
|
|
267
|
+
}
|
|
268
|
+
finally {
|
|
269
|
+
clearInterval(timer);
|
|
270
|
+
if (ip)
|
|
271
|
+
deps.registry.unregister(ip);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
const realSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
275
|
+
/** Lease and work runs until the signal aborts (or one pass with `once`). */
|
|
276
|
+
export async function serve(deps, opts) {
|
|
277
|
+
const sleep = opts.sleep ?? realSleep;
|
|
278
|
+
await reap(deps.backend, new Set(), deps.log);
|
|
279
|
+
let lastWait = "";
|
|
280
|
+
while (!opts.signal?.aborted) {
|
|
281
|
+
try {
|
|
282
|
+
const guard = checkGuards(await deps.backend.readHost(), opts.guards);
|
|
283
|
+
if (!guard.ok) {
|
|
284
|
+
if (guard.reason !== lastWait)
|
|
285
|
+
deps.log(`waiting, turn kept: ${guard.reason}`);
|
|
286
|
+
lastWait = guard.reason;
|
|
287
|
+
if (opts.once)
|
|
288
|
+
return;
|
|
289
|
+
await sleep(opts.guardWaitMs);
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
lastWait = "";
|
|
293
|
+
const run = await deps.hub.lease();
|
|
294
|
+
if (run) {
|
|
295
|
+
deps.log(`leased run ${run.id} (${run.script.name}, ${run.script.language})`);
|
|
296
|
+
await runOne(run, { ...deps, signal: opts.signal });
|
|
297
|
+
await reap(deps.backend, new Set(), deps.log);
|
|
298
|
+
}
|
|
299
|
+
if (opts.once)
|
|
300
|
+
return;
|
|
301
|
+
if (!run)
|
|
302
|
+
await sleep(opts.idleMs);
|
|
303
|
+
}
|
|
304
|
+
catch (err) {
|
|
305
|
+
deps.log(`error: ${err.message}`);
|
|
306
|
+
if (opts.once)
|
|
307
|
+
throw err;
|
|
308
|
+
await sleep(opts.guardWaitMs);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
//# sourceMappingURL=provisioner.js.map
|