@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.
@@ -1,6 +1,6 @@
1
- // Xudex runtime: the seam between what runs and where it runs.
1
+ // Xucode runtime: the seam between what runs and where it runs.
2
2
  //
3
- // See docs/plans/xudex.md section 3.8. Every layer above this one (the engine, the
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 xudex_conf = () => global._conf.xudex || {};
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(xudex_conf().run_timeout_ms) || DEFAULT_TIMEOUT_MS;
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 xudex customer substrate and must never become one. Section 3.1 is
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 xudex_conf().allow_local_substrate === true;
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 xudex runtime is available (asked for ${req.preferred_kind || 'any'}, have ${[...substrates.keys()].join(', ') || 'none'})`,
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
- // Xudex run tracker: what a run cost, and whether it was a build at all.
1
+ // Xucode run tracker: what a run cost, and whether it was a build at all.
2
2
  //
3
- // docs/plans/xudex.md 9.4. One component, three jobs, which is why it is built properly
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 xudex tiers are 1, 2 and 4 vCPU, so
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
- // `xudex.tracker.enforce` is false. The tracker records a verdict and logs it and does
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.xudex?.tracker || {};
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. Xudex v1 is Node and TypeScript only
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 (xudex_runtime.mjs),
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(`[xudex] tracker start sample failed: ${e.message}`);
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(`[xudex] tracker sample failed: ${e.message}`);
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: 'xudex_run',
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/xudex.md section 6. Codex and Claude Code are terminals: they will happily
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. Xudex has one, so after every run and BEFORE the customer is told anything
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.xudex?.verify || {};
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 (xudex_runtime.mjs), so this
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(`[xudex] repair attempt ${n} threw: ${err.message}`);
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 xudex workspace on the account's own machine.
1
+ // The VM substrate: a xucode workspace on the account's own machine.
2
2
  //
3
- // docs/plans/xudex.md 3.2 and 3.8. This is the second implementation of the runtime
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 xudex machine is `app_type: 'vps'` (the flavor rides on `is_xudex`, see deploy_xudex),
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.xudex?.vm || {};
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: st['out-data'] != null ? String(st['out-data']) : '',
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
- try {
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.xudex?.enabled === true && !!global._conf.proxmox && global._conf.proxmox.fs_agent !== false;
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 xudex machine');
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.xudex?.projects_root || '/srv/xudex';
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/xudex-exec-${vmid}-${started}.log` : null;
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 xudex machine publishes nothing but previews (plan 3.7), so this is
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.xudex?.preview_domain;
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
  },