xapi-to 0.1.18 → 0.1.20

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.
@@ -0,0 +1,219 @@
1
+ #!/usr/bin/env bun
2
+
3
+ /**
4
+ * Real OpenAI Sandbox Agents SDK integration:
5
+ * model harness -> DeepSeek through https://ai.xapi.to/v1
6
+ * sandbox compute -> xAPI Sandbox Gateway
7
+ *
8
+ * The adapter intentionally implements the smallest honest contract required
9
+ * by this example: an empty Manifest plus the SDK Shell capability. The xAPI
10
+ * credentials come from env/normal CLI config, are never accepted on argv,
11
+ * never enter the model prompt or sandbox, and are never written to the report.
12
+ */
13
+
14
+ import { mkdir, writeFile } from 'node:fs/promises';
15
+ import { dirname, resolve } from 'node:path';
16
+ import {
17
+ OpenAIProvider,
18
+ Runner,
19
+ } from '@openai/agents';
20
+ import {
21
+ Manifest,
22
+ SandboxAgent,
23
+ shell,
24
+ } from '@openai/agents/sandbox';
25
+ import { getConfig } from '../src/config.ts';
26
+ import { XapiAgentsSandboxClient } from '../src/openai-sandbox-client.ts';
27
+ import { sandboxHistory } from '../src/sandbox-client.ts';
28
+
29
+ const argv = process.argv.slice(2);
30
+ const usage = `Usage: bun scripts/openai-sandbox-agent-e2e.ts [options]\n\n` +
31
+ ` --host HOST Sandbox Gateway (default: sandbox.test.xapi.to)\n` +
32
+ ` --provider NAME Sandbox provider (default: daytona)\n` +
33
+ ` --model NAME ai.xapi.to model (default: deepseek-v4-pro)\n` +
34
+ ` --report FILE Redacted JSON report path\n`;
35
+ if (argv.includes('--help')) {
36
+ process.stdout.write(usage);
37
+ process.exit(0);
38
+ }
39
+ const valueFlags = new Set(['--host', '--provider', '--model', '--report']);
40
+ for (let index = 0; index < argv.length; index += 1) {
41
+ const arg = argv[index];
42
+ if (!valueFlags.has(arg)) throw new Error(`unknown argument: ${arg}; use --help`);
43
+ if (!argv[index + 1] || argv[index + 1].startsWith('--')) {
44
+ throw new Error(`${arg} requires a value`);
45
+ }
46
+ index += 1;
47
+ }
48
+ const after = (name: string, fallback: string) => {
49
+ const index = argv.indexOf(name);
50
+ return index >= 0 ? argv[index + 1] || fallback : fallback;
51
+ };
52
+ const sandboxHost = after('--host', process.env.XAPI_SANDBOX_HOST || 'sandbox.test.xapi.to');
53
+ const provider = after('--provider', 'daytona');
54
+ const model = after('--model', 'deepseek-v4-pro');
55
+ const reportPath = resolve(after(
56
+ '--report',
57
+ `/tmp/xapi-openai-sandbox-agent-${new Date().toISOString().replaceAll(':', '-')}.json`,
58
+ ));
59
+ const startedAt = new Date();
60
+
61
+ function assert(condition: unknown, message: string): asserts condition {
62
+ if (!condition) throw new Error(message);
63
+ }
64
+
65
+ async function probeDeepSeek(apiKey: string) {
66
+ const response = await fetch('https://ai.xapi.to/v1/chat/completions', {
67
+ method: 'POST',
68
+ headers: { authorization: `Bearer ${apiKey}`, 'content-type': 'application/json' },
69
+ body: JSON.stringify({
70
+ model,
71
+ messages: [{ role: 'user', content: 'Reply exactly XAPI_DEEPSEEK_OK' }],
72
+ temperature: 0,
73
+ max_tokens: 128,
74
+ }),
75
+ });
76
+ const body = await response.json() as any;
77
+ const content = String(body?.choices?.[0]?.message?.content || '');
78
+ assert(response.ok, `ai.xapi.to DeepSeek probe failed with HTTP ${response.status}`);
79
+ assert(content.includes('XAPI_DEEPSEEK_OK'), 'DeepSeek probe did not return its marker');
80
+ return {
81
+ status: response.status,
82
+ gatewayProvider: response.headers.get('x-routing-provider'),
83
+ model: body?.model,
84
+ markerVerified: true,
85
+ usage: body?.usage,
86
+ };
87
+ }
88
+
89
+ const cfg = getConfig();
90
+ const configuredApiKey = process.env.XAPI_KEY || process.env.XAPI_API_KEY || cfg.apiKey;
91
+ const configuredKeySource = process.env.XAPI_KEY
92
+ ? 'XAPI_KEY'
93
+ : process.env.XAPI_API_KEY
94
+ ? 'XAPI_API_KEY'
95
+ : '~/.xapi/config.json';
96
+ const sandboxApiKey =
97
+ process.env.XAPI_SANDBOX_KEY || process.env.XAPI_TEST_API_KEY || configuredApiKey;
98
+ const aiApiKey = process.env.XAPI_AI_KEY || configuredApiKey;
99
+ const sandboxKeySource = process.env.XAPI_SANDBOX_KEY
100
+ ? 'XAPI_SANDBOX_KEY'
101
+ : process.env.XAPI_TEST_API_KEY
102
+ ? 'XAPI_TEST_API_KEY'
103
+ : configuredKeySource;
104
+ const aiKeySource = process.env.XAPI_AI_KEY ? 'XAPI_AI_KEY' : configuredKeySource;
105
+ assert(
106
+ sandboxApiKey,
107
+ 'Sandbox key is not configured; set XAPI_SANDBOX_KEY (or XAPI_TEST_API_KEY/XAPI_KEY)',
108
+ );
109
+ assert(
110
+ aiApiKey,
111
+ 'AI Gateway key is not configured; set XAPI_AI_KEY (or configure a production xAPI key)',
112
+ );
113
+ const report: Record<string, unknown> = {
114
+ startedAt: startedAt.toISOString(),
115
+ modelGateway: 'https://ai.xapi.to/v1',
116
+ model,
117
+ sandboxHost,
118
+ provider,
119
+ credentials: {
120
+ sandbox: { source: sandboxKeySource },
121
+ ai: { source: aiKeySource },
122
+ },
123
+ };
124
+ let failure: unknown;
125
+ const sandboxClient = new XapiAgentsSandboxClient({
126
+ apiKey: sandboxApiKey,
127
+ sandboxHost,
128
+ provider,
129
+ maxHourlyUsd: 0.20,
130
+ model,
131
+ });
132
+ const workspaceRoot = sandboxClient.workspaceRoot;
133
+
134
+ try {
135
+ report.deepSeekProbe = await probeDeepSeek(aiApiKey);
136
+ const modelProvider = new OpenAIProvider({
137
+ apiKey: aiApiKey,
138
+ baseURL: 'https://ai.xapi.to/v1',
139
+ useResponses: false,
140
+ strictFeatureValidation: true,
141
+ });
142
+ const runner = new Runner({ modelProvider, tracingDisabled: true });
143
+ const agent = new SandboxAgent({
144
+ name: 'xAPI OpenAI Sandbox verifier',
145
+ model,
146
+ defaultManifest: new Manifest({ root: workspaceRoot }),
147
+ capabilities: [shell()],
148
+ instructions:
149
+ 'You are validating a real sandbox. You MUST use the shell tool. ' +
150
+ `Run a Python command that writes ${workspaceRoot}/result.txt containing ` +
151
+ 'OPENAI_XAPI_SANDBOX_OK=42, then use shell to read that file. ' +
152
+ 'Only after the shell output confirms it, answer exactly OPENAI_XAPI_SANDBOX_OK=42.',
153
+ });
154
+ const result = await runner.run(
155
+ agent,
156
+ 'Perform the sandbox verification now.',
157
+ { maxTurns: 8, sandbox: { client: sandboxClient } },
158
+ );
159
+ const finalOutput = String(result.finalOutput || '');
160
+ assert(finalOutput.includes('OPENAI_XAPI_SANDBOX_OK=42'), 'agent final output missed marker');
161
+ assert(sandboxClient.evidence.execCount >= 2, 'agent did not perform write and read shell work');
162
+ assert(sandboxClient.evidence.shellMarkerSeen, 'real sandbox shell output missed marker');
163
+ report.sdk = {
164
+ package: '@openai/agents',
165
+ version: '0.15.0',
166
+ transport: 'OpenAI Chat Completions compatible',
167
+ tracingDisabled: true,
168
+ finalMarkerVerified: true,
169
+ };
170
+ report.sandbox = sandboxClient.evidence;
171
+ } catch (error) {
172
+ failure = error;
173
+ report.error = error instanceof Error ? error.message : String(error);
174
+ } finally {
175
+ if (sandboxClient.lastSession && !sandboxClient.evidence.finalState) {
176
+ await sandboxClient.lastSession.close().catch((error) => {
177
+ failure ||= error;
178
+ report.cleanupError = error instanceof Error ? error.message : String(error);
179
+ });
180
+ }
181
+ try {
182
+ const active = await sandboxHistory({ sandboxHost, apiKey: sandboxApiKey }, {
183
+ state: 'ACTIVE', pageSize: 100,
184
+ });
185
+ const count = active?.total ?? active?.items?.length ?? -1;
186
+ const testInstanceId = sandboxClient.evidence.instanceId;
187
+ const testInstanceActive = Boolean(
188
+ testInstanceId && active?.items?.some((item: any) => item.id === testInstanceId),
189
+ );
190
+ report.finalGate = {
191
+ accountActiveInstances: count,
192
+ testInstanceId,
193
+ testInstanceActive,
194
+ stateCounts: active?.stateCounts,
195
+ };
196
+ if (testInstanceActive) failure ||= new Error(`test sandbox ${testInstanceId} remains active`);
197
+ } catch (error) {
198
+ failure ||= error;
199
+ report.finalGate = { error: error instanceof Error ? error.message : String(error) };
200
+ }
201
+ report.status = failure ? 'failed' : 'passed';
202
+ report.finishedAt = new Date().toISOString();
203
+ report.durationMs = Date.now() - startedAt.getTime();
204
+ await mkdir(dirname(reportPath), { recursive: true });
205
+ await writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8');
206
+ console.log(JSON.stringify({
207
+ status: report.status,
208
+ model,
209
+ provider,
210
+ report: reportPath,
211
+ sandbox: sandboxClient.evidence,
212
+ finalGate: report.finalGate,
213
+ }, null, 2));
214
+ }
215
+
216
+ if (failure) {
217
+ console.error(failure instanceof Error ? failure.stack : String(failure));
218
+ process.exitCode = 1;
219
+ }
@@ -0,0 +1,463 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Real Sandbox Playground suite, executed exclusively through the built xAPI CLI.
5
+ *
6
+ * The suite covers the same nine user journeys as the web Playground and pins
7
+ * workloads across all seven configured test providers. Every created instance
8
+ * is tracked and terminated in finally; the final gate requires ACTIVE history
9
+ * to be empty. The API key is read by the CLI from its normal config/env and is
10
+ * never passed on argv or written to the report.
11
+ */
12
+
13
+ import { spawn } from 'node:child_process';
14
+ import { randomUUID } from 'node:crypto';
15
+ import { mkdir, writeFile } from 'node:fs/promises';
16
+ import { dirname, resolve } from 'node:path';
17
+ import { fileURLToPath } from 'node:url';
18
+
19
+ const here = dirname(fileURLToPath(import.meta.url));
20
+ const root = resolve(here, '..');
21
+ const cliEntry = resolve(root, 'dist/index.js');
22
+ const argv = process.argv.slice(2);
23
+ if (argv.includes('--help')) {
24
+ process.stdout.write(`Usage: node scripts/sandbox-playground-e2e.mjs [options]\n\n` +
25
+ ` --host HOST Sandbox Gateway (default: sandbox.test.xapi.to)\n` +
26
+ ` --report FILE Redacted JSON report path\n` +
27
+ ` --only 1,8,9 Run selected scenario numbers\n` +
28
+ ` --skip-gpu Skip the billable RunPod GPU scenario\n`);
29
+ process.exit(0);
30
+ }
31
+ const valueFlags = new Set(['--host', '--report', '--only']);
32
+ for (let index = 0; index < argv.length; index += 1) {
33
+ const arg = argv[index];
34
+ if (arg === '--skip-gpu') continue;
35
+ if (!valueFlags.has(arg)) throw new Error(`unknown argument: ${arg}; use --help`);
36
+ if (!argv[index + 1] || argv[index + 1].startsWith('--')) {
37
+ throw new Error(`${arg} requires a value`);
38
+ }
39
+ index += 1;
40
+ }
41
+ const valueAfter = (name, fallback) => {
42
+ const index = argv.indexOf(name);
43
+ return index >= 0 ? argv[index + 1] : fallback;
44
+ };
45
+ const host = valueAfter('--host', process.env.XAPI_SANDBOX_HOST || 'sandbox.test.xapi.to');
46
+ const reportPath = resolve(valueAfter(
47
+ '--report',
48
+ `/tmp/xapi-sandbox-cli-e2e-${new Date().toISOString().replaceAll(':', '-')}.json`,
49
+ ));
50
+ const skipGpu = argv.includes('--skip-gpu');
51
+ const only = valueAfter('--only', '')
52
+ .split(',').map((value) => value.trim()).filter(Boolean);
53
+ const startedAt = new Date();
54
+ const runTag = `sandbox-playground-e2e:${startedAt.toISOString()}:${randomUUID()}`;
55
+ const tracked = new Map();
56
+ const report = { startedAt: startedAt.toISOString(), host, runTag, scenarios: [], cleanup: [], finalGate: null };
57
+ let baselineActiveIds = new Set();
58
+
59
+ const sleep = (ms) => new Promise((resolvePromise) => setTimeout(resolvePromise, ms));
60
+ const assert = (condition, message) => {
61
+ if (!condition) throw new Error(message);
62
+ };
63
+ const json = (value) => JSON.stringify(value);
64
+ const markerIn = (value, marker) => json(value).includes(marker);
65
+
66
+ function signalProcessTree(child, signal) {
67
+ if (!child.pid || child.exitCode !== null) return;
68
+ try {
69
+ if (process.platform !== 'win32') process.kill(-child.pid, signal);
70
+ else child.kill(signal);
71
+ } catch (error) {
72
+ if (error?.code === 'ESRCH') return;
73
+ try { child.kill(signal); } catch { /* The process may have exited between checks. */ }
74
+ }
75
+ }
76
+
77
+ function runProcess(args, timeoutMs) {
78
+ return new Promise((resolvePromise, reject) => {
79
+ const child = spawn(process.execPath, [cliEntry, ...args], {
80
+ cwd: root,
81
+ env: process.env,
82
+ stdio: ['ignore', 'pipe', 'pipe'],
83
+ detached: process.platform !== 'win32',
84
+ });
85
+ let stdout = '';
86
+ let stderr = '';
87
+ let timedOut = false;
88
+ let forceKill;
89
+ const timeout = setTimeout(() => {
90
+ timedOut = true;
91
+ signalProcessTree(child, 'SIGTERM');
92
+ forceKill = setTimeout(() => signalProcessTree(child, 'SIGKILL'), 30_000);
93
+ forceKill.unref?.();
94
+ }, timeoutMs);
95
+ timeout.unref?.();
96
+ child.stdout.on('data', (chunk) => { stdout += chunk; });
97
+ child.stderr.on('data', (chunk) => { stderr += chunk; });
98
+ child.on('error', (error) => {
99
+ clearTimeout(timeout);
100
+ if (forceKill) clearTimeout(forceKill);
101
+ reject(error);
102
+ });
103
+ child.on('close', (code) => {
104
+ clearTimeout(timeout);
105
+ if (forceKill) clearTimeout(forceKill);
106
+ const result = { code, stdout: stdout.trim(), stderr: stderr.trim() };
107
+ if (timedOut) {
108
+ reject(new Error(
109
+ `CLI command timed out after ${timeoutMs}ms and was terminated` +
110
+ (result.stderr ? `: ${result.stderr.slice(0, 500)}` : ''),
111
+ ));
112
+ return;
113
+ }
114
+ resolvePromise(result);
115
+ });
116
+ });
117
+ }
118
+
119
+ function runBinary(binary, args) {
120
+ return new Promise((resolvePromise, reject) => {
121
+ const child = spawn(binary, args, {
122
+ cwd: root,
123
+ env: process.env,
124
+ stdio: ['ignore', 'pipe', 'pipe'],
125
+ });
126
+ let stdout = '';
127
+ let stderr = '';
128
+ child.stdout.on('data', (chunk) => { stdout += chunk; });
129
+ child.stderr.on('data', (chunk) => { stderr += chunk; });
130
+ child.on('error', reject);
131
+ child.on('close', (code) => resolvePromise({ code, stdout: stdout.trim(), stderr: stderr.trim() }));
132
+ });
133
+ }
134
+
135
+ async function cli(command, { provider, timeoutMs = 420_000 } = {}) {
136
+ const args = ['sandbox', ...command];
137
+ if (provider) args.push('--provider', provider);
138
+ args.push('--host', host, '--format', 'json');
139
+ process.stdout.write(` $ xapi ${args.slice(0, -2).join(' ')}\n`);
140
+ const result = await runProcess(args, timeoutMs);
141
+ if (result.code !== 0) {
142
+ throw new Error(result.stderr || result.stdout || `CLI exited ${result.code}`);
143
+ }
144
+ try { return result.stdout ? JSON.parse(result.stdout) : null; }
145
+ catch { throw new Error(`CLI returned non-JSON output: ${result.stdout.slice(0, 500)}`); }
146
+ }
147
+
148
+ async function scenario(name, providers, task) {
149
+ const scenarioNumber = name.match(/^\d+/)?.[0];
150
+ if (only.length && !only.includes(scenarioNumber)) {
151
+ report.scenarios.push({ name, providers, status: 'skipped', reason: '--only filter' });
152
+ return;
153
+ }
154
+ const begin = Date.now();
155
+ process.stdout.write(`\n[scenario] ${name}\n`);
156
+ const item = { name, providers, status: 'running', startedAt: new Date().toISOString() };
157
+ report.scenarios.push(item);
158
+ try {
159
+ item.evidence = await task();
160
+ item.status = 'passed';
161
+ } catch (error) {
162
+ item.status = 'failed';
163
+ item.error = error.message;
164
+ throw error;
165
+ } finally {
166
+ item.durationMs = Date.now() - begin;
167
+ process.stdout.write(`[${item.status}] ${name} (${item.durationMs}ms)\n`);
168
+ }
169
+ }
170
+
171
+ async function create(provider, capabilities, maxHourlyUsd, extra = []) {
172
+ const command = ['create'];
173
+ if (capabilities) command.push('--capabilities', capabilities);
174
+ command.push(
175
+ '--max-hourly-usd', String(maxHourlyUsd),
176
+ '--metadata', JSON.stringify({ e2eRun: runTag, scenarioProvider: provider }),
177
+ '--wait', '--wait-timeout', '6m', ...extra,
178
+ );
179
+ const detail = await cli(command, { provider });
180
+ assert(detail?.id, `${provider} create did not return id`);
181
+ assert(detail.observedState === 'RUNNING', `${provider} did not reach RUNNING`);
182
+ tracked.set(detail.id, provider);
183
+ return detail;
184
+ }
185
+
186
+ async function terminate(id, provider) {
187
+ const result = await cli(['terminate', id, '--wait-timeout', '6m'], { provider });
188
+ const state = result?.sandbox?.observedState || result?.observedState;
189
+ assert(['TERMINATED', 'FAILED'].includes(state), `${id} cleanup ended in ${state || 'unknown state'}`);
190
+ tracked.delete(id);
191
+ return result?.sandbox || result;
192
+ }
193
+
194
+ async function audits(id, provider) {
195
+ const values = (value) => Array.isArray(value) ? value : value?.items ?? value?.data ?? [];
196
+ const deadline = Date.now() + 60_000;
197
+ let lastError = 'audit has not settled';
198
+ while (Date.now() < deadline) {
199
+ const audit = {};
200
+ for (const kind of ['operations', 'events', 'usageSegments', 'billingPeriods']) {
201
+ audit[kind] = await cli(['audit', id, '--kind', kind]);
202
+ }
203
+ const detail = await cli(['get', id]);
204
+ const operations = values(audit.operations);
205
+ const events = values(audit.events);
206
+ const usageSegments = values(audit.usageSegments);
207
+ const billingPeriods = values(audit.billingPeriods);
208
+ const operationsValid = operations.length > 0
209
+ && operations.every((item) => item.status === 'SUCCEEDED');
210
+ const terminalEvent = events.some((item) => ['TERMINATED', 'FAILED'].includes(item.currentState));
211
+ const usageSettled = usageSegments.length > 0
212
+ && usageSegments.every((item) => item.status === 'SETTLED' && item.endsAt);
213
+ const billingSettled = billingPeriods.length > 0
214
+ && billingPeriods.every((item) => item.status === 'SETTLED' && item.endedAt);
215
+ const billingTotal = billingPeriods.reduce((sum, item) => sum + Number(item.amount || 0), 0);
216
+ const totalCost = Number(detail?.totalCost);
217
+ const costMatches = Number.isFinite(totalCost) && Number.isFinite(billingTotal)
218
+ && Math.abs(totalCost - billingTotal) <= 1e-9;
219
+ if (operationsValid && terminalEvent && usageSettled && billingSettled && costMatches) {
220
+ return {
221
+ counts: Object.fromEntries(Object.entries(audit).map(([kind, value]) => [kind, values(value).length])),
222
+ statuses: Object.fromEntries(Object.entries(audit).map(([kind, value]) => [
223
+ kind, values(value).map((item) => item.status || item.currentState || 'UNKNOWN'),
224
+ ])),
225
+ state: detail.observedState,
226
+ totalCost: detail.totalCost,
227
+ billingTotal,
228
+ verified: true,
229
+ };
230
+ }
231
+ lastError = [
232
+ !operationsValid && 'operation status',
233
+ !terminalEvent && 'terminal event',
234
+ !usageSettled && 'usage settlement',
235
+ !billingSettled && 'billing settlement',
236
+ !costMatches && `cost mismatch ${billingTotal} != ${detail?.totalCost}`,
237
+ ].filter(Boolean).join(', ');
238
+ await sleep(1_000);
239
+ }
240
+ throw new Error(`${provider} sandbox ${id} audit verification timed out: ${lastError}`);
241
+ }
242
+
243
+ async function oneShot(provider, command, marker, maxHourlyUsd) {
244
+ const value = await cli([
245
+ 'run', '--command', command, '--capabilities', 'exec',
246
+ '--metadata', JSON.stringify({ e2eRun: runTag, scenarioProvider: provider }),
247
+ '--max-hourly-usd', String(maxHourlyUsd), '--wait-timeout', '6m',
248
+ ], { provider });
249
+ assert(value?.finalState === 'TERMINATED', `${provider} one-shot left ${value?.finalState}`);
250
+ assert(markerIn(value?.result, marker), `${provider} output missed ${marker}`);
251
+ return {
252
+ instanceId: value.instanceId,
253
+ finalState: value.finalState,
254
+ totalCost: value.totalCost,
255
+ marker,
256
+ audit: await audits(value.instanceId, provider),
257
+ };
258
+ }
259
+
260
+ async function run() {
261
+ const baseline = await cli(['history', '--state', 'ACTIVE', '--page-size', '100']);
262
+ baselineActiveIds = new Set((baseline?.items || []).map((item) => item.id).filter(Boolean));
263
+ report.baseline = { activeInstances: baselineActiveIds.size };
264
+ await scenario('1. 跨服务商目录与报价', ['auto'], async () => {
265
+ const offerings = await cli(['offerings']);
266
+ const quote = await cli(['quote', '--capabilities', 'exec,files', '--max-hourly-usd', '0.60']);
267
+ assert(Array.isArray(offerings) && offerings.length >= 7, `expected >=7 offerings, got ${offerings?.length}`);
268
+ assert(quote?.quoteId && quote?.offering?.id, 'quote is incomplete');
269
+ return { offeringCount: offerings.length, selected: quote.offering.name, quoteId: quote.quoteId };
270
+ });
271
+
272
+ await scenario('2. AI 编码 Agent', ['daytona'], async () => {
273
+ const box = await create('daytona', 'exec,files', 0.20);
274
+ try {
275
+ await cli(['file', 'write', box.id, 'agent-task.txt', '--content', 'implement multiply and run tests'], { provider: 'daytona' });
276
+ const execution = await cli(['exec', box.id, '--command', "mkdir -p agent-demo; printf 'export const multiply = (a,b) => a*b;\\n' > agent-demo/math.js; node -e \"import('./agent-demo/math.js').then(m=>{if(m.multiply(6,7)!==42)process.exit(1);console.log('AI_AGENT_OK=42')})\""], { provider: 'daytona' });
277
+ assert(execution?.exitCode === 0 && markerIn(execution, 'AI_AGENT_OK=42'), 'AI coding test failed');
278
+ const artifact = await cli(['file', 'read', box.id, 'agent-demo/math.js'], { provider: 'daytona' });
279
+ assert(markerIn(artifact, 'multiply'), 'AI artifact was not readable');
280
+ const final = await terminate(box.id, 'daytona');
281
+ return { instanceId: box.id, artifactBytes: String(artifact?.content || '').length, audit: await audits(box.id, 'daytona'), finalState: final.observedState };
282
+ } finally {
283
+ if (tracked.has(box.id)) await terminate(box.id, 'daytona');
284
+ }
285
+ });
286
+
287
+ await scenario('3. CI 复现与自动修复', ['runloop'], async () => oneShot(
288
+ 'runloop',
289
+ "mkdir -p ci-demo; printf 'broken\\n' > ci-demo/status; sed -i.bak 's/broken/fixed/' ci-demo/status 2>/dev/null || sed -i 's/broken/fixed/' ci-demo/status; grep -q fixed ci-demo/status && echo CI_REPAIR_OK",
290
+ 'CI_REPAIR_OK', 0.30,
291
+ ));
292
+
293
+ await scenario('4. 数据分析与报告', ['modal'], async () => {
294
+ const box = await create('modal', 'exec,files', 0.60);
295
+ try {
296
+ await cli(['file', 'write', box.id, '/tmp/metrics.csv', '--content', 'day,value\nmon,12\ntue,18\nwed,24\n'], { provider: 'modal' });
297
+ const execution = await cli(['exec', box.id, '--command', "awk -F, 'NR>1{s+=$2;n++}END{printf \"DATA_REPORT_OK average=%.2f\\n\",s/n}' /tmp/metrics.csv | tee /tmp/report.txt"], { provider: 'modal' });
298
+ assert(execution?.exitCode === 0 && markerIn(execution, 'DATA_REPORT_OK average=18.00'), 'data result mismatch');
299
+ const reportFile = await cli(['file', 'read', box.id, '/tmp/report.txt'], { provider: 'modal' });
300
+ assert(markerIn(reportFile, 'average=18.00'), 'report file was not persisted');
301
+ const final = await terminate(box.id, 'modal');
302
+ return { instanceId: box.id, result: 'average=18.00', audit: await audits(box.id, 'modal'), finalState: final.observedState };
303
+ } finally {
304
+ if (tracked.has(box.id)) await terminate(box.id, 'modal');
305
+ }
306
+ });
307
+
308
+ await scenario('5. 多 Agent 并行隔离', ['e2b', 'vc-sandbox'], async () => {
309
+ const [implementer, reviewer] = await Promise.all([
310
+ oneShot('e2b', "sleep 2; echo IMPLEMENT_AGENT_OK", 'IMPLEMENT_AGENT_OK', 0.30),
311
+ oneShot('vc-sandbox', "sleep 2; echo REVIEW_AGENT_OK", 'REVIEW_AGENT_OK', 0.50),
312
+ ]);
313
+ assert(implementer.instanceId !== reviewer.instanceId, 'agents unexpectedly shared one instance');
314
+ return {
315
+ isolatedInstanceIds: [implementer.instanceId, reviewer.instanceId],
316
+ finalStates: [implementer.finalState, reviewer.finalState],
317
+ audits: [implementer.audit, reviewer.audit],
318
+ };
319
+ });
320
+
321
+ if (!skipGpu) {
322
+ await scenario('6. GPU 实例与连接信息', ['runpod'], async () => {
323
+ const box = await create('runpod', null, 1.00, ['--gpu-count', '1']);
324
+ try {
325
+ let connection = null;
326
+ for (let attempt = 1; attempt <= 30; attempt += 1) {
327
+ connection = await cli(['extension', box.id, 'runpod.connection_info', '--input', '{}'], { provider: 'runpod' });
328
+ if (connection?.result?.result?.connectionReady) break;
329
+ await sleep(10_000);
330
+ }
331
+ const info = connection?.result?.result;
332
+ assert(info?.connectionReady === true, 'RunPod connection info did not become ready');
333
+ assert(info?.publicIp && Object.keys(info?.portMappings || {}).length > 0, 'RunPod connection details are incomplete');
334
+ const final = await terminate(box.id, 'runpod');
335
+ return { instanceId: box.id, connectionReady: true, publicIpPresent: true, portMappingCount: Object.keys(info.portMappings).length, audit: await audits(box.id, 'runpod'), finalState: final.observedState };
336
+ } finally {
337
+ if (tracked.has(box.id)) await terminate(box.id, 'runpod');
338
+ }
339
+ });
340
+ } else {
341
+ report.scenarios.push({ name: '6. GPU 实例与连接信息', providers: ['runpod'], status: 'skipped' });
342
+ }
343
+
344
+ await scenario('7. 单次代码执行', ['auto'], async () => oneShot(
345
+ 'auto',
346
+ "python3 -c \"print('RUN_CODE_OK=' + str(sum(i*i for i in range(10))))\"",
347
+ 'RUN_CODE_OK=285', 0.20,
348
+ ));
349
+
350
+ await scenario('8. Cloudflare Web 预览与临时 API', ['cf-edge'], async () => {
351
+ const allocations = [];
352
+ // Quick Tunnels are explicitly a best-effort debugging surface. A small
353
+ // fraction of allocations complete the control-plane handshake but never
354
+ // serve TLS. Retry with a fresh sandbox (and therefore a fresh tunnel
355
+ // process) while the provider-side adapter rollout gains its own probe.
356
+ for (let allocation = 1; allocation <= 4; allocation += 1) {
357
+ const box = await create('cf-edge', 'exec,files,ports', 0.20);
358
+ let tunnelHost = '';
359
+ try {
360
+ await cli(['file', 'write', box.id, 'index.html', '--content', '<!doctype html><h1>CF_PLAYGROUND_OK</h1>'], { provider: 'cf-edge' });
361
+ const server = await cli([
362
+ 'exec', box.id, '--command',
363
+ 'nohup python3 -m http.server 8080 >/tmp/serve.log 2>&1 & i=0; until curl -sf http://127.0.0.1:8080/; do i=$((i+1)); [ "$i" -ge 20 ] && exit 7; sleep 1; done',
364
+ ], { provider: 'cf-edge' });
365
+ assert(server?.exitCode === 0 && markerIn(server, 'CF_PLAYGROUND_OK'), 'Cloudflare local preview failed');
366
+ const port = await cli(['port', box.id, '8080'], { provider: 'cf-edge' });
367
+ assert(port?.url, 'Cloudflare port did not return a public URL');
368
+ tunnelHost = new URL(port.url).hostname;
369
+ process.stdout.write(` Cloudflare tunnel allocation ${allocation}/4: ${tunnelHost}\n`);
370
+ let publicBody = '';
371
+ let lastPublicError = '';
372
+ for (let attempt = 1; attempt <= 4; attempt += 1) {
373
+ const probe = await runBinary('curl', [
374
+ '--silent', '--show-error', '--fail', '--max-time', '8', port.url,
375
+ ]);
376
+ publicBody = probe.stdout;
377
+ if (probe.code === 0 && publicBody.includes('CF_PLAYGROUND_OK')) break;
378
+ lastPublicError = probe.stderr || `curl exit ${probe.code}`;
379
+ process.stdout.write(` Cloudflare public probe ${attempt}/4: ${lastPublicError}\n`);
380
+ await sleep(2_000);
381
+ }
382
+ if (publicBody.includes('CF_PLAYGROUND_OK')) {
383
+ const final = await terminate(box.id, 'cf-edge');
384
+ allocations.push({ instanceId: box.id, tunnelHost, reachable: true });
385
+ return {
386
+ instanceId: box.id,
387
+ publicUrlVerified: true,
388
+ publicStatus: 200,
389
+ tunnelAllocations: allocations,
390
+ audit: await audits(box.id, 'cf-edge'),
391
+ finalState: final.observedState,
392
+ };
393
+ }
394
+ allocations.push({ instanceId: box.id, tunnelHost, reachable: false, error: lastPublicError });
395
+ } finally {
396
+ if (tracked.has(box.id)) await terminate(box.id, 'cf-edge');
397
+ }
398
+ }
399
+ throw new Error(`Cloudflare public URL stayed unreachable across ${allocations.length} fresh allocations`);
400
+ });
401
+
402
+ await scenario('9. 挂起、恢复与文件持久性', ['e2b'], async () => {
403
+ const box = await create('e2b', 'exec,files', 0.30);
404
+ try {
405
+ await cli(['file', 'write', box.id, 'lifecycle-marker.txt', '--content', 'XAPI_LIFECYCLE_OK=42'], { provider: 'e2b' });
406
+ const suspended = await cli(['suspend', box.id, '--wait-timeout', '6m'], { provider: 'e2b' });
407
+ assert(suspended?.sandbox?.observedState === 'SUSPENDED', 'E2B did not suspend');
408
+ const resumed = await cli(['resume', box.id, '--wait-timeout', '6m'], { provider: 'e2b' });
409
+ assert(resumed?.sandbox?.observedState === 'RUNNING', 'E2B did not resume');
410
+ const marker = await cli(['file', 'read', box.id, 'lifecycle-marker.txt'], { provider: 'e2b' });
411
+ assert(markerIn(marker, 'XAPI_LIFECYCLE_OK=42'), 'file did not survive suspend/resume');
412
+ const final = await terminate(box.id, 'e2b');
413
+ return { instanceId: box.id, markerPreserved: true, audit: await audits(box.id, 'e2b'), finalState: final.observedState };
414
+ } finally {
415
+ if (tracked.has(box.id)) await terminate(box.id, 'e2b');
416
+ }
417
+ });
418
+ }
419
+
420
+ let failure = null;
421
+ try {
422
+ await run();
423
+ } catch (error) {
424
+ failure = error;
425
+ } finally {
426
+ for (const [id, provider] of [...tracked.entries()]) {
427
+ try {
428
+ const detail = await terminate(id, provider);
429
+ report.cleanup.push({ id, provider, status: detail.observedState });
430
+ } catch (error) {
431
+ report.cleanup.push({ id, provider, status: 'failed', error: error.message });
432
+ failure ||= error;
433
+ }
434
+ }
435
+ try {
436
+ const active = await cli(['history', '--state', 'ACTIVE', '--page-size', '100']);
437
+ const activeItems = active?.items || [];
438
+ const newActive = activeItems.filter((item) => item.id && !baselineActiveIds.has(item.id));
439
+ report.finalGate = {
440
+ accountActiveInstances: active?.total ?? activeItems.length,
441
+ baselineActiveInstances: baselineActiveIds.size,
442
+ testCreatedActiveInstances: newActive.map((item) => item.id),
443
+ stateCounts: active?.stateCounts,
444
+ };
445
+ if (newActive.length !== 0) {
446
+ failure ||= new Error(`zero-residual gate failed: ${newActive.length} test-created instances remain`);
447
+ }
448
+ } catch (error) {
449
+ report.finalGate = { error: error.message };
450
+ failure ||= error;
451
+ }
452
+ report.finishedAt = new Date().toISOString();
453
+ report.durationMs = Date.now() - startedAt.getTime();
454
+ report.status = failure ? 'failed' : 'passed';
455
+ await mkdir(dirname(reportPath), { recursive: true });
456
+ await writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8');
457
+ process.stdout.write(`\nReport: ${reportPath}\nStatus: ${report.status}\n`);
458
+ }
459
+
460
+ if (failure) {
461
+ process.stderr.write(`${failure.stack || failure.message}\n`);
462
+ process.exitCode = 1;
463
+ }