@xuda.io/ai_module 1.1.5658 → 1.1.5660
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/index.mjs +291 -118
- package/index_ms.mjs +10 -10
- package/index_msa.mjs +10 -10
- package/package.json +1 -1
- package/{xudex_engines.mjs → xucode_engines.mjs} +74 -7
- package/{xudex_mirror.mjs → xucode_mirror.mjs} +115 -8
- package/{xudex_preview.mjs → xucode_preview.mjs} +5 -5
- package/xucode_proxy.mjs +426 -0
- package/{xudex_run.mjs → xucode_run.mjs} +111 -14
- package/{xudex_runtime.mjs → xucode_runtime.mjs} +13 -7
- package/{xudex_tracker.mjs → xucode_tracker.mjs} +40 -10
- package/{xudex_verify.mjs → xucode_verify.mjs} +5 -5
- package/{xudex_vm.mjs → xucode_vm.mjs} +69 -21
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Xucode runtime: the seam between what runs and where it runs.
|
|
2
2
|
//
|
|
3
|
-
// See docs/plans/
|
|
3
|
+
// See docs/plans/xucode.md section 3.8. Every layer above this one (the engine, the
|
|
4
4
|
// verify loop, preview, git, the run tracker) talks to a WORKSPACE and must never
|
|
5
5
|
// know whether that workspace is a persistent VM, an ephemeral runner, a container
|
|
6
6
|
// or a microVM. Section 3.2 says the Proxmox VM is a v1 choice made because we
|
|
@@ -44,14 +44,14 @@ if (!global._conf) {
|
|
|
44
44
|
global._conf = (await import(path.join(process.env.XUDA_HOME, 'common', 'load_conf.mjs'))).loadConf();
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
const
|
|
47
|
+
const xucode_conf = () => global._conf.xucode || {};
|
|
48
48
|
|
|
49
49
|
// Wall clock for a single command. This is abuse layer 3 in the plan (9.3) as much
|
|
50
50
|
// as it is a timeout: an ephemeral workspace that dies on schedule takes a miner
|
|
51
51
|
// with it, so no caller is allowed to run without a deadline.
|
|
52
52
|
const DEFAULT_TIMEOUT_MS = 30 * 60 * 1000;
|
|
53
53
|
|
|
54
|
-
const run_timeout_ms = () => Number(
|
|
54
|
+
const run_timeout_ms = () => Number(xucode_conf().run_timeout_ms) || DEFAULT_TIMEOUT_MS;
|
|
55
55
|
|
|
56
56
|
// ── Path safety ────────────────────────────────────────────────────────────────
|
|
57
57
|
// Every relative path from above this layer is attacker-influenced: the agent
|
|
@@ -74,7 +74,7 @@ const safe_join = function (root, rel) {
|
|
|
74
74
|
// interface so the seam has a real implementation from day one rather than a
|
|
75
75
|
// comment, and so the existing Code tab has somewhere to land.
|
|
76
76
|
//
|
|
77
|
-
// It is NOT a
|
|
77
|
+
// It is NOT a xucode customer substrate and must never become one. Section 3.1 is
|
|
78
78
|
// the reason: the verify loop installs arbitrary packages and runs arbitrary test
|
|
79
79
|
// suites, and this substrate's filesystem belongs to a box that also runs CouchDB
|
|
80
80
|
// and our cpi services. Hence the config gate below, off unless a box opts in.
|
|
@@ -83,7 +83,7 @@ const local_substrate = function ({ run_process }) {
|
|
|
83
83
|
kind: 'local',
|
|
84
84
|
|
|
85
85
|
available() {
|
|
86
|
-
return
|
|
86
|
+
return xucode_conf().allow_local_substrate === true;
|
|
87
87
|
},
|
|
88
88
|
|
|
89
89
|
async acquire({ project_id, dir, timeout_ms }) {
|
|
@@ -107,6 +107,10 @@ const local_substrate = function ({ run_process }) {
|
|
|
107
107
|
killSignal: 'SIGKILL',
|
|
108
108
|
onStdout: opts.onStdout,
|
|
109
109
|
onStderr: opts.onStderr,
|
|
110
|
+
// The abuse kill switch, same contract as the VM substrate: return a reason and
|
|
111
|
+
// whatever is running stops. Both substrates have to honour it or the tracker can
|
|
112
|
+
// only enforce on one kind of machine.
|
|
113
|
+
abort_check: opts.should_abort || undefined,
|
|
110
114
|
});
|
|
111
115
|
const ms = Date.now() - started;
|
|
112
116
|
return {
|
|
@@ -117,6 +121,8 @@ const local_substrate = function ({ run_process }) {
|
|
|
117
121
|
// flag, so the deadline is re-derived here instead of being guessed by
|
|
118
122
|
// every caller.
|
|
119
123
|
timed_out: ms >= (Number(opts.timeout_ms) || deadline),
|
|
124
|
+
aborted: ret.aborted === true,
|
|
125
|
+
abort_reason: ret.abort_reason || null,
|
|
120
126
|
ms,
|
|
121
127
|
};
|
|
122
128
|
},
|
|
@@ -191,7 +197,7 @@ export const create_runtime = function ({ run_process }) {
|
|
|
191
197
|
if (s && s.available()) return await s.acquire(req);
|
|
192
198
|
}
|
|
193
199
|
throw new Error(
|
|
194
|
-
`no
|
|
200
|
+
`no xucode runtime is available (asked for ${req.preferred_kind || 'any'}, have ${[...substrates.keys()].join(', ') || 'none'})`,
|
|
195
201
|
);
|
|
196
202
|
},
|
|
197
203
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Xucode run tracker: what a run cost, and whether it was a build at all.
|
|
2
2
|
//
|
|
3
|
-
// docs/plans/
|
|
3
|
+
// docs/plans/xucode.md 9.4. One component, three jobs, which is why it is built properly
|
|
4
4
|
// rather than bolted on as monitoring:
|
|
5
5
|
//
|
|
6
6
|
// 1. ABUSE DETECTION. A machine that runs arbitrary customer code is the classic mining
|
|
@@ -30,14 +30,14 @@
|
|
|
30
30
|
// CPU here is SYSTEM WIDE, from /proc/stat, so on a multi-core box a miner that uses only
|
|
31
31
|
// some of the cores never reaches the threshold. Measured on dev 2026-08-14: a
|
|
32
32
|
// single-threaded busy loop peaked at 85% and would not have tripped a 90% line. It is
|
|
33
|
-
// much less of a gap than it first looks, because the
|
|
33
|
+
// much less of a gap than it first looks, because the xucode tiers are 1, 2 and 4 vCPU, so
|
|
34
34
|
// there are not many cores to hide behind, and an attacker throttling to stay under the
|
|
35
35
|
// line is an attacker earning proportionally less. It is a real gap all the same, so the
|
|
36
36
|
// core count is recorded on every run: thresholds cannot be tuned per box size without it,
|
|
37
37
|
// and tuning on real data is exactly what log-only mode is for.
|
|
38
38
|
//
|
|
39
39
|
// ── Log-only first, and this is not optional ───────────────────────────────────────────
|
|
40
|
-
// `
|
|
40
|
+
// `xucode.tracker.enforce` is false. The tracker records a verdict and logs it and does
|
|
41
41
|
// nothing else, so thresholds are tuned against real builds before anything can act on
|
|
42
42
|
// them. The first false positive under enforcement suspends a paying customer in the
|
|
43
43
|
// middle of their work, and there is no good way to apologise for that.
|
|
@@ -48,7 +48,7 @@ if (!global._conf) {
|
|
|
48
48
|
global._conf = (await import(path.join(process.env.XUDA_HOME, 'common', 'load_conf.mjs'))).loadConf();
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
const tracker_conf = () => global._conf.
|
|
51
|
+
const tracker_conf = () => global._conf.xucode?.tracker || {};
|
|
52
52
|
|
|
53
53
|
// One shell command per sample, because sampling a workspace costs an exec and a run is
|
|
54
54
|
// sampled repeatedly. Every line is prefixed so the parser never has to guess what it is
|
|
@@ -62,7 +62,7 @@ export const SAMPLE_COMMAND = [
|
|
|
62
62
|
`ps -eo comm= 2>/dev/null | sort -u | tr '\\n' ',' | sed 's/^/proc /'`,
|
|
63
63
|
].join('; ');
|
|
64
64
|
|
|
65
|
-
// Process names that mean real work is happening.
|
|
65
|
+
// Process names that mean real work is happening. Xucode v1 is Node and TypeScript only
|
|
66
66
|
// (plan section 10), so this list is deliberately small: a wider list is a wider hole,
|
|
67
67
|
// because every name on it is a name a miner can adopt.
|
|
68
68
|
export const BUILD_PROCESSES = [
|
|
@@ -167,7 +167,7 @@ export const evaluate_run = function (record, conf = tracker_conf()) {
|
|
|
167
167
|
};
|
|
168
168
|
};
|
|
169
169
|
|
|
170
|
-
// A tracker for one run. `workspace` is the runtime interface's handle (
|
|
170
|
+
// A tracker for one run. `workspace` is the runtime interface's handle (xucode_runtime.mjs),
|
|
171
171
|
// so this works identically on a VM, an ephemeral runner or a container, which is the
|
|
172
172
|
// whole reason that seam exists.
|
|
173
173
|
export const create_run_tracker = function ({ workspace, conf = tracker_conf(), now = () => Date.now() }) {
|
|
@@ -194,7 +194,7 @@ export const create_run_tracker = function ({ workspace, conf = tracker_conf(),
|
|
|
194
194
|
last_sample_ts = started_ts;
|
|
195
195
|
} catch (e) {
|
|
196
196
|
// Sampling is observation. It must never be the reason a customer's run fails.
|
|
197
|
-
console.warn(`[
|
|
197
|
+
console.warn(`[xucode] tracker start sample failed: ${e.message}`);
|
|
198
198
|
}
|
|
199
199
|
return true;
|
|
200
200
|
},
|
|
@@ -225,7 +225,7 @@ export const create_run_tracker = function ({ workspace, conf = tracker_conf(),
|
|
|
225
225
|
last = cur;
|
|
226
226
|
last_sample_ts = ts;
|
|
227
227
|
} catch (e) {
|
|
228
|
-
console.warn(`[
|
|
228
|
+
console.warn(`[xucode] tracker sample failed: ${e.message}`);
|
|
229
229
|
}
|
|
230
230
|
return true;
|
|
231
231
|
},
|
|
@@ -239,7 +239,7 @@ export const create_run_tracker = function ({ workspace, conf = tracker_conf(),
|
|
|
239
239
|
const mean = cpu_values.length ? cpu_values.reduce((a, b) => a + b, 0) / cpu_values.length : 0;
|
|
240
240
|
|
|
241
241
|
const record = {
|
|
242
|
-
docType: '
|
|
242
|
+
docType: 'xucode_run',
|
|
243
243
|
...meta,
|
|
244
244
|
engine,
|
|
245
245
|
model,
|
|
@@ -272,5 +272,35 @@ export const create_run_tracker = function ({ workspace, conf = tracker_conf(),
|
|
|
272
272
|
record.enforced = verdict.verdict === 'flag' && verdict.enforce === true;
|
|
273
273
|
return record;
|
|
274
274
|
},
|
|
275
|
+
|
|
276
|
+
// The same rules, asked DURING the run instead of after it (plan 9.3 layer 6).
|
|
277
|
+
//
|
|
278
|
+
// Judging only at the end is close to useless against the thing this exists to catch: a
|
|
279
|
+
// miner takes everything the run timeout allows, so a verdict delivered afterwards has
|
|
280
|
+
// already paid for thirty minutes of the cycles it was meant to deny. Asked every sample,
|
|
281
|
+
// the same evidence stops it in single digit minutes.
|
|
282
|
+
//
|
|
283
|
+
// The grace period is deliberately generous and separate from the CPU window. A cold
|
|
284
|
+
// `npm install` on a big dependency tree is minutes of real CPU before anything a sampler
|
|
285
|
+
// would recognise as a build process appears, and killing a customer's first honest run is
|
|
286
|
+
// a far worse outcome than a miner getting five extra minutes.
|
|
287
|
+
live_verdict() {
|
|
288
|
+
const out_bytes = first && last && last.net_tx != null && first.net_tx != null ? Math.max(0, last.net_tx - first.net_tx) : 0;
|
|
289
|
+
const partial = {
|
|
290
|
+
cpu: { sustained_high_seconds: Math.round(sustained_high_ms / 1000) },
|
|
291
|
+
net: { out_bytes },
|
|
292
|
+
build_seen,
|
|
293
|
+
};
|
|
294
|
+
const verdict = evaluate_run(partial, conf);
|
|
295
|
+
const elapsed_seconds = started_ts ? Math.round((now() - started_ts) / 1000) : 0;
|
|
296
|
+
const grace = Number(conf.min_seconds_before_kill ?? 300);
|
|
297
|
+
return {
|
|
298
|
+
...verdict,
|
|
299
|
+
elapsed_seconds,
|
|
300
|
+
// Three separate conditions, and all three have to hold: it looks wrong, enforcement is
|
|
301
|
+
// switched on at all, and the run has been going long enough for that to mean something.
|
|
302
|
+
stop: verdict.verdict === 'flag' && verdict.enforce === true && elapsed_seconds >= grace,
|
|
303
|
+
};
|
|
304
|
+
},
|
|
275
305
|
};
|
|
276
306
|
};
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// The verify loop. This is the product.
|
|
2
2
|
//
|
|
3
|
-
// docs/plans/
|
|
3
|
+
// docs/plans/xucode.md section 6. Codex and Claude Code are terminals: they will happily
|
|
4
4
|
// tell you they are done and leave your build broken, because they have no environment to
|
|
5
|
-
// check against.
|
|
5
|
+
// check against. Xucode has one, so after every run and BEFORE the customer is told anything
|
|
6
6
|
// succeeded, the change is put through the project's own gates:
|
|
7
7
|
//
|
|
8
8
|
// install (only if the manifest moved) -> typecheck -> test -> build -> the running app
|
|
@@ -33,7 +33,7 @@ if (!global._conf) {
|
|
|
33
33
|
global._conf = (await import(path.join(process.env.XUDA_HOME, 'common', 'load_conf.mjs'))).loadConf();
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
const verify_conf = () => global._conf.
|
|
36
|
+
const verify_conf = () => global._conf.xucode?.verify || {};
|
|
37
37
|
|
|
38
38
|
const DEFAULTS = {
|
|
39
39
|
max_repair_attempts: 2,
|
|
@@ -139,7 +139,7 @@ export const repair_prompt = function (result) {
|
|
|
139
139
|
].join('\n');
|
|
140
140
|
};
|
|
141
141
|
|
|
142
|
-
// Run the gates. `workspace` is the runtime interface's handle (
|
|
142
|
+
// Run the gates. `workspace` is the runtime interface's handle (xucode_runtime.mjs), so this
|
|
143
143
|
// is identical on a VM, an ephemeral runner or a container.
|
|
144
144
|
//
|
|
145
145
|
// `runtime_check` is the seam for the preview leg (build order step 11): once a preview
|
|
@@ -246,7 +246,7 @@ export const verify_with_repair = async function ({ workspace, repair, max_attem
|
|
|
246
246
|
try {
|
|
247
247
|
repaired = await repair(repair_prompt(result), { attempt: n, failed: result.failed });
|
|
248
248
|
} catch (err) {
|
|
249
|
-
console.warn(`[
|
|
249
|
+
console.warn(`[xucode] repair attempt ${n} threw: ${err.message}`);
|
|
250
250
|
break;
|
|
251
251
|
}
|
|
252
252
|
// A repair that declined to change anything will fail identically next time, so
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
// The VM substrate: a
|
|
1
|
+
// The VM substrate: a xucode workspace on the account's own machine.
|
|
2
2
|
//
|
|
3
|
-
// docs/plans/
|
|
3
|
+
// docs/plans/xucode.md 3.2 and 3.8. This is the second implementation of the runtime
|
|
4
4
|
// interface, and building it is what proves that seam was real rather than a comment: the
|
|
5
5
|
// engine, the verify loop, the tracker and preview are not touched by anything in this file.
|
|
6
6
|
//
|
|
7
7
|
// ── It does NOT use SSH, and that is not a preference ──────────────────────────────────
|
|
8
|
-
// A
|
|
8
|
+
// A xucode machine is `app_type: 'vps'` (the flavor rides on `is_xucode`, see deploy_xucode),
|
|
9
9
|
// and customer VPS on this platform are KEYLESS by design: since the 2026-07-15 consent
|
|
10
10
|
// split no standing platform SSH key lives in those guests, and they correctly refuse one.
|
|
11
11
|
// Reaching them over SSH would mean pushing the control-plane key back in, which regresses
|
|
@@ -35,7 +35,7 @@ if (!global._conf) {
|
|
|
35
35
|
// attacker-influenced: the agent writes the file names and the customer writes the prompt.
|
|
36
36
|
export const shq = (value) => `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
37
37
|
|
|
38
|
-
const vm_conf = () => global._conf.
|
|
38
|
+
const vm_conf = () => global._conf.xucode?.vm || {};
|
|
39
39
|
|
|
40
40
|
export const create_vm_substrate = function ({ pve_request, resolve_machine, delay = (ms) => new Promise((r) => setTimeout(r, ms)), now = () => Date.now() }) {
|
|
41
41
|
if (typeof pve_request !== 'function') throw new Error('create_vm_substrate needs pve_request');
|
|
@@ -46,7 +46,7 @@ export const create_vm_substrate = function ({ pve_request, resolve_machine, del
|
|
|
46
46
|
// Dispatch a script in the guest and wait for it, with a deadline rather than a poll count.
|
|
47
47
|
// `on_tail` is called with new bytes from the log file as they appear, which is the only way
|
|
48
48
|
// to see progress on a channel that otherwise speaks once, at the end.
|
|
49
|
-
const guest_exec = async function ({ node_doc, vmid, script, timeout_ms, on_tail = null, log_file = null }) {
|
|
49
|
+
const guest_exec = async function ({ node_doc, vmid, script, timeout_ms, on_tail = null, log_file = null, should_abort = null }) {
|
|
50
50
|
const body = [
|
|
51
51
|
['command', '/bin/bash'],
|
|
52
52
|
['command', '-c'],
|
|
@@ -56,14 +56,68 @@ export const create_vm_substrate = function ({ pve_request, resolve_machine, del
|
|
|
56
56
|
const pid = disp && (disp.pid ?? disp.PID);
|
|
57
57
|
if (pid == null) throw new Error('guest exec returned no pid (is qemu-guest-agent running?)');
|
|
58
58
|
|
|
59
|
+
// Killing the process GROUP, not the process. A build spawns children, and signalling only
|
|
60
|
+
// the shell we started leaves the thing actually burning the CPU running happily.
|
|
61
|
+
const kill_in_guest = async () => {
|
|
62
|
+
try {
|
|
63
|
+
await pve_request(node_doc, 'POST', `/nodes/${node_doc.node}/qemu/${vmid}/agent/exec`, [
|
|
64
|
+
['command', '/bin/bash'],
|
|
65
|
+
['command', '-c'],
|
|
66
|
+
['command', `kill -TERM -${pid} 2>/dev/null; sleep 2; kill -KILL -${pid} 2>/dev/null; true`],
|
|
67
|
+
]);
|
|
68
|
+
} catch (e) {
|
|
69
|
+
// Nothing useful is left to try, and the caller is already being told it failed.
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
59
73
|
const deadline = now() + (Number(timeout_ms) || 30 * 60 * 1000);
|
|
60
74
|
let offset = 0;
|
|
61
75
|
|
|
62
76
|
while (now() < deadline) {
|
|
77
|
+
// Asked before the status read so a decision to stop is acted on at the first opportunity
|
|
78
|
+
// rather than one poll later. This is the abuse kill switch (plan 9.3 layer 6): whatever is
|
|
79
|
+
// running has been judged, and it stops now.
|
|
80
|
+
if (should_abort) {
|
|
81
|
+
let reason = null;
|
|
82
|
+
try {
|
|
83
|
+
reason = await should_abort();
|
|
84
|
+
} catch (e) {
|
|
85
|
+
// A broken judge must never kill a customer's run.
|
|
86
|
+
reason = null;
|
|
87
|
+
}
|
|
88
|
+
if (reason) {
|
|
89
|
+
await kill_in_guest();
|
|
90
|
+
return { stdout: '', stderr: String(reason), exit_code: 137, truncated: false, timed_out: false, aborted: true, abort_reason: String(reason) };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
63
94
|
const st = await pve_request(node_doc, 'GET', `/nodes/${node_doc.node}/qemu/${vmid}/agent/exec-status?pid=${pid}`);
|
|
64
95
|
if (st && st.exited) {
|
|
96
|
+
const stdout = st['out-data'] != null ? String(st['out-data']) : '';
|
|
97
|
+
|
|
98
|
+
// FLUSH THE TAIL BEFORE RETURNING. Everything a command printed between the last poll
|
|
99
|
+
// and its exit is in `stdout` but has never been handed to `on_tail`, and a caller that
|
|
100
|
+
// is PARSING the stream (rather than just displaying it) loses those events entirely.
|
|
101
|
+
//
|
|
102
|
+
// This was not theoretical. An engine's last events are the ones that matter most: the
|
|
103
|
+
// closing message and the token usage both arrive in the final moments, so a real run
|
|
104
|
+
// showed the agent's opening sentence, never its conclusion, and recorded 0 tokens
|
|
105
|
+
// against a turn that actually cost 40,969. The run meter is what abuse detection and
|
|
106
|
+
// accounting read, so silently zeroing it is worse than losing the text.
|
|
107
|
+
//
|
|
108
|
+
// Sliced on the BYTE offset the tail reads used, so the two agree exactly and nothing is
|
|
109
|
+
// delivered twice.
|
|
110
|
+
if (on_tail && log_file) {
|
|
111
|
+
try {
|
|
112
|
+
const buf = Buffer.from(stdout, 'utf8');
|
|
113
|
+
if (buf.length > offset) on_tail(buf.subarray(offset).toString('utf8'));
|
|
114
|
+
} catch (e) {
|
|
115
|
+
// The command already succeeded. Failing it over a progress callback would be absurd.
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
65
119
|
return {
|
|
66
|
-
stdout
|
|
120
|
+
stdout,
|
|
67
121
|
stderr: st['err-data'] != null ? String(st['err-data']) : '',
|
|
68
122
|
exit_code: st.exitcode ?? 0,
|
|
69
123
|
truncated: !!(st['out-truncated'] || st['err-truncated']),
|
|
@@ -100,13 +154,7 @@ export const create_vm_substrate = function ({ pve_request, resolve_machine, del
|
|
|
100
154
|
|
|
101
155
|
// Out of time. Kill the process group in the guest rather than leaving a build running on
|
|
102
156
|
// a machine the customer is paying for, then say so.
|
|
103
|
-
|
|
104
|
-
await pve_request(node_doc, 'POST', `/nodes/${node_doc.node}/qemu/${vmid}/agent/exec`, [
|
|
105
|
-
['command', '/bin/bash'],
|
|
106
|
-
['command', '-c'],
|
|
107
|
-
['command', `kill -TERM -${pid} 2>/dev/null; kill -KILL -${pid} 2>/dev/null; true`],
|
|
108
|
-
]);
|
|
109
|
-
} catch (e) {}
|
|
157
|
+
await kill_in_guest();
|
|
110
158
|
return { stdout: '', stderr: `the command did not finish within ${Math.round((Number(timeout_ms) || 0) / 1000)}s and was stopped`, exit_code: 124, truncated: false, timed_out: true };
|
|
111
159
|
};
|
|
112
160
|
|
|
@@ -117,14 +165,14 @@ export const create_vm_substrate = function ({ pve_request, resolve_machine, del
|
|
|
117
165
|
// Available when this box can reach Proxmox at all. Whether a PARTICULAR account has a
|
|
118
166
|
// machine is answered by acquire, because that is a per-request question.
|
|
119
167
|
available() {
|
|
120
|
-
return global._conf.
|
|
168
|
+
return global._conf.xucode?.enabled === true && !!global._conf.proxmox && global._conf.proxmox.fs_agent !== false;
|
|
121
169
|
},
|
|
122
170
|
|
|
123
171
|
async acquire({ uid, project_id, app_id }) {
|
|
124
172
|
const machine = await resolve_machine({ uid, app_id: app_id || project_id });
|
|
125
|
-
if (!machine || machine.error) throw new Error(machine?.error || 'this account has no
|
|
173
|
+
if (!machine || machine.error) throw new Error(machine?.error || 'this account has no xucode machine');
|
|
126
174
|
const { node_doc, vmid } = machine;
|
|
127
|
-
const root = global._conf.
|
|
175
|
+
const root = global._conf.xucode?.projects_root || '/srv/xucode';
|
|
128
176
|
const dir = path.posix.join(root, String(project_id));
|
|
129
177
|
|
|
130
178
|
await guest_exec({ node_doc, vmid, script: `mkdir -p ${shq(dir)}`, timeout_ms: 60000 });
|
|
@@ -153,13 +201,13 @@ export const create_vm_substrate = function ({ pve_request, resolve_machine, del
|
|
|
153
201
|
|
|
154
202
|
// A caller that wants progress gets a log file and an incremental tail; one that does
|
|
155
203
|
// not gets the plain form, which is cheaper by one exec per poll.
|
|
156
|
-
const log_file = opts.onStdout ? `/tmp/
|
|
204
|
+
const log_file = opts.onStdout ? `/tmp/xucode-exec-${vmid}-${started}.log` : null;
|
|
157
205
|
const script = log_file
|
|
158
206
|
? `cd ${shq(cwd)}; ${env} { ${command}; } > ${shq(log_file)} 2>&1; rc=$?; cat ${shq(log_file)}; rm -f ${shq(log_file)}; exit $rc`
|
|
159
207
|
: `cd ${shq(cwd)}; ${env} ${command}`;
|
|
160
208
|
|
|
161
|
-
const ret = await guest_exec({ node_doc, vmid, script, timeout_ms: opts.timeout_ms, on_tail: opts.onStdout || null, log_file });
|
|
162
|
-
return { exit_code: ret.exit_code, stdout: ret.stdout || '', stderr: ret.stderr || '', timed_out: ret.timed_out === true, ms: now() - started, truncated: ret.truncated };
|
|
209
|
+
const ret = await guest_exec({ node_doc, vmid, script, timeout_ms: opts.timeout_ms, on_tail: opts.onStdout || null, log_file, should_abort: opts.should_abort || null });
|
|
210
|
+
return { exit_code: ret.exit_code, stdout: ret.stdout || '', stderr: ret.stderr || '', timed_out: ret.timed_out === true, aborted: ret.aborted === true, abort_reason: ret.abort_reason || null, ms: now() - started, truncated: ret.truncated };
|
|
163
211
|
},
|
|
164
212
|
|
|
165
213
|
// Base64 across the channel in both directions. Proxmox hands back out-data already
|
|
@@ -197,11 +245,11 @@ export const create_vm_substrate = function ({ pve_request, resolve_machine, del
|
|
|
197
245
|
.filter(Boolean);
|
|
198
246
|
},
|
|
199
247
|
|
|
200
|
-
// The preview URL. A
|
|
248
|
+
// The preview URL. A xucode machine publishes nothing but previews (plan 3.7), so this is
|
|
201
249
|
// the one port that is ever reachable, and it goes through the platform's existing web
|
|
202
250
|
// route for the app rather than a second proxy invented here.
|
|
203
251
|
async expose_port(port) {
|
|
204
|
-
const base = global._conf.
|
|
252
|
+
const base = global._conf.xucode?.preview_domain;
|
|
205
253
|
if (!base) return null;
|
|
206
254
|
return { url: `https://${String(project_id).replace(/[^a-zA-Z0-9]/g, '')}.${base}`, port };
|
|
207
255
|
},
|