@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
package/index.mjs
CHANGED
|
@@ -37,7 +37,7 @@ const run_process = function (command, args, input, options = {}) {
|
|
|
37
37
|
// leaves the native process orphaned (PPID 1), still holding its OpenAI
|
|
38
38
|
// socket and running forever past the deadline. We instead run the child in
|
|
39
39
|
// its own process group (detached) and kill the whole group ourselves.
|
|
40
|
-
const { onStdout, onStderr, timeout, killSignal = 'SIGKILL', ...spawn_options } = options;
|
|
40
|
+
const { onStdout, onStderr, timeout, killSignal = 'SIGKILL', abort_check, abort_interval_ms, ...spawn_options } = options;
|
|
41
41
|
const child = spawn(command, args, {
|
|
42
42
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
43
43
|
...(timeout ? { detached: true } : {}),
|
|
@@ -66,6 +66,30 @@ const run_process = function (command, args, input, options = {}) {
|
|
|
66
66
|
}, timeout)
|
|
67
67
|
: null;
|
|
68
68
|
|
|
69
|
+
// A caller that can decide, part way through, that this process must stop. Used by the
|
|
70
|
+
// Xucode run tracker to end a run it has judged (plan 9.3 layer 6) without waiting for the
|
|
71
|
+
// timeout, which is the whole difference between minutes of stolen cycles and half an hour.
|
|
72
|
+
// It reuses kill_tree for the same reason the timeout does: signalling the direct child
|
|
73
|
+
// leaves the native binary running.
|
|
74
|
+
let aborted_reason = null;
|
|
75
|
+
const abort_timer = typeof abort_check === 'function'
|
|
76
|
+
? setInterval(async () => {
|
|
77
|
+
try {
|
|
78
|
+
const reason = await abort_check();
|
|
79
|
+
if (!reason) return;
|
|
80
|
+
aborted_reason = String(reason);
|
|
81
|
+
stderr += `\n${aborted_reason}`;
|
|
82
|
+
kill_tree(killSignal);
|
|
83
|
+
} catch (e) {
|
|
84
|
+
// A judge that throws must never take down the process it is judging.
|
|
85
|
+
}
|
|
86
|
+
}, Number(abort_interval_ms) || 5000)
|
|
87
|
+
: null;
|
|
88
|
+
const clear_timers = () => {
|
|
89
|
+
if (timer) clearTimeout(timer);
|
|
90
|
+
if (abort_timer) clearInterval(abort_timer);
|
|
91
|
+
};
|
|
92
|
+
|
|
69
93
|
child.stdout.on('data', (chunk) => {
|
|
70
94
|
const text = chunk.toString();
|
|
71
95
|
stdout += text;
|
|
@@ -83,12 +107,12 @@ const run_process = function (command, args, input, options = {}) {
|
|
|
83
107
|
});
|
|
84
108
|
|
|
85
109
|
child.on('error', (err) => {
|
|
86
|
-
|
|
110
|
+
clear_timers();
|
|
87
111
|
reject(err);
|
|
88
112
|
});
|
|
89
113
|
child.on('close', (exit_code) => {
|
|
90
|
-
|
|
91
|
-
resolve({ exit_code, stdout, stderr, timed_out });
|
|
114
|
+
clear_timers();
|
|
115
|
+
resolve({ exit_code, stdout, stderr, timed_out, aborted: !!aborted_reason, abort_reason: aborted_reason });
|
|
92
116
|
});
|
|
93
117
|
|
|
94
118
|
if (input) {
|
|
@@ -98,26 +122,28 @@ const run_process = function (command, args, input, options = {}) {
|
|
|
98
122
|
});
|
|
99
123
|
};
|
|
100
124
|
|
|
101
|
-
// UI-228 (
|
|
102
|
-
// whether it got a VM, an ephemeral runner or a local working copy. See
|
|
103
|
-
// docs/plans/
|
|
125
|
+
// UI-228 (xucode). The runtime seam: every xucode layer asks this for a WORKSPACE and never learns
|
|
126
|
+
// whether it got a VM, an ephemeral runner or a local working copy. See xucode_runtime.mjs and
|
|
127
|
+
// docs/plans/xucode.md section 3.8. Instantiated here rather than inside that file because
|
|
104
128
|
// run_process above is the only spawn wrapper whose detached process-group kill actually stops a
|
|
105
129
|
// Codex run, and a second copy of that fix is the last thing this codebase needs.
|
|
106
|
-
const { create_runtime:
|
|
107
|
-
const { create_mirror:
|
|
130
|
+
const { create_runtime: _create_xucode_runtime } = await import('./xucode_runtime.mjs');
|
|
131
|
+
const { create_mirror: _create_xucode_mirror } = await import('./xucode_mirror.mjs');
|
|
108
132
|
// The run tracker watches a workspace through the same handle everything else uses, so it
|
|
109
|
-
// works identically on a VM, an ephemeral runner or a container. See
|
|
110
|
-
const { create_run_tracker:
|
|
133
|
+
// works identically on a VM, an ephemeral runner or a container. See xucode_tracker.mjs.
|
|
134
|
+
const { create_run_tracker: create_xucode_run_tracker } = await import('./xucode_tracker.mjs');
|
|
111
135
|
// The engine registry: codex and Claude Code behind one launch/parse contract, so nothing above
|
|
112
|
-
// this line branches on which CLI is running. See
|
|
113
|
-
const
|
|
114
|
-
// The verify loop: what makes a
|
|
115
|
-
// leaves your build broken. See
|
|
116
|
-
const
|
|
117
|
-
//
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
const
|
|
136
|
+
// this line branches on which CLI is running. See xucode_engines.mjs.
|
|
137
|
+
const xucode_engines = await import('./xucode_engines.mjs');
|
|
138
|
+
// The verify loop: what makes a xucode run different from a terminal that says "done" and
|
|
139
|
+
// leaves your build broken. See xucode_verify.mjs.
|
|
140
|
+
const xucode_verify = await import('./xucode_verify.mjs');
|
|
141
|
+
// Where the provider credential lives. The machine gets a run token; this box keeps the key.
|
|
142
|
+
const xucode_proxy = await import('./xucode_proxy.mjs');
|
|
143
|
+
// The last gate of that loop: is the app still actually running. See xucode_preview.mjs.
|
|
144
|
+
const { create_preview: _create_xucode_preview } = await import('./xucode_preview.mjs');
|
|
145
|
+
const xucode_preview = _create_xucode_preview({});
|
|
146
|
+
const xucode_runtime = _create_xucode_runtime({ run_process });
|
|
121
147
|
|
|
122
148
|
// UI-220. Same spawn, opposite lifetime: start the process, hand back its pid and walk away, with
|
|
123
149
|
// stdout and stderr going straight to a file that never passes through this process. A code run
|
|
@@ -531,10 +557,10 @@ const bot_ms = await import(`${module_path}/bot_protection_module/index_ms.mjs`)
|
|
|
531
557
|
// Owns whether the AI answers, and with what, on every channel. This module used to decide
|
|
532
558
|
// it from flat fields on the profile doc; see auto_response() below.
|
|
533
559
|
const auto_response_ms = await import(`${module_path}/auto_response_module/index_ms.mjs`);
|
|
534
|
-
// UI-228: the `
|
|
560
|
+
// UI-228: the `xucode_run` identity gate. Declared at level 2 in verify_policy and currently
|
|
535
561
|
// SHADOWED, so this reports and never denies until the platform enforcement floor moves.
|
|
536
562
|
const verify_ms = await import(`${module_path}/verify_module/index_ms.mjs`);
|
|
537
|
-
// UI-228: `
|
|
563
|
+
// UI-228: `xucode_touch`, which is what the idle clock that suspends a machine measures from.
|
|
538
564
|
// The ASYNC wrapper deliberately: nothing about a run should wait on a timestamp being written.
|
|
539
565
|
const deploy_msa = await import(`${module_path}/deploy_module/index_msa.mjs`);
|
|
540
566
|
// The SYNC twin: the VM substrate has to know which machine it got and what Proxmox answered,
|
|
@@ -543,7 +569,7 @@ const deploy_ms = await import(`${module_path}/deploy_module/index_ms.mjs`);
|
|
|
543
569
|
// The entitlement rules themselves, imported as plain logic rather than called over the broker.
|
|
544
570
|
// They are a pure function of the account document, so a queue round trip would buy nothing and
|
|
545
571
|
// cost a hop on the path of every single run.
|
|
546
|
-
const
|
|
572
|
+
const xucode_deploy_rules = await import(`${module_path}/deploy_module/xucode.mjs`);
|
|
547
573
|
|
|
548
574
|
const ws_dashboard_msa = await import(`${module_path}/ws_dashboard_module/index_msa.mjs`);
|
|
549
575
|
// Sync twin of the above: the chat-finished alert has to ASK whether the user is
|
|
@@ -3447,6 +3473,10 @@ export const get_code_run = async function (req) {
|
|
|
3447
3473
|
repo_id: run.repo_id || null,
|
|
3448
3474
|
branch: run.branch || null,
|
|
3449
3475
|
base_sha: run.base_sha || null,
|
|
3476
|
+
// UI-228: what the verification found. Present only on a xucode run, and the reason the
|
|
3477
|
+
// panel can say "test failed" with the output instead of leaving the customer to guess
|
|
3478
|
+
// why the answer sounded confident and the build is red.
|
|
3479
|
+
verify: run.verify || null,
|
|
3450
3480
|
commands: (run.commands || []).map((c) => ({ command: c.command, exit_code: c.exit_code, ts: c.ts, output: c.output })),
|
|
3451
3481
|
files: (run.files || []).map((f) => ({ path: f.path, rel: f.rel, change: f.change, diff_available: !!f.diff_available, revert_available: !!f.revert_available, reverted_ts: f.reverted_ts || null })),
|
|
3452
3482
|
files_truncated: !!run.files_truncated,
|
|
@@ -3682,7 +3712,7 @@ export const git_repo_list = async function (req) {
|
|
|
3682
3712
|
};
|
|
3683
3713
|
|
|
3684
3714
|
// ── UI-228: BYOK provider keys ─────────────────────────────────────────────────────────────
|
|
3685
|
-
// docs/plans/
|
|
3715
|
+
// docs/plans/xucode.md 5.2. Claude Code and every engine after it run on the CUSTOMER'S key, not
|
|
3686
3716
|
// ours. That removes model cost from our books entirely and turns efficiency work into their
|
|
3687
3717
|
// saving rather than our margin, which is a better product and an honest one.
|
|
3688
3718
|
//
|
|
@@ -3690,40 +3720,40 @@ export const git_repo_list = async function (req) {
|
|
|
3690
3720
|
// doc in the ACCOUNT'S project database, never on the app doc in xuda_master, because control DBs
|
|
3691
3721
|
// replicate bidirectionally fleet-wide through the master hub and a secret there would be copied
|
|
3692
3722
|
// to every region. No read path returns more than the last four characters.
|
|
3693
|
-
const
|
|
3723
|
+
const XUCODE_KEY_PROVIDERS = ['anthropic', 'openai'];
|
|
3694
3724
|
|
|
3695
3725
|
// What a key looks like, checked only enough to catch a paste that obviously is not one. This is
|
|
3696
3726
|
// deliberately not strict: providers change their prefixes, and refusing a valid key is worse than
|
|
3697
3727
|
// accepting an invalid one that fails clearly on first use.
|
|
3698
|
-
const
|
|
3728
|
+
const xucode_key_shape_error = function (provider, api_key) {
|
|
3699
3729
|
const key = String(api_key || '').trim();
|
|
3700
3730
|
if (key.length < 20) return 'That does not look like an API key. Paste the whole key.';
|
|
3701
3731
|
if (provider === 'anthropic' && !key.startsWith('sk-ant-')) {
|
|
3702
3732
|
// The single most common wrong thing to paste, so it is worth naming: a Claude Pro or Max
|
|
3703
3733
|
// SUBSCRIPTION cannot drive Claude Code on our machines, whatever it can do on a laptop.
|
|
3704
3734
|
// Saying it here costs one line and saves a support conversation with every team that tries.
|
|
3705
|
-
return 'That does not look like an Anthropic API key (they start with sk-ant-). A Claude Pro or Max subscription will not work here:
|
|
3735
|
+
return 'That does not look like an Anthropic API key (they start with sk-ant-). A Claude Pro or Max subscription will not work here: Xucode needs an API key with credits on it, from console.anthropic.com.';
|
|
3706
3736
|
}
|
|
3707
3737
|
if (provider === 'openai' && !key.startsWith('sk-')) return 'That does not look like an OpenAI API key (they start with sk-).';
|
|
3708
3738
|
return null;
|
|
3709
3739
|
};
|
|
3710
3740
|
|
|
3711
|
-
const
|
|
3741
|
+
const xucode_key_safe = function (doc) {
|
|
3712
3742
|
if (!doc) return null;
|
|
3713
3743
|
return { provider: doc.provider, last4: doc.key_last4 || null, added_by_uid: doc.added_by_uid, ts: doc.ts };
|
|
3714
3744
|
};
|
|
3715
3745
|
|
|
3716
|
-
export const
|
|
3746
|
+
export const xucode_key_set = async function (req) {
|
|
3717
3747
|
const { uid, profile_id, provider, api_key } = req;
|
|
3718
3748
|
try {
|
|
3719
|
-
if (!
|
|
3720
|
-
const shape_error =
|
|
3749
|
+
if (!XUCODE_KEY_PROVIDERS.includes(provider)) return { code: -1, data: 'that provider is not supported' };
|
|
3750
|
+
const shape_error = xucode_key_shape_error(provider, api_key);
|
|
3721
3751
|
if (shape_error) return { code: -1, data: shape_error };
|
|
3722
3752
|
|
|
3723
3753
|
const account_profile_info = await get_active_account_profile_info(uid, profile_id);
|
|
3724
3754
|
// One key per provider per account: adding a new one replaces the old rather than stacking,
|
|
3725
3755
|
// so there is never a question of which of two keys a run used.
|
|
3726
|
-
const existing = await db_module.find_app_couch_query(account_profile_info.app_id, { selector: { docType: '
|
|
3756
|
+
const existing = await db_module.find_app_couch_query(account_profile_info.app_id, { selector: { docType: 'xucode_key', provider, stat: 3 }, limit: 5 });
|
|
3727
3757
|
for (const doc of existing?.docs || []) {
|
|
3728
3758
|
doc.stat = 4;
|
|
3729
3759
|
doc.ts = Date.now();
|
|
@@ -3731,8 +3761,8 @@ export const xudex_key_set = async function (req) {
|
|
|
3731
3761
|
}
|
|
3732
3762
|
|
|
3733
3763
|
const key_doc = {
|
|
3734
|
-
_id: await _common.xuda_get_uuid('
|
|
3735
|
-
docType: '
|
|
3764
|
+
_id: await _common.xuda_get_uuid('xucode_key'),
|
|
3765
|
+
docType: 'xucode_key',
|
|
3736
3766
|
stat: 3,
|
|
3737
3767
|
provider,
|
|
3738
3768
|
api_key: String(api_key).trim(),
|
|
@@ -3742,29 +3772,29 @@ export const xudex_key_set = async function (req) {
|
|
|
3742
3772
|
ts: Date.now(),
|
|
3743
3773
|
};
|
|
3744
3774
|
await db_module.save_app_couch_doc_native(account_profile_info.app_id, key_doc);
|
|
3745
|
-
return { code: 1, data: { key:
|
|
3775
|
+
return { code: 1, data: { key: xucode_key_safe(key_doc) } };
|
|
3746
3776
|
} catch (err) {
|
|
3747
|
-
console.error(`[
|
|
3777
|
+
console.error(`[xucode] key set failed: ${err?.message || err}`);
|
|
3748
3778
|
return { code: -1, data: 'could not save that key' };
|
|
3749
3779
|
}
|
|
3750
3780
|
};
|
|
3751
3781
|
|
|
3752
|
-
export const
|
|
3782
|
+
export const xucode_key_list = async function (req) {
|
|
3753
3783
|
const { uid, profile_id } = req;
|
|
3754
3784
|
try {
|
|
3755
3785
|
const account_profile_info = await get_active_account_profile_info(uid, profile_id);
|
|
3756
|
-
const q = await db_module.find_app_couch_query(account_profile_info.app_id, { selector: { docType: '
|
|
3757
|
-
return { code: 1, data: { keys: (q?.docs || []).map(
|
|
3786
|
+
const q = await db_module.find_app_couch_query(account_profile_info.app_id, { selector: { docType: 'xucode_key', stat: 3 }, limit: 20 });
|
|
3787
|
+
return { code: 1, data: { keys: (q?.docs || []).map(xucode_key_safe) } };
|
|
3758
3788
|
} catch (err) {
|
|
3759
3789
|
return { code: -1, data: 'could not read your keys' };
|
|
3760
3790
|
}
|
|
3761
3791
|
};
|
|
3762
3792
|
|
|
3763
|
-
export const
|
|
3793
|
+
export const xucode_key_delete = async function (req) {
|
|
3764
3794
|
const { uid, profile_id, provider } = req;
|
|
3765
3795
|
try {
|
|
3766
3796
|
const account_profile_info = await get_active_account_profile_info(uid, profile_id);
|
|
3767
|
-
const q = await db_module.find_app_couch_query(account_profile_info.app_id, { selector: { docType: '
|
|
3797
|
+
const q = await db_module.find_app_couch_query(account_profile_info.app_id, { selector: { docType: 'xucode_key', provider, stat: 3 }, limit: 5 });
|
|
3768
3798
|
let removed = 0;
|
|
3769
3799
|
for (const doc of q?.docs || []) {
|
|
3770
3800
|
doc.stat = 4;
|
|
@@ -3779,12 +3809,26 @@ export const xudex_key_delete = async function (req) {
|
|
|
3779
3809
|
}
|
|
3780
3810
|
};
|
|
3781
3811
|
|
|
3782
|
-
//
|
|
3783
|
-
//
|
|
3784
|
-
|
|
3812
|
+
// Does this account have its own key for a provider, and WHERE does it live. Deliberately asks for
|
|
3813
|
+
// `_id` only: the run path no longer needs to see a customer key at all when the proxy is on, and
|
|
3814
|
+
// the safest way to keep a secret out of a process is not to load it. The app id goes into the run
|
|
3815
|
+
// token so the proxy can do the read on our side.
|
|
3816
|
+
const xucode_key_ref = async function (uid, profile_id, provider) {
|
|
3785
3817
|
try {
|
|
3786
3818
|
const account_profile_info = await get_active_account_profile_info(uid, profile_id);
|
|
3787
|
-
const q = await db_module.find_app_couch_query(account_profile_info.app_id, { selector: { docType: '
|
|
3819
|
+
const q = await db_module.find_app_couch_query(account_profile_info.app_id, { selector: { docType: 'xucode_key', provider, stat: 3 }, limit: 1, fields: ['_id'] });
|
|
3820
|
+
return { app_id: account_profile_info.app_id, has_key: !!q?.docs?.length };
|
|
3821
|
+
} catch (err) {
|
|
3822
|
+
return { app_id: null, has_key: false };
|
|
3823
|
+
}
|
|
3824
|
+
};
|
|
3825
|
+
|
|
3826
|
+
// Internal: the key itself, for a run that is about to start. Only reached when the proxy is OFF,
|
|
3827
|
+
// which is the one case where the engine still needs the key in its own environment.
|
|
3828
|
+
const xucode_resolve_api_key = async function (uid, profile_id, provider) {
|
|
3829
|
+
try {
|
|
3830
|
+
const account_profile_info = await get_active_account_profile_info(uid, profile_id);
|
|
3831
|
+
const q = await db_module.find_app_couch_query(account_profile_info.app_id, { selector: { docType: 'xucode_key', provider, stat: 3 }, limit: 1 });
|
|
3788
3832
|
return q?.docs?.[0]?.api_key || null;
|
|
3789
3833
|
} catch (err) {
|
|
3790
3834
|
return null;
|
|
@@ -3792,24 +3836,76 @@ const xudex_resolve_api_key = async function (uid, profile_id, provider) {
|
|
|
3792
3836
|
};
|
|
3793
3837
|
|
|
3794
3838
|
// What the run path asks for: everything needed to launch, or a refusal a customer can act on.
|
|
3795
|
-
// Kept here rather than in
|
|
3839
|
+
// Kept here rather than in xucode_engines.mjs because only this file can read the key store.
|
|
3796
3840
|
//
|
|
3797
3841
|
// DELIBERATELY NOT EXPORTED. Its return value contains `launch.env`, which holds the customer's
|
|
3798
3842
|
// provider API key, and every `export const` in this module gets a generated queue wrapper and is
|
|
3799
3843
|
// therefore callable over the broker. Exporting this would put a customer's key one internal
|
|
3800
3844
|
// broker call away from anybody, for no benefit at all: the only caller is the run path, which
|
|
3801
3845
|
// lives in this same file.
|
|
3802
|
-
const
|
|
3803
|
-
const descriptor =
|
|
3846
|
+
const xucode_prepare_engine = async function ({ uid, profile_id, engine, model, resume_session_id, sandbox, run_id, project_id } = {}) {
|
|
3847
|
+
const descriptor = xucode_engines.get_engine(engine);
|
|
3804
3848
|
if (!descriptor) return { code: -1, data: 'That engine is not available.' };
|
|
3805
|
-
|
|
3806
|
-
|
|
3849
|
+
|
|
3850
|
+
// Does this account have its own key for this provider. Asked WITHOUT reading it, because with
|
|
3851
|
+
// the proxy on nothing in this process needs to see one.
|
|
3852
|
+
const key_ref = await xucode_key_ref(uid, profile_id, descriptor.provider);
|
|
3853
|
+
|
|
3854
|
+
// A BYOK engine with no key is refused HERE, before a workspace is prepared or a machine woken.
|
|
3855
|
+
// The check moved up from prepare_launch because a proxied launch legitimately carries no key,
|
|
3856
|
+
// so "no key in hand" stopped being the same question as "this account cannot run this engine".
|
|
3857
|
+
if (descriptor.byok && !key_ref.has_key) {
|
|
3858
|
+
return {
|
|
3859
|
+
code: -1,
|
|
3860
|
+
data: `${descriptor.label} runs on your own API key. Add an ${descriptor.provider === 'anthropic' ? 'Anthropic' : descriptor.provider} API key in settings, then try again.`,
|
|
3861
|
+
needs_key: descriptor.provider,
|
|
3862
|
+
};
|
|
3863
|
+
}
|
|
3864
|
+
|
|
3865
|
+
// ── Where the credential lives ────────────────────────────────────────────────────────────
|
|
3866
|
+
// NO provider key reaches the machine, whoever owns it. The machine gets a signed run token and
|
|
3867
|
+
// the region server attaches the real credential: the customer's own when they have one for this
|
|
3868
|
+
// provider, ours otherwise (xucode_proxy.mjs).
|
|
3869
|
+
//
|
|
3870
|
+
// BYOK rides the same path deliberately. The first argument for the proxy was that OUR shared
|
|
3871
|
+
// key must not sit on a box running AI-written code, but a customer's key on that same box is
|
|
3872
|
+
// the same exposure with a different victim, and "they chose it" is a thin defence when the
|
|
3873
|
+
// machine is ours.
|
|
3874
|
+
const pconf = _conf.xucode?.proxy || {};
|
|
3875
|
+
const secret = xucode_proxy.proxy_secret();
|
|
3876
|
+
if (pconf.enabled !== false && secret) {
|
|
3877
|
+
const ttl = (Number(_conf.xucode?.run_timeout_ms) || 30 * 60 * 1000) + (Number(pconf.token_ttl_slack_ms) || 15 * 60 * 1000);
|
|
3878
|
+
const minted = xucode_proxy.mint_token({
|
|
3879
|
+
run_id,
|
|
3880
|
+
uid,
|
|
3881
|
+
project_id,
|
|
3882
|
+
engine: descriptor.key,
|
|
3883
|
+
provider: descriptor.provider,
|
|
3884
|
+
// Present means "this run spends the customer's own account". Absent means ours.
|
|
3885
|
+
key_app_id: key_ref.has_key ? key_ref.app_id : null,
|
|
3886
|
+
ttl_ms: ttl,
|
|
3887
|
+
secret,
|
|
3888
|
+
});
|
|
3889
|
+
if (minted.error) return { code: -1, data: 'Xuda could not prepare a credential for this run.' };
|
|
3890
|
+
const base = pconf.base_url || `https://${process.env.XUDA_HOSTNAME || _conf.domain}${pconf.prefix || '/cpi/xucode_proxy'}`;
|
|
3891
|
+
const proxy = { base_url: base, token: minted.token, home: `${pconf.engine_home_root || '/srv/xucode/.engine'}/${run_id}` };
|
|
3892
|
+
const launch = xucode_engines.prepare_launch({ engine, model, resume_session_id, sandbox, proxy });
|
|
3893
|
+
if (launch.error) return { code: -1, data: launch.error, needs_key: launch.needs_key || null };
|
|
3894
|
+
return { code: 1, launch, has_key: key_ref.has_key, proxied: true, byok: key_ref.has_key };
|
|
3895
|
+
}
|
|
3896
|
+
|
|
3897
|
+
// Proxy off. The key travels to the machine in the engine's environment, which is correct when
|
|
3898
|
+
// the substrate IS this server and a real exposure anywhere else, so it is said out loud rather
|
|
3899
|
+
// than happening quietly.
|
|
3900
|
+
const api_key = key_ref.has_key ? await xucode_resolve_api_key(uid, profile_id, descriptor.provider) : descriptor.provider === 'openai' ? _conf.OPENAI_API_KEY || null : null;
|
|
3901
|
+
console.warn(`[xucode] proxy disabled: ${descriptor.key} will run with ${key_ref.has_key ? "the customer's" : 'the platform'} key in its environment`);
|
|
3902
|
+
const launch = xucode_engines.prepare_launch({ engine, model, resume_session_id, api_key, sandbox });
|
|
3807
3903
|
if (launch.error) return { code: -1, data: launch.error, needs_key: launch.needs_key || null };
|
|
3808
|
-
return { code: 1, launch, has_key: !!api_key };
|
|
3904
|
+
return { code: 1, launch, has_key: !!api_key, proxied: false, byok: key_ref.has_key };
|
|
3809
3905
|
};
|
|
3810
3906
|
|
|
3811
|
-
export const
|
|
3812
|
-
return { code: 1, data: { engines:
|
|
3907
|
+
export const xucode_engine_list = async function () {
|
|
3908
|
+
return { code: 1, data: { engines: xucode_engines.list_engines() } };
|
|
3813
3909
|
};
|
|
3814
3910
|
|
|
3815
3911
|
export const git_repo_disconnect = async function (req) {
|
|
@@ -3908,25 +4004,25 @@ const ensure_git_working_copy = async function ({ uid, repo, conversation_id })
|
|
|
3908
4004
|
};
|
|
3909
4005
|
|
|
3910
4006
|
// ── UI-228: the bare mirror ────────────────────────────────────────────────────────────────
|
|
3911
|
-
// See
|
|
4007
|
+
// See xucode_mirror.mjs for why this exists: it is what lets a working copy live on a machine that
|
|
3912
4008
|
// never holds the customer's token. git_exec is handed over rather than re-implemented, because it is
|
|
3913
4009
|
// the only thing here that knows how to give git a credential without it landing in an argv.
|
|
3914
4010
|
// UI-228: the VM substrate, registered as the second implementation of the runtime interface.
|
|
3915
|
-
// It reaches the machine over the Proxmox guest agent rather than SSH, because a
|
|
3916
|
-
// app_type 'vps' and customer VPS on this platform are keyless by design. See
|
|
4011
|
+
// It reaches the machine over the Proxmox guest agent rather than SSH, because a xucode box is
|
|
4012
|
+
// app_type 'vps' and customer VPS on this platform are keyless by design. See xucode_vm.mjs.
|
|
3917
4013
|
//
|
|
3918
4014
|
// `resolve_machine` and the Proxmox client are injected rather than imported: only deploy_module
|
|
3919
4015
|
// owns Proxmox, and asking it over the broker keeps that ownership where it belongs.
|
|
3920
|
-
const { create_vm_substrate:
|
|
3921
|
-
|
|
3922
|
-
|
|
4016
|
+
const { create_vm_substrate: _create_xucode_vm } = await import('./xucode_vm.mjs');
|
|
4017
|
+
xucode_runtime.register(
|
|
4018
|
+
_create_xucode_vm({
|
|
3923
4019
|
pve_request: async (node_doc, method, url, body) => {
|
|
3924
4020
|
const ret = await deploy_ms.proxmox_api_request({ node_doc, method, url, body });
|
|
3925
4021
|
if (!ret || ret.code < 0) throw new Error(ret?.data || 'proxmox request failed');
|
|
3926
4022
|
return ret.data;
|
|
3927
4023
|
},
|
|
3928
4024
|
resolve_machine: async ({ uid, app_id }) => {
|
|
3929
|
-
const ret = await deploy_ms.
|
|
4025
|
+
const ret = await deploy_ms.xucode_machine_for({ uid, app_id });
|
|
3930
4026
|
return ret?.code > 0 ? ret.data : null;
|
|
3931
4027
|
},
|
|
3932
4028
|
}),
|
|
@@ -3939,28 +4035,28 @@ xudex_runtime.register(
|
|
|
3939
4035
|
//
|
|
3940
4036
|
// A substrate that is registered but not available is the normal case, not a fault: `local` is
|
|
3941
4037
|
// gated off outside dev on purpose (plan 3.1), and `vm` is unavailable on a box with no Proxmox.
|
|
3942
|
-
console.log(`[
|
|
4038
|
+
console.log(`[xucode] runtime substrates: ${xucode_runtime.list().map((s) => `${s.kind}=${s.available ? 'available' : 'off'}`).join(' ') || 'none'}`);
|
|
3943
4039
|
|
|
3944
|
-
const
|
|
4040
|
+
const xucode_mirror = _create_xucode_mirror({
|
|
3945
4041
|
git_exec,
|
|
3946
4042
|
du_mb: async (dir) =>
|
|
3947
4043
|
Number(((await run_process('bash', ['-lc', `du -sm ${JSON.stringify(dir)} | cut -f1`], null, { timeout: 60000 })).stdout || '').trim()) || 0,
|
|
3948
|
-
mirror_root: () => _conf.
|
|
4044
|
+
mirror_root: () => _conf.xucode?.mirror_path || path.join(git_repos_root(), '_mirrors'),
|
|
3949
4045
|
clone_timeout_ms: GIT_CLONE_TIMEOUT_MS,
|
|
3950
4046
|
clone_max_mb: GIT_CLONE_MAX_MB,
|
|
3951
4047
|
});
|
|
3952
4048
|
|
|
3953
4049
|
// UI-228: the whole chain in one place. Deliberately constructed HERE rather than beside the
|
|
3954
|
-
// other
|
|
4050
|
+
// other xucode imports at the top of the file: it needs `xucode_mirror` and `git_exec`, both of
|
|
3955
4051
|
// which are defined further down, and a top-level const that reads them earlier is a temporal
|
|
3956
4052
|
// dead zone error that takes the whole module down at boot. `node --check` does not catch it.
|
|
3957
|
-
const { create_runner:
|
|
3958
|
-
const
|
|
3959
|
-
runtime:
|
|
3960
|
-
mirror:
|
|
3961
|
-
engines:
|
|
3962
|
-
verify:
|
|
3963
|
-
tracker_factory: ({ workspace }) =>
|
|
4053
|
+
const { create_runner: _create_xucode_runner } = await import('./xucode_run.mjs');
|
|
4054
|
+
const xucode_runner = _create_xucode_runner({
|
|
4055
|
+
runtime: xucode_runtime,
|
|
4056
|
+
mirror: xucode_mirror,
|
|
4057
|
+
engines: xucode_engines,
|
|
4058
|
+
verify: xucode_verify,
|
|
4059
|
+
tracker_factory: ({ workspace }) => create_xucode_run_tracker({ workspace }),
|
|
3964
4060
|
run_process,
|
|
3965
4061
|
git_exec,
|
|
3966
4062
|
});
|
|
@@ -3972,54 +4068,54 @@ const xudex_runner = _create_xudex_runner({
|
|
|
3972
4068
|
// The monthly cap from the plan (50 on free, unlimited above it). Counted from the tracker's own
|
|
3973
4069
|
// records, which is the second of the three jobs section 9.4 gives that component: one meter, not
|
|
3974
4070
|
// a separate counter that can drift away from what actually ran.
|
|
3975
|
-
const
|
|
4071
|
+
const xucode_runs_this_month = async function (app_db_id) {
|
|
3976
4072
|
const since = new Date();
|
|
3977
4073
|
since.setUTCDate(1);
|
|
3978
4074
|
since.setUTCHours(0, 0, 0, 0);
|
|
3979
4075
|
try {
|
|
3980
|
-
const q = await db_module.find_app_couch_query(app_db_id, { selector: { docType: '
|
|
4076
|
+
const q = await db_module.find_app_couch_query(app_db_id, { selector: { docType: 'xucode_run', ts: { $gte: since.getTime() } }, limit: 1000 });
|
|
3981
4077
|
return (q?.docs || []).length;
|
|
3982
4078
|
} catch (err) {
|
|
3983
4079
|
// A failed count must not block a paying customer's work. Erring toward letting the run
|
|
3984
4080
|
// happen is the right way round: the cap protects margin, and the abuse layers protect the
|
|
3985
4081
|
// thing that actually matters.
|
|
3986
|
-
console.warn(`[
|
|
4082
|
+
console.warn(`[xucode] run count failed: ${err.message}`);
|
|
3987
4083
|
return 0;
|
|
3988
4084
|
}
|
|
3989
4085
|
};
|
|
3990
4086
|
|
|
3991
|
-
export const
|
|
4087
|
+
export const xucode_run = async function (req) {
|
|
3992
4088
|
const { uid, profile_id, app_id, conversation_id, prompt, engine, model } = req;
|
|
3993
4089
|
try {
|
|
3994
|
-
if (_conf.
|
|
4090
|
+
if (_conf.xucode?.enabled !== true) return { code: -1, data: 'Xucode is not available yet.' };
|
|
3995
4091
|
if (!app_id) return { code: -1, data: 'app_id is required' };
|
|
3996
4092
|
if (!prompt || !String(prompt).trim()) return { code: -1, data: 'There is nothing to do: the request was empty.' };
|
|
3997
4093
|
|
|
3998
|
-
// 1. Entitlement. The same rules
|
|
4094
|
+
// 1. Entitlement. The same rules deploy_xucode uses to hand out a machine, asked again here,
|
|
3999
4095
|
// because a membership can lapse between provisioning a machine and using it.
|
|
4000
4096
|
//
|
|
4001
4097
|
// Asked BEFORE resolving the profile's project database, deliberately. That lookup throws on
|
|
4002
4098
|
// an account it cannot find, and a throw lands in the catch at the bottom as "please try
|
|
4003
4099
|
// again", which is the least useful thing we could say to someone whose real problem is that
|
|
4004
|
-
// their membership does not include
|
|
4100
|
+
// their membership does not include Xucode. Cheap checks first, and each with its own answer.
|
|
4005
4101
|
let account_doc = null;
|
|
4006
4102
|
try {
|
|
4007
4103
|
const acct = await db_module.get_couch_doc('xuda_accounts', uid);
|
|
4008
4104
|
account_doc = acct?.code > -1 ? acct.data : null;
|
|
4009
4105
|
} catch (e) {}
|
|
4010
4106
|
if (!account_doc) return { code: -1, data: 'Xuda could not read your account just now. Please try again.' };
|
|
4011
|
-
const ent =
|
|
4107
|
+
const ent = xucode_deploy_rules.xucode_entitlement(account_doc);
|
|
4012
4108
|
if (!ent.allowed) return { code: -1, data: ent.reason, needs_membership: true };
|
|
4013
4109
|
|
|
4014
|
-
// 2. The identity gate. It is SHADOWED today (verify_policy declares
|
|
4110
|
+
// 2. The identity gate. It is SHADOWED today (verify_policy declares xucode_run at level 2 and
|
|
4015
4111
|
// the platform enforcement floor is above it), so this reports what it would have refused and
|
|
4016
4112
|
// lets the run proceed. When the floor moves, this line starts denying without being touched.
|
|
4017
4113
|
try {
|
|
4018
|
-
const gate = await verify_ms.verify_gate_check_for_uid({ data: { uid, product: '
|
|
4114
|
+
const gate = await verify_ms.verify_gate_check_for_uid({ data: { uid, product: 'xucode_run' } });
|
|
4019
4115
|
if (gate?.code > 0 && gate.data && gate.data.allow === false) {
|
|
4020
4116
|
return {
|
|
4021
4117
|
code: -412,
|
|
4022
|
-
data: gate.data.message || 'Identity verification is required before running code on
|
|
4118
|
+
data: gate.data.message || 'Identity verification is required before running code on Xucode.',
|
|
4023
4119
|
error: 'id_verification_required',
|
|
4024
4120
|
required_level: gate.data.required_level,
|
|
4025
4121
|
level: gate.data.level,
|
|
@@ -4028,7 +4124,7 @@ export const xudex_run = async function (req) {
|
|
|
4028
4124
|
}
|
|
4029
4125
|
} catch (e) {
|
|
4030
4126
|
// A verification service that is down must not stop paying customers working.
|
|
4031
|
-
console.warn(`[
|
|
4127
|
+
console.warn(`[xucode] gate check failed, allowing: ${e.message}`);
|
|
4032
4128
|
}
|
|
4033
4129
|
|
|
4034
4130
|
// 3. The monthly cap. The project database is resolved here, at the first point that actually
|
|
@@ -4036,13 +4132,13 @@ export const xudex_run = async function (req) {
|
|
|
4036
4132
|
const account_profile_info = await get_active_account_profile_info(uid, profile_id);
|
|
4037
4133
|
const cap = Number(ent.flags?.runs_per_month);
|
|
4038
4134
|
if (Number.isFinite(cap) && cap > 0) {
|
|
4039
|
-
const used = await
|
|
4135
|
+
const used = await xucode_runs_this_month(account_profile_info.app_id);
|
|
4040
4136
|
if (used >= cap) {
|
|
4041
|
-
return { code: -1, data: `You have used all ${cap}
|
|
4137
|
+
return { code: -1, data: `You have used all ${cap} Xucode runs on your plan this month. They reset at the start of next month, or you can upgrade for unlimited runs.`, cap_reached: true, used, cap };
|
|
4042
4138
|
}
|
|
4043
4139
|
}
|
|
4044
4140
|
|
|
4045
|
-
// 4. The repository. A
|
|
4141
|
+
// 4. The repository. A xucode run without one has nothing to work on, and saying so is better
|
|
4046
4142
|
// than starting an engine in an empty directory and letting it improvise.
|
|
4047
4143
|
const repo_list = await git_repo_list({ uid, profile_id, app_id });
|
|
4048
4144
|
const repo_ref = repo_list?.data?.repos?.[0];
|
|
@@ -4052,12 +4148,17 @@ export const xudex_run = async function (req) {
|
|
|
4052
4148
|
|
|
4053
4149
|
// 5. The engine, and its key. A BYOK engine with no key is refused here, before a workspace is
|
|
4054
4150
|
// prepared or a machine is woken.
|
|
4055
|
-
|
|
4151
|
+
//
|
|
4152
|
+
// The run id is minted HERE rather than at the end with the record, because the proxy token
|
|
4153
|
+
// is scoped to it: the id has to exist before anything can be authorized against it. It goes
|
|
4154
|
+
// onto the record too, so a line in the proxy log and a run in the database are the same run.
|
|
4155
|
+
const run_id = await _common.xuda_get_uuid('xucode_run');
|
|
4156
|
+
const prepared = await xucode_prepare_engine({ uid, profile_id, engine, model, resume_session_id: req.resume_session_id || null, run_id, project_id: app_id });
|
|
4056
4157
|
if (prepared.code < 0) return { code: -1, data: prepared.data, needs_key: prepared.needs_key || null };
|
|
4057
4158
|
|
|
4058
4159
|
// 6. Run it.
|
|
4059
4160
|
const emit = typeof req.on_event === 'function' ? req.on_event : null;
|
|
4060
|
-
const ret = await
|
|
4161
|
+
const ret = await xucode_runner.run({
|
|
4061
4162
|
uid,
|
|
4062
4163
|
repo: repo_full.repo,
|
|
4063
4164
|
project_id: app_id,
|
|
@@ -4066,32 +4167,50 @@ export const xudex_run = async function (req) {
|
|
|
4066
4167
|
launch: prepared.launch,
|
|
4067
4168
|
emit,
|
|
4068
4169
|
max_repair_attempts: req.max_repair_attempts,
|
|
4069
|
-
|
|
4170
|
+
// A factory, not a value: the project's files live on whichever machine the runner ends up
|
|
4171
|
+
// acquiring, so this can only be answered once that machine exists.
|
|
4172
|
+
verify_ctx_factory: (workspace) => xucode_verify_context_for(workspace, app_id),
|
|
4070
4173
|
});
|
|
4071
4174
|
|
|
4072
4175
|
// 7. The record. It is the run meter, the abuse evidence and the benchmark sample all at once,
|
|
4073
4176
|
// so it is written whether the run succeeded or not.
|
|
4074
4177
|
if (ret.record) {
|
|
4075
4178
|
try {
|
|
4076
|
-
await db_module.save_app_couch_doc_native(account_profile_info.app_id, { _id:
|
|
4179
|
+
await db_module.save_app_couch_doc_native(account_profile_info.app_id, { _id: run_id, ...ret.record, app_id, uid });
|
|
4077
4180
|
} catch (e) {
|
|
4078
|
-
console.error(`[
|
|
4181
|
+
console.error(`[xucode] could not persist the run record: ${e.message}`);
|
|
4079
4182
|
}
|
|
4080
4183
|
if (ret.record.verdict === 'flag') {
|
|
4081
|
-
//
|
|
4082
|
-
//
|
|
4083
|
-
|
|
4184
|
+
// Loud either way, because this is the line somebody will grep for when an abuse
|
|
4185
|
+
// complaint arrives. `enforced` says whether anything was actually done about it, which
|
|
4186
|
+
// is a separate question and stays configurable (9.4).
|
|
4187
|
+
console.warn(`[xucode] RUN FLAGGED uid=${uid} app=${app_id} reasons=${(ret.record.reasons || []).join('; ')} enforced=${ret.record.enforced}`);
|
|
4188
|
+
}
|
|
4189
|
+
|
|
4190
|
+
// Layer 6 (9.3). The run has already been stopped by this point; this is what stops the
|
|
4191
|
+
// NEXT one, which is the half that actually ends an abuser rather than inconveniencing
|
|
4192
|
+
// them. Only ever reached with `tracker.enforce` on, so the default remains observe.
|
|
4193
|
+
if (ret.record.enforced === true) {
|
|
4194
|
+
try {
|
|
4195
|
+
const q = await deploy_msa.xucode_quarantine({ app_id, reasons: ret.record.reasons || [], run_id });
|
|
4196
|
+
if (q?.code < 0) console.error(`[xucode] quarantine failed for ${app_id}: ${q.data}`);
|
|
4197
|
+
} catch (e) {
|
|
4198
|
+
console.error(`[xucode] quarantine threw for ${app_id}: ${e?.message || e}`);
|
|
4199
|
+
}
|
|
4084
4200
|
}
|
|
4085
4201
|
}
|
|
4086
4202
|
|
|
4087
|
-
// 8. Keep the machine awake for as long as it is being used.
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4203
|
+
// 8. Keep the machine awake for as long as it is being used. Skipped for a run we just
|
|
4204
|
+
// stopped: touching it would reset the idle clock on a machine we want left alone.
|
|
4205
|
+
if (!ret.killed && ret.record?.enforced !== true) {
|
|
4206
|
+
try {
|
|
4207
|
+
await deploy_msa.xucode_touch({ app_id });
|
|
4208
|
+
} catch (e) {}
|
|
4209
|
+
}
|
|
4091
4210
|
|
|
4092
4211
|
return ret;
|
|
4093
4212
|
} catch (err) {
|
|
4094
|
-
console.error(`[
|
|
4213
|
+
console.error(`[xucode] run failed: ${err?.message || err}`);
|
|
4095
4214
|
return { code: -1, data: 'That run could not be completed. Please try again.' };
|
|
4096
4215
|
}
|
|
4097
4216
|
};
|
|
@@ -4099,31 +4218,36 @@ export const xudex_run = async function (req) {
|
|
|
4099
4218
|
// What the verify loop needs to know about the project: its manifest, its top-level files and
|
|
4100
4219
|
// whether dependencies are installed. Read from the working copy rather than assumed, because a
|
|
4101
4220
|
// project whose package.json changed between runs plans different gates.
|
|
4102
|
-
|
|
4103
|
-
|
|
4221
|
+
// Read from the WORKSPACE, not from this server's disk.
|
|
4222
|
+
//
|
|
4223
|
+
// This is the clone bug again, one layer up, and it is worth naming because it is the mistake
|
|
4224
|
+
// this architecture invites. Reading `/srv/xucode/<project>` locally was correct while runs
|
|
4225
|
+
// happened on the region server; once the workspace moved onto a machine, that path is simply
|
|
4226
|
+
// absent here, the read throws, and the catch below hands the verify loop an empty project. The
|
|
4227
|
+
// visible symptom is the worst kind: a run reporting "nothing to verify" over a repository with
|
|
4228
|
+
// a perfectly good test script, which is a verdict nobody should trust.
|
|
4229
|
+
//
|
|
4230
|
+
// Anything that needs to look at the project's files has to ask the workspace, because the
|
|
4231
|
+
// workspace is the only thing that knows which computer they are on.
|
|
4232
|
+
const xucode_verify_context_for = async function (workspace, project_id) {
|
|
4104
4233
|
try {
|
|
4105
|
-
const files = await
|
|
4234
|
+
const files = await workspace.list_dir('.');
|
|
4106
4235
|
let manifest = null;
|
|
4107
4236
|
if (files.includes('package.json')) {
|
|
4108
4237
|
try {
|
|
4109
|
-
manifest = JSON.parse(await
|
|
4238
|
+
manifest = JSON.parse(await workspace.read_file('package.json'));
|
|
4110
4239
|
} catch (e) {
|
|
4111
|
-
|
|
4112
|
-
// so through whatever gate trips on it. It is not a reason to refuse the run.
|
|
4113
|
-
console.warn(`[xudex] unreadable package.json in ${project_id}`);
|
|
4240
|
+
console.warn(`[xucode] unreadable package.json in ${project_id}`);
|
|
4114
4241
|
}
|
|
4115
4242
|
}
|
|
4116
4243
|
return {
|
|
4117
4244
|
manifest,
|
|
4118
4245
|
files,
|
|
4119
4246
|
node_modules_present: files.includes('node_modules'),
|
|
4120
|
-
|
|
4121
|
-
// knows which project it is and how to reach its dev server; the loop just calls it and
|
|
4122
|
-
// treats a dead app like any other failing step, which means a broken preview goes back to
|
|
4123
|
-
// the engine through the same repair path as a broken test.
|
|
4124
|
-
runtime_check: xudex_preview.runtime_check({ project_id, manifest, package_manager: detect_pm(files) }),
|
|
4247
|
+
runtime_check: xucode_preview.runtime_check({ project_id, manifest, package_manager: detect_pm(files) }),
|
|
4125
4248
|
};
|
|
4126
4249
|
} catch (err) {
|
|
4250
|
+
console.warn(`[xucode] could not read the project on its machine: ${err?.message || err}`);
|
|
4127
4251
|
return { manifest: null, files: [], node_modules_present: false };
|
|
4128
4252
|
}
|
|
4129
4253
|
};
|
|
@@ -11335,8 +11459,8 @@ ${conversation_history || `User (dashboard): ${prompt}`}
|
|
|
11335
11459
|
const from_code_surface = get_dashboard_context_obj(req, conversation_doc)?.code_surface === true;
|
|
11336
11460
|
const project_repo = from_code_surface ? (await git_repo_list({ uid, profile_id, app_id: target_app_id })).data?.repos?.[0] : null;
|
|
11337
11461
|
|
|
11338
|
-
// ── UI-228: the
|
|
11339
|
-
// The same surface, a different engine underneath. A
|
|
11462
|
+
// ── UI-228: the Xucode route ─────────────────────────────────────────────────────────
|
|
11463
|
+
// The same surface, a different engine underneath. A Xucode run adds what the Code tab
|
|
11340
11464
|
// cannot do today: it verifies the change against the project's own build and tests before
|
|
11341
11465
|
// answering, hands failures back for a bounded repair, and records what the run cost.
|
|
11342
11466
|
//
|
|
@@ -11344,15 +11468,19 @@ ${conversation_history || `User (dashboard): ${prompt}`}
|
|
|
11344
11468
|
// path below is dev-verified and working; this one is newer. `route_code_tab` is false in
|
|
11345
11469
|
// prod, true on dev, so it can be turned on for real once it has been watched, and turned
|
|
11346
11470
|
// off in one config edit if it misbehaves, with no deploy and no code change.
|
|
11347
|
-
if (project_repo && _conf.
|
|
11348
|
-
emitToDashboard('stream_phase', 'Starting
|
|
11349
|
-
const xret = await
|
|
11471
|
+
if (project_repo && _conf.xucode?.enabled === true && _conf.xucode?.route_code_tab === true) {
|
|
11472
|
+
emitToDashboard('stream_phase', 'Starting Xucode run');
|
|
11473
|
+
const xret = await xucode_run({
|
|
11350
11474
|
uid,
|
|
11351
11475
|
profile_id,
|
|
11352
11476
|
app_id: target_app_id,
|
|
11353
11477
|
conversation_id,
|
|
11354
11478
|
prompt,
|
|
11355
|
-
|
|
11479
|
+
// The picker sends its choice in the dashboard CONTEXT, which is where the Code
|
|
11480
|
+
// workspace already puts code_surface and project_id, so read it from there first and
|
|
11481
|
+
// fall back to a top-level field for any caller that sets one. Unset means the engine
|
|
11482
|
+
// registry's own default decides, rather than a name hard-coded here.
|
|
11483
|
+
engine: get_dashboard_context_obj(req, conversation_doc)?.xucode_engine || req.xucode_engine || null,
|
|
11356
11484
|
model: req.codex_model || req.ai_model || null,
|
|
11357
11485
|
resume_session_id: conversation_doc.codex_session_id && conversation_doc.codex_session_host === code_run_host() ? conversation_doc.codex_session_id : null,
|
|
11358
11486
|
on_event: (event) => {
|
|
@@ -11393,7 +11521,52 @@ ${conversation_history || `User (dashboard): ${prompt}`}
|
|
|
11393
11521
|
emitToDashboard('response_start');
|
|
11394
11522
|
streamText(answer);
|
|
11395
11523
|
emitToDashboard('stream_end');
|
|
11396
|
-
|
|
11524
|
+
// A code_run doc, so the workspace panel keeps working unchanged. The panel loads the
|
|
11525
|
+
// latest run for a conversation and renders files, diffs, revert and the git header from
|
|
11526
|
+
// it; without this a xucode run would leave it saying "nothing has run in this chat yet"
|
|
11527
|
+
// right after a run, and every one of those features would have to be rebuilt against a
|
|
11528
|
+
// second representation of the same thing.
|
|
11529
|
+
try {
|
|
11530
|
+
await db_module.save_app_couch_doc_native(account_profile_info.app_id, {
|
|
11531
|
+
_id: await _common.xuda_get_uuid('code_run'),
|
|
11532
|
+
docType: 'code_run',
|
|
11533
|
+
stat: 3,
|
|
11534
|
+
engine: xret.data.engine || 'xucode',
|
|
11535
|
+
state: xret.data.ok ? 'done' : 'failed',
|
|
11536
|
+
uid,
|
|
11537
|
+
profile_id,
|
|
11538
|
+
app_id: account_profile_info.app_id,
|
|
11539
|
+
target_app_id,
|
|
11540
|
+
conversation_id,
|
|
11541
|
+
reference_id: conversation_doc.reference_id,
|
|
11542
|
+
host: code_run_host(),
|
|
11543
|
+
repo_id: project_repo?._id || null,
|
|
11544
|
+
branch: xret.data.branch || null,
|
|
11545
|
+
base_sha: xret.data.base_sha || null,
|
|
11546
|
+
codex_session_id: xret.data.session_id || null,
|
|
11547
|
+
started_ts: xret.record?.started_ts || Date.now(),
|
|
11548
|
+
ended_ts: xret.record?.ended_ts || Date.now(),
|
|
11549
|
+
usage: xret.record?.tokens || null,
|
|
11550
|
+
// Revert is deliberately NOT offered on a xucode run yet: the file lives on the
|
|
11551
|
+
// machine, and code_run_revert_file works against the region server's own copy. A
|
|
11552
|
+
// Revert button that quietly did nothing is worse than no button.
|
|
11553
|
+
files: (xret.data.files || []).map((f) => ({ path: f.rel, rel: f.rel, change: f.status === '??' ? 'add' : f.status === 'D' ? 'delete' : 'change', diff_available: false, revert_available: false })),
|
|
11554
|
+
commands: [],
|
|
11555
|
+
verify: {
|
|
11556
|
+
ok: xret.data.ok,
|
|
11557
|
+
verdict: xret.data.verdict,
|
|
11558
|
+
repair_turns: xret.data.repair_turns || 0,
|
|
11559
|
+
pre_existing_failure: !!xret.data.pre_existing_failure,
|
|
11560
|
+
failed: xret.data.failed ? { name: xret.data.failed.name, output: xret.data.failed.output, exit_code: xret.data.failed.exit_code } : null,
|
|
11561
|
+
steps: (xret.data.steps || []).map((s) => ({ name: s.name, ran: s.ran, ok: s.ok, why: s.why, ms: s.ms })),
|
|
11562
|
+
},
|
|
11563
|
+
ts: Date.now(),
|
|
11564
|
+
});
|
|
11565
|
+
} catch (e) {
|
|
11566
|
+
console.error(`[xucode] could not save the run doc: ${e.message}`);
|
|
11567
|
+
}
|
|
11568
|
+
|
|
11569
|
+
return await saveAssistantItem(answer, { xucode: true, verified: xret.data.ok, files: xret.data.files, branch: xret.data.branch });
|
|
11397
11570
|
}
|
|
11398
11571
|
|
|
11399
11572
|
if (project_repo) {
|