@synap-core/cli 1.12.2 → 1.13.0
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 +25 -8
- package/dist/build-info.json +2 -2
- package/dist/commands/agents.d.ts +0 -1
- package/dist/commands/agents.js +1 -1
- 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 +1 -1
- package/dist/commands/capability.js +57 -34
- 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.js +12 -4
- 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 +21 -0
- package/dist/commands/doctor.js +108 -13
- 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 +59 -19
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/knowledge.js +4 -2
- 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/mcp.js +2 -2
- 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 +32 -15
- package/dist/commands/pods.js.map +1 -1
- package/dist/commands/proton-setup.js +3 -1
- package/dist/commands/proton-setup.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 +1 -1
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/template.d.ts +42 -0
- package/dist/commands/template.js +191 -0
- package/dist/commands/template.js.map +1 -0
- package/dist/commands/tracks.d.ts +67 -6
- package/dist/commands/tracks.js +268 -16
- package/dist/commands/tracks.js.map +1 -1
- 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.d.ts +219 -10
- package/dist/commands/worker.js +749 -117
- package/dist/commands/worker.js.map +1 -1
- package/dist/commands/worker.test.js +321 -10
- package/dist/commands/worker.test.js.map +1 -1
- package/dist/index.js +164 -27
- 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/auth.d.ts +3 -2
- package/dist/lib/auth.js +3 -3
- 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 +246 -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/browser-auth.d.ts +35 -0
- package/dist/lib/browser-auth.js +170 -0
- package/dist/lib/browser-auth.js.map +1 -0
- package/dist/lib/client-header.test.d.ts +1 -0
- package/dist/lib/client-header.test.js +85 -0
- package/dist/lib/client-header.test.js.map +1 -0
- package/dist/lib/conversation-headers.d.ts +24 -9
- package/dist/lib/conversation-headers.js +40 -3
- package/dist/lib/conversation-headers.js.map +1 -1
- package/dist/lib/credential-class.d.ts +28 -1
- package/dist/lib/credential-class.js +28 -6
- package/dist/lib/credential-class.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 +112 -0
- package/dist/lib/goal-words.js.map +1 -0
- package/dist/lib/hub-client.d.ts +28 -7
- package/dist/lib/hub-client.js +75 -34
- 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-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/mcp-bridge.d.ts +7 -0
- package/dist/lib/mcp-bridge.js +34 -1
- package/dist/lib/mcp-bridge.js.map +1 -1
- package/dist/lib/mcp-bridge.test.js +31 -3
- package/dist/lib/mcp-bridge.test.js.map +1 -1
- package/dist/lib/openclaw.d.ts +32 -0
- package/dist/lib/openclaw.js +165 -0
- package/dist/lib/openclaw.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 -0
- package/dist/lib/pod.js +12 -3
- 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 +78 -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/seed.d.ts +13 -0
- package/dist/lib/seed.js +135 -0
- package/dist/lib/seed.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 +21 -3
- package/dist/lib/targets.js +52 -107
- package/dist/lib/targets.js.map +1 -1
- 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 +141 -84
- package/skills/synap/code-skills.md +70 -0
- package/skills/synap/concepts.md +28 -20
- package/skills/synap/creative-loop.md +1 -1
- package/skills/synap/document-embeds.md +3 -1
- package/skills/synap/focus-sessions.md +16 -7
- package/skills/synap/from-intent.md +32 -16
- package/skills/synap/governance-rules.md +1 -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 +6 -6
- 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-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-schema/SKILL.md +21 -21
- package/skills/synap-schema/intent-vocabulary-ssot.md +21 -21
- package/templates/box/runner.py +206 -0
|
@@ -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
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provisioner.js","sourceRoot":"","sources":["../../../src/lib/box/provisioner.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Proxmox backend of the run box: one fresh unprivileged LXC per run,
|
|
3
|
+
* linked-cloned from a template CT, on the isolated box bridge, destroyed after.
|
|
4
|
+
*
|
|
5
|
+
* It talks to the Proxmox API with a POOL-SCOPED token (least privilege: it
|
|
6
|
+
* can clone, configure, start, stop and destroy guests of ONE pool, and read
|
|
7
|
+
* node memory, one storage's fill and the thin pools' fill — nothing else;
|
|
8
|
+
* host side in `my server` docs/RUNBOX.md, `scripts/box/setup-box-access.sh`). TLS is verified against the host's own CA, pinned by file,
|
|
9
|
+
* never `rejectUnauthorized: false`.
|
|
10
|
+
*/
|
|
11
|
+
export interface ProxmoxConfig {
|
|
12
|
+
/** e.g. https://10.10.0.1:8006 */
|
|
13
|
+
endpoint: string;
|
|
14
|
+
/** `user@realm!tokenid=secret` */
|
|
15
|
+
token: string;
|
|
16
|
+
/** PEM of the host CA (/etc/pve/pve-root-ca.pem). */
|
|
17
|
+
ca: string;
|
|
18
|
+
node: string;
|
|
19
|
+
/**
|
|
20
|
+
* The name the host certificate is checked against when the endpoint is an
|
|
21
|
+
* address it does not list. The founder's host cert names 192.168.1.201,
|
|
22
|
+
* `main` and `main.local.lan`, not 10.10.0.1 (read 2026-10-26), so the
|
|
23
|
+
* provisioner connects to 10.10.0.1 and verifies the cert as `main`.
|
|
24
|
+
*/
|
|
25
|
+
serverName?: string;
|
|
26
|
+
}
|
|
27
|
+
export interface ProxmoxApi {
|
|
28
|
+
get<T>(path: string): Promise<T>;
|
|
29
|
+
post<T>(path: string, params?: Record<string, string | number>): Promise<T>;
|
|
30
|
+
put<T>(path: string, params?: Record<string, string | number>): Promise<T>;
|
|
31
|
+
del<T>(path: string, params?: Record<string, string | number>): Promise<T>;
|
|
32
|
+
}
|
|
33
|
+
export declare class ProxmoxError extends Error {
|
|
34
|
+
readonly status: number;
|
|
35
|
+
constructor(status: number, message: string);
|
|
36
|
+
}
|
|
37
|
+
export declare function createProxmoxApi(cfg: ProxmoxConfig): ProxmoxApi;
|
|
38
|
+
export interface BoxHostConfig {
|
|
39
|
+
/** VMID of the template CT (a Proxmox template, linked clones only). */
|
|
40
|
+
templateVmid: number;
|
|
41
|
+
/** The pool every box is created in; the token can touch nothing else. */
|
|
42
|
+
pool: string;
|
|
43
|
+
/** Storage the template and its linked clones live on (the fill guard reads it). */
|
|
44
|
+
storage: string;
|
|
45
|
+
/**
|
|
46
|
+
* The LVM thin pool behind `storage`, `vg/lv` (e.g. `pve/data`), for the
|
|
47
|
+
* metadata guard. Unset = no metadata guard (said once at start).
|
|
48
|
+
*/
|
|
49
|
+
thinpool?: string;
|
|
50
|
+
/** The isolated box bridge. */
|
|
51
|
+
bridge: string;
|
|
52
|
+
/** First three octets of the box subnet, e.g. "10.20.0". */
|
|
53
|
+
subnetPrefix: string;
|
|
54
|
+
/** VMIDs reserved for boxes, inclusive. */
|
|
55
|
+
vmidMin: number;
|
|
56
|
+
vmidMax: number;
|
|
57
|
+
cpuunits: number;
|
|
58
|
+
}
|
|
59
|
+
/** Every box carries this tag; the reaper touches nothing without it. */
|
|
60
|
+
export declare const BOX_TAG = "synap-box";
|
|
61
|
+
export interface BoxHandle {
|
|
62
|
+
vmid: number;
|
|
63
|
+
ip: string;
|
|
64
|
+
}
|
|
65
|
+
export interface HostReading {
|
|
66
|
+
memAvailableMb: number;
|
|
67
|
+
storageUsedPct: number;
|
|
68
|
+
/** Thin pool metadata fill; null only when no `thinpool` is configured (never for a failed read: that throws). */
|
|
69
|
+
metaUsedPct: number | null;
|
|
70
|
+
}
|
|
71
|
+
export interface ListedBox {
|
|
72
|
+
vmid: number;
|
|
73
|
+
name: string;
|
|
74
|
+
}
|
|
75
|
+
export interface BoxBackend {
|
|
76
|
+
readHost(): Promise<HostReading>;
|
|
77
|
+
/** Clone, size, attach to the bridge and start a box for one run. */
|
|
78
|
+
create(runId: string, limits: {
|
|
79
|
+
cpus: number;
|
|
80
|
+
memoryMb: number;
|
|
81
|
+
}): Promise<BoxHandle>;
|
|
82
|
+
/** Stop and destroy; idempotent (a box already gone is success). */
|
|
83
|
+
destroy(vmid: number): Promise<void>;
|
|
84
|
+
/** Every tagged box in the pool (the reaper destroys those no run holds). */
|
|
85
|
+
list(): Promise<ListedBox[]>;
|
|
86
|
+
}
|
|
87
|
+
/** The IP a box gets on the bridge, by its VMID slot (.10 upward). */
|
|
88
|
+
export declare function boxIpFor(cfg: BoxHostConfig, vmid: number): string;
|
|
89
|
+
type ClusterGuest = {
|
|
90
|
+
vmid?: number;
|
|
91
|
+
type?: string;
|
|
92
|
+
name?: string;
|
|
93
|
+
pool?: string;
|
|
94
|
+
tags?: string;
|
|
95
|
+
template?: number;
|
|
96
|
+
};
|
|
97
|
+
/**
|
|
98
|
+
* The boxes the reaper may destroy: guests of the box pool, in the box VMID
|
|
99
|
+
* range, tagged `synap-box`, never the template. Read from /cluster/resources
|
|
100
|
+
* (VM.Audit + Pool.Audit on the pool), not the deprecated GET /pools/{id}.
|
|
101
|
+
*/
|
|
102
|
+
export declare function boxesOf(cfg: BoxHostConfig, guests: readonly ClusterGuest[]): ListedBox[];
|
|
103
|
+
type ThinPool = {
|
|
104
|
+
lv: string;
|
|
105
|
+
vg: string;
|
|
106
|
+
metadata_size: number;
|
|
107
|
+
metadata_used: number;
|
|
108
|
+
};
|
|
109
|
+
/** Metadata fill of `vg/lv`; throws when the pool is not listed (a missing reading is never "fine"). */
|
|
110
|
+
export declare function thinMetaPct(spec: string, pools: readonly ThinPool[]): number;
|
|
111
|
+
export declare function createProxmoxBackend(api: ProxmoxApi, node: string, cfg: BoxHostConfig): BoxBackend;
|
|
112
|
+
export {};
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Proxmox backend of the run box: one fresh unprivileged LXC per run,
|
|
3
|
+
* linked-cloned from a template CT, on the isolated box bridge, destroyed after.
|
|
4
|
+
*
|
|
5
|
+
* It talks to the Proxmox API with a POOL-SCOPED token (least privilege: it
|
|
6
|
+
* can clone, configure, start, stop and destroy guests of ONE pool, and read
|
|
7
|
+
* node memory, one storage's fill and the thin pools' fill — nothing else;
|
|
8
|
+
* host side in `my server` docs/RUNBOX.md, `scripts/box/setup-box-access.sh`). TLS is verified against the host's own CA, pinned by file,
|
|
9
|
+
* never `rejectUnauthorized: false`.
|
|
10
|
+
*/
|
|
11
|
+
import https from "node:https";
|
|
12
|
+
export class ProxmoxError extends Error {
|
|
13
|
+
status;
|
|
14
|
+
constructor(status, message) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.status = status;
|
|
17
|
+
this.name = "ProxmoxError";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export function createProxmoxApi(cfg) {
|
|
21
|
+
const base = new URL("/api2/json", cfg.endpoint);
|
|
22
|
+
const agent = new https.Agent({ ca: cfg.ca, keepAlive: true, ...(cfg.serverName ? { servername: cfg.serverName } : {}) });
|
|
23
|
+
function request(method, path, params) {
|
|
24
|
+
const url = new URL(base.pathname + path, base);
|
|
25
|
+
let body;
|
|
26
|
+
const form = params
|
|
27
|
+
? new URLSearchParams(Object.entries(params).map(([k, v]) => [k, String(v)])).toString()
|
|
28
|
+
: "";
|
|
29
|
+
if (method === "GET" || method === "DELETE") {
|
|
30
|
+
if (form)
|
|
31
|
+
url.search = form;
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
body = form;
|
|
35
|
+
}
|
|
36
|
+
return new Promise((resolve, reject) => {
|
|
37
|
+
const req = https.request(url, {
|
|
38
|
+
method,
|
|
39
|
+
agent,
|
|
40
|
+
headers: {
|
|
41
|
+
Authorization: `PVEAPIToken=${cfg.token}`,
|
|
42
|
+
...(body !== undefined
|
|
43
|
+
? { "Content-Type": "application/x-www-form-urlencoded", "Content-Length": Buffer.byteLength(body) }
|
|
44
|
+
: {}),
|
|
45
|
+
},
|
|
46
|
+
timeout: 30_000,
|
|
47
|
+
}, (res) => {
|
|
48
|
+
const chunks = [];
|
|
49
|
+
res.on("data", (c) => chunks.push(c));
|
|
50
|
+
res.on("end", () => {
|
|
51
|
+
const text = Buffer.concat(chunks).toString("utf8");
|
|
52
|
+
if ((res.statusCode ?? 500) >= 400) {
|
|
53
|
+
reject(new ProxmoxError(res.statusCode ?? 500, `${method} ${path}: HTTP ${res.statusCode} ${res.statusMessage ?? ""} ${text.slice(0, 300)}`.trim()));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
resolve(JSON.parse(text).data);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
reject(new ProxmoxError(res.statusCode ?? 500, `${method} ${path}: not JSON`));
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
req.on("timeout", () => req.destroy(new Error(`${method} ${path}: timed out`)));
|
|
65
|
+
req.on("error", reject);
|
|
66
|
+
if (body !== undefined)
|
|
67
|
+
req.write(body);
|
|
68
|
+
req.end();
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
get: (p) => request("GET", p),
|
|
73
|
+
post: (p, params) => request("POST", p, params),
|
|
74
|
+
put: (p, params) => request("PUT", p, params),
|
|
75
|
+
del: (p, params) => request("DELETE", p, params),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/** Every box carries this tag; the reaper touches nothing without it. */
|
|
79
|
+
export const BOX_TAG = "synap-box";
|
|
80
|
+
/** The IP a box gets on the bridge, by its VMID slot (.10 upward). */
|
|
81
|
+
export function boxIpFor(cfg, vmid) {
|
|
82
|
+
return `${cfg.subnetPrefix}.${10 + (vmid - cfg.vmidMin)}`;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* The boxes the reaper may destroy: guests of the box pool, in the box VMID
|
|
86
|
+
* range, tagged `synap-box`, never the template. Read from /cluster/resources
|
|
87
|
+
* (VM.Audit + Pool.Audit on the pool), not the deprecated GET /pools/{id}.
|
|
88
|
+
*/
|
|
89
|
+
export function boxesOf(cfg, guests) {
|
|
90
|
+
return guests
|
|
91
|
+
.filter((g) => g.type === "lxc" && typeof g.vmid === "number" && g.pool === cfg.pool)
|
|
92
|
+
.filter((g) => g.vmid !== cfg.templateVmid && !g.template)
|
|
93
|
+
.filter((g) => g.vmid >= cfg.vmidMin && g.vmid <= cfg.vmidMax)
|
|
94
|
+
.filter((g) => (g.tags ?? "").split(/[;, ]/).includes(BOX_TAG))
|
|
95
|
+
.map((g) => ({ vmid: g.vmid, name: g.name ?? "" }));
|
|
96
|
+
}
|
|
97
|
+
/** Metadata fill of `vg/lv`; throws when the pool is not listed (a missing reading is never "fine"). */
|
|
98
|
+
export function thinMetaPct(spec, pools) {
|
|
99
|
+
const [vg, lv] = spec.split("/");
|
|
100
|
+
const p = pools.find((t) => t.vg === vg && t.lv === lv);
|
|
101
|
+
if (!p)
|
|
102
|
+
throw new Error(`thin pool ${spec} not found on the node`);
|
|
103
|
+
return p.metadata_size > 0 ? (p.metadata_used / p.metadata_size) * 100 : 100;
|
|
104
|
+
}
|
|
105
|
+
export function createProxmoxBackend(api, node, cfg) {
|
|
106
|
+
const n = encodeURIComponent(node);
|
|
107
|
+
async function waitTask(upid, timeoutMs = 120_000) {
|
|
108
|
+
const deadline = Date.now() + timeoutMs;
|
|
109
|
+
for (;;) {
|
|
110
|
+
const t = await api.get(`/nodes/${n}/tasks/${encodeURIComponent(upid)}/status`);
|
|
111
|
+
if (t.status === "stopped") {
|
|
112
|
+
if (t.exitstatus !== "OK")
|
|
113
|
+
throw new ProxmoxError(500, `task ${upid} ended: ${t.exitstatus}`);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (Date.now() > deadline)
|
|
117
|
+
throw new ProxmoxError(504, `task ${upid} still running after ${timeoutMs} ms`);
|
|
118
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* The first VMID of the box range that is free cluster-wide. /cluster/resources
|
|
123
|
+
* shows only the guests this token may audit (the pool), so each candidate is
|
|
124
|
+
* confirmed by /cluster/nextid?vmid=N, which checks every guest.
|
|
125
|
+
*/
|
|
126
|
+
async function freeVmid() {
|
|
127
|
+
const seen = new Set((await api.get(`/cluster/resources?type=vm`)).map((g) => g.vmid).filter((v) => typeof v === "number"));
|
|
128
|
+
for (let id = cfg.vmidMin; id <= cfg.vmidMax; id++) {
|
|
129
|
+
if (seen.has(id))
|
|
130
|
+
continue;
|
|
131
|
+
try {
|
|
132
|
+
await api.get(`/cluster/nextid?vmid=${id}`);
|
|
133
|
+
return id;
|
|
134
|
+
}
|
|
135
|
+
catch (err) {
|
|
136
|
+
if (err instanceof ProxmoxError && err.status === 400)
|
|
137
|
+
continue; // "VM <id> already exists"
|
|
138
|
+
throw err;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
async function destroy(vmid) {
|
|
144
|
+
try {
|
|
145
|
+
const st = await api.get(`/nodes/${n}/lxc/${vmid}/status/current`);
|
|
146
|
+
if (st.status === "running") {
|
|
147
|
+
await waitTask(await api.post(`/nodes/${n}/lxc/${vmid}/status/stop`));
|
|
148
|
+
}
|
|
149
|
+
await waitTask(await api.del(`/nodes/${n}/lxc/${vmid}`, { purge: 1, "destroy-unreferenced-disks": 1 }));
|
|
150
|
+
}
|
|
151
|
+
catch (err) {
|
|
152
|
+
// A box already gone is the outcome we want.
|
|
153
|
+
if (err instanceof ProxmoxError && (err.status === 404 || /does not exist/.test(err.message)))
|
|
154
|
+
return;
|
|
155
|
+
throw err;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
async readHost() {
|
|
160
|
+
// `memory.free` here is MemFree; total - used is MemAvailable (read on
|
|
161
|
+
// the host 2026-10-26: within 1% of /proc/meminfo MemAvailable).
|
|
162
|
+
const status = await api.get(`/nodes/${n}/status`);
|
|
163
|
+
const st = await api.get(`/nodes/${n}/storage/${encodeURIComponent(cfg.storage)}/status`);
|
|
164
|
+
const metaUsedPct = cfg.thinpool
|
|
165
|
+
? thinMetaPct(cfg.thinpool, await api.get(`/nodes/${n}/disks/lvmthin`))
|
|
166
|
+
: null;
|
|
167
|
+
return {
|
|
168
|
+
memAvailableMb: Math.floor((status.memory.total - status.memory.used) / 1024 / 1024),
|
|
169
|
+
storageUsedPct: st.total > 0 ? (st.used / st.total) * 100 : 100,
|
|
170
|
+
metaUsedPct,
|
|
171
|
+
};
|
|
172
|
+
},
|
|
173
|
+
async create(runId, limits) {
|
|
174
|
+
const vmid = await freeVmid();
|
|
175
|
+
if (vmid === null)
|
|
176
|
+
throw new ProxmoxError(503, `no free VMID in ${cfg.vmidMin}-${cfg.vmidMax}`);
|
|
177
|
+
const ip = boxIpFor(cfg, vmid);
|
|
178
|
+
const upid = await api.post(`/nodes/${n}/lxc/${cfg.templateVmid}/clone`, {
|
|
179
|
+
newid: vmid,
|
|
180
|
+
hostname: `box-${runId.slice(0, 8)}`,
|
|
181
|
+
pool: cfg.pool,
|
|
182
|
+
full: 0,
|
|
183
|
+
description: `synap box run ${runId}`,
|
|
184
|
+
});
|
|
185
|
+
await waitTask(upid);
|
|
186
|
+
try {
|
|
187
|
+
await api.put(`/nodes/${n}/lxc/${vmid}/config`, {
|
|
188
|
+
cores: limits.cpus,
|
|
189
|
+
memory: limits.memoryMb,
|
|
190
|
+
swap: 0,
|
|
191
|
+
cpuunits: cfg.cpuunits,
|
|
192
|
+
// No gateway: the box's only reachable neighbour is the provisioner.
|
|
193
|
+
net0: `name=eth0,bridge=${cfg.bridge},ip=${ip}/24,firewall=0`,
|
|
194
|
+
tags: `${BOX_TAG};run-${runId.slice(0, 8)}`,
|
|
195
|
+
onboot: 0,
|
|
196
|
+
});
|
|
197
|
+
await waitTask(await api.post(`/nodes/${n}/lxc/${vmid}/status/start`));
|
|
198
|
+
}
|
|
199
|
+
catch (err) {
|
|
200
|
+
await destroy(vmid).catch(() => undefined);
|
|
201
|
+
throw err;
|
|
202
|
+
}
|
|
203
|
+
return { vmid, ip };
|
|
204
|
+
},
|
|
205
|
+
destroy,
|
|
206
|
+
async list() {
|
|
207
|
+
return boxesOf(cfg, await api.get(`/cluster/resources?type=vm`));
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
//# sourceMappingURL=proxmox.js.map
|