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.
- package/LICENSE +21 -0
- package/README.md +258 -10
- package/dist/chunk-TYY6JR6O.js +870 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1256 -590
- package/dist/openai-sandbox-client.d.ts +85 -0
- package/dist/openai-sandbox-client.js +285 -0
- package/examples/openai-agents-sandbox-local.ts +131 -0
- package/examples/sandbox-api-cli-openai.mjs +450 -0
- package/package.json +33 -4
- package/scripts/openai-sandbox-agent-e2e.ts +219 -0
- package/scripts/sandbox-playground-e2e.mjs +463 -0
- package/skills/xapi/SKILL.md +498 -0
- package/skills/xapi/guides/ai.md +200 -0
- package/skills/xapi/guides/ai_gateway.md +263 -0
- package/skills/xapi/guides/crypto.md +197 -0
- package/skills/xapi/guides/douyin.md +297 -0
- package/skills/xapi/guides/google_search.md +194 -0
- package/skills/xapi/guides/linkedin.md +253 -0
- package/skills/xapi/guides/reddit.md +312 -0
- package/skills/xapi/guides/sandbox.md +466 -0
- package/skills/xapi/guides/serper.md +124 -0
- package/skills/xapi/guides/sms.md +186 -0
- package/skills/xapi/guides/tiktok.md +322 -0
- package/skills/xapi/guides/twitter.md +276 -0
- package/skills/xapi/guides/weibo.md +301 -0
- package/skills/xapi/guides/ws_gateway.md +206 -0
- package/skills/xapi/guides/xiaohongshu.md +315 -0
- package/skills/xapi/scripts/download_tweet_videos.sh +125 -0
- package/src/client.ts +664 -0
- package/src/config.ts +160 -0
- package/src/openai-sandbox-client.ts +349 -0
- package/src/sandbox-client.ts +289 -0
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Zero-context xAPI Sandbox walkthrough.
|
|
5
|
+
*
|
|
6
|
+
* It demonstrates three independent entry points against the test service:
|
|
7
|
+
* 1. Direct Sandbox HTTP API lifecycle
|
|
8
|
+
* 2. Local xapi-to CLI one-shot lifecycle
|
|
9
|
+
* 3. OpenAI Agents SDK SandboxAgent + xAPI DeepSeek + xAPI Sandbox
|
|
10
|
+
*
|
|
11
|
+
* No instance ID is required. Every demo creates its own instance and verifies
|
|
12
|
+
* termination/cost before returning. Credentials are read only from environment
|
|
13
|
+
* variables or the normal local xAPI configuration and are never embedded here.
|
|
14
|
+
*
|
|
15
|
+
* Usage (run through the package script so dist/ is built first):
|
|
16
|
+
* npm run demo:sandbox
|
|
17
|
+
* npm run demo:sandbox -- api
|
|
18
|
+
* npm run demo:sandbox -- cli
|
|
19
|
+
* npm run demo:sandbox -- openai
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { randomUUID } from 'node:crypto';
|
|
23
|
+
import { readFile } from 'node:fs/promises';
|
|
24
|
+
import { homedir } from 'node:os';
|
|
25
|
+
import { dirname, join, resolve } from 'node:path';
|
|
26
|
+
import { fileURLToPath } from 'node:url';
|
|
27
|
+
import { spawn } from 'node:child_process';
|
|
28
|
+
import { OpenAIProvider, Runner } from '@openai/agents';
|
|
29
|
+
import { Manifest, SandboxAgent, shell } from '@openai/agents/sandbox';
|
|
30
|
+
import { XapiAgentsSandboxClient } from '../dist/openai-sandbox-client.js';
|
|
31
|
+
|
|
32
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
33
|
+
const CLI = resolve(HERE, '../dist/index.js');
|
|
34
|
+
const mode = process.argv[2] || 'all';
|
|
35
|
+
const allowedModes = new Set(['all', 'api', 'cli', 'openai']);
|
|
36
|
+
|
|
37
|
+
if (!allowedModes.has(mode)) {
|
|
38
|
+
throw new Error('usage: npm run demo:sandbox -- [all|api|cli|openai]');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const sandboxHost = process.env.XAPI_SANDBOX_HOST || 'sandbox.test.xapi.to';
|
|
42
|
+
const provider = process.env.XAPI_SANDBOX_PROVIDER || 'daytona';
|
|
43
|
+
const model = process.env.XAPI_MODEL || 'deepseek-v4-pro';
|
|
44
|
+
const maxHourlyUsd = Number(process.env.XAPI_SANDBOX_MAX_HOURLY_USD || '0.20');
|
|
45
|
+
const effectiveSandboxApiKey = process.env.XAPI_SANDBOX_KEY || process.env.XAPI_TEST_API_KEY;
|
|
46
|
+
const sandboxCredentialSource = process.env.XAPI_SANDBOX_KEY
|
|
47
|
+
? 'XAPI_SANDBOX_KEY'
|
|
48
|
+
: process.env.XAPI_TEST_API_KEY
|
|
49
|
+
? 'XAPI_TEST_API_KEY'
|
|
50
|
+
: null;
|
|
51
|
+
|
|
52
|
+
if (!effectiveSandboxApiKey) {
|
|
53
|
+
throw new Error('set XAPI_SANDBOX_KEY (or XAPI_TEST_API_KEY) before running the Sandbox demo');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!Number.isFinite(maxHourlyUsd) || maxHourlyUsd <= 0) {
|
|
57
|
+
throw new Error('XAPI_SANDBOX_MAX_HOURLY_USD must be a positive number');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function step(scope, message) {
|
|
61
|
+
console.log(`\n[${scope}] ${message}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function assert(condition, message) {
|
|
65
|
+
if (!condition) throw new Error(message);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function sleep(ms) {
|
|
69
|
+
return new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function loadAiApiKey() {
|
|
73
|
+
if (process.env.XAPI_AI_KEY) return { apiKey: process.env.XAPI_AI_KEY, source: 'XAPI_AI_KEY' };
|
|
74
|
+
try {
|
|
75
|
+
const config = JSON.parse(await readFile(join(homedir(), '.xapi', 'config.json'), 'utf8'));
|
|
76
|
+
if (typeof config?.apiKey === 'string' && config.apiKey.trim()) {
|
|
77
|
+
return { apiKey: config.apiKey, source: '~/.xapi/config.json' };
|
|
78
|
+
}
|
|
79
|
+
} catch {
|
|
80
|
+
// Fall through to the focused error below.
|
|
81
|
+
}
|
|
82
|
+
throw new Error(
|
|
83
|
+
'OpenAI/DeepSeek demo needs a production ai.xapi.to credential in XAPI_AI_KEY ' +
|
|
84
|
+
'or ~/.xapi/config.json; Sandbox and AI credentials are intentionally kept separate',
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function sandboxBaseUrl(host, pinnedProvider) {
|
|
89
|
+
const url = new URL(host.includes('://') ? host : `https://${host}`);
|
|
90
|
+
if (url.protocol !== 'https:') throw new Error('public Sandbox host must use HTTPS');
|
|
91
|
+
if (!url.hostname.endsWith('.xapi.to')) throw new Error('Sandbox host must be under *.xapi.to');
|
|
92
|
+
if (pinnedProvider && pinnedProvider !== 'auto') {
|
|
93
|
+
assert(/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(pinnedProvider), 'invalid provider');
|
|
94
|
+
const labels = url.hostname.split('.');
|
|
95
|
+
const sandboxIndex = labels.indexOf('sandbox');
|
|
96
|
+
const productionAliases = { daytona: 'daytona-sandbox', e2b: 'e2b-sandbox' };
|
|
97
|
+
const gatewayLabel = labels.slice(sandboxIndex).join('.') === 'sandbox.xapi.to'
|
|
98
|
+
? productionAliases[pinnedProvider] || pinnedProvider
|
|
99
|
+
: pinnedProvider;
|
|
100
|
+
if (sandboxIndex === 0) labels.unshift(gatewayLabel);
|
|
101
|
+
else if (sandboxIndex === 1) labels[0] = gatewayLabel;
|
|
102
|
+
else throw new Error('provider pinning requires a sandbox.<xapi-domain> host');
|
|
103
|
+
url.hostname = labels.join('.');
|
|
104
|
+
}
|
|
105
|
+
return url.toString().replace(/\/$/, '');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function apiRequest(apiKey, baseUrl, path, init = {}) {
|
|
109
|
+
const response = await fetch(`${baseUrl}${path}`, {
|
|
110
|
+
redirect: 'error',
|
|
111
|
+
...init,
|
|
112
|
+
headers: {
|
|
113
|
+
accept: 'application/json',
|
|
114
|
+
'xapi-key': apiKey,
|
|
115
|
+
...(init.body ? { 'content-type': 'application/json' } : {}),
|
|
116
|
+
...init.headers,
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
const text = await response.text();
|
|
120
|
+
let body;
|
|
121
|
+
try { body = text ? JSON.parse(text) : {}; }
|
|
122
|
+
catch { body = { raw: text.slice(0, 500) }; }
|
|
123
|
+
if (!response.ok) {
|
|
124
|
+
throw new Error(`${init.method || 'GET'} ${path} failed: HTTP ${response.status} ${JSON.stringify(body)}`);
|
|
125
|
+
}
|
|
126
|
+
return body;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function waitForState(apiKey, baseUrl, id, wanted, timeoutMs = 360_000) {
|
|
130
|
+
const deadline = Date.now() + timeoutMs;
|
|
131
|
+
let detail;
|
|
132
|
+
while (Date.now() < deadline) {
|
|
133
|
+
detail = await apiRequest(apiKey, baseUrl, `/v1/sandboxes/${encodeURIComponent(id)}`);
|
|
134
|
+
if (wanted.includes(detail.observedState)) return detail;
|
|
135
|
+
if (['FAILED', 'TERMINATED'].includes(detail.observedState)) {
|
|
136
|
+
throw new Error(`sandbox ${id} entered ${detail.observedState} while waiting for ${wanted.join('/')}`);
|
|
137
|
+
}
|
|
138
|
+
await sleep(2_000);
|
|
139
|
+
}
|
|
140
|
+
throw new Error(`sandbox ${id} did not enter ${wanted.join('/')} (last: ${detail?.observedState})`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function terminateApiSandbox(apiKey, baseUrl, id) {
|
|
144
|
+
let detail = await apiRequest(apiKey, baseUrl, `/v1/sandboxes/${encodeURIComponent(id)}`);
|
|
145
|
+
const deadline = Date.now() + 360_000;
|
|
146
|
+
const idempotencyKey = `demo:api:terminate:${randomUUID()}`;
|
|
147
|
+
while (!['TERMINATED', 'FAILED'].includes(detail.observedState) && Date.now() < deadline) {
|
|
148
|
+
try {
|
|
149
|
+
await apiRequest(apiKey, baseUrl, `/v1/sandboxes/${encodeURIComponent(id)}/terminate`, {
|
|
150
|
+
method: 'POST',
|
|
151
|
+
body: JSON.stringify({ idempotencyKey }),
|
|
152
|
+
});
|
|
153
|
+
return waitForState(apiKey, baseUrl, id, ['TERMINATED', 'FAILED'], deadline - Date.now());
|
|
154
|
+
} catch (error) {
|
|
155
|
+
detail = await apiRequest(apiKey, baseUrl, `/v1/sandboxes/${encodeURIComponent(id)}`);
|
|
156
|
+
if (['TERMINATED', 'FAILED'].includes(detail.observedState)) return detail;
|
|
157
|
+
if (!String(error?.message || error).includes('HTTP 409')) throw error;
|
|
158
|
+
await sleep(2_000);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (!['TERMINATED', 'FAILED'].includes(detail.observedState)) {
|
|
162
|
+
throw new Error(`sandbox ${id} cleanup timed out in ${detail.observedState}`);
|
|
163
|
+
}
|
|
164
|
+
return detail;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function itemCount(value) {
|
|
168
|
+
if (Array.isArray(value)) return value.length;
|
|
169
|
+
return value?.items?.length ?? value?.data?.length ?? 0;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function items(value) {
|
|
173
|
+
if (Array.isArray(value)) return value;
|
|
174
|
+
return value?.items ?? value?.data ?? [];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function readAndVerifyAudit(apiKey, baseUrl, id) {
|
|
178
|
+
const audit = {};
|
|
179
|
+
for (const kind of ['operations', 'events', 'usageSegments', 'billingPeriods']) {
|
|
180
|
+
audit[kind] = await apiRequest(
|
|
181
|
+
apiKey,
|
|
182
|
+
baseUrl,
|
|
183
|
+
`/v1/sandboxes/${encodeURIComponent(id)}/audit?kind=${kind}&page=1&pageSize=100`,
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
const operations = items(audit.operations);
|
|
187
|
+
const events = items(audit.events);
|
|
188
|
+
const usageSegments = items(audit.usageSegments);
|
|
189
|
+
const billingPeriods = items(audit.billingPeriods);
|
|
190
|
+
assert(operations.length > 0, `sandbox ${id} has no operations audit`);
|
|
191
|
+
assert(operations.every((item) => item.status === 'SUCCEEDED'), `sandbox ${id} has a non-SUCCEEDED operation`);
|
|
192
|
+
assert(events.some((item) => item.currentState === 'TERMINATED'), `sandbox ${id} has no TERMINATED event`);
|
|
193
|
+
assert(usageSegments.length > 0, `sandbox ${id} has no usage segments`);
|
|
194
|
+
assert(usageSegments.every((item) => item.status === 'SETTLED' && item.endsAt), `sandbox ${id} has open usage`);
|
|
195
|
+
assert(billingPeriods.length > 0, `sandbox ${id} has no billing periods`);
|
|
196
|
+
assert(billingPeriods.every((item) => item.status === 'SETTLED' && item.endedAt), `sandbox ${id} has open billing`);
|
|
197
|
+
return Object.fromEntries(Object.entries(audit).map(([kind, value]) => [kind, itemCount(value)]));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function runApiDemo(apiKey) {
|
|
201
|
+
const scope = 'API';
|
|
202
|
+
const baseUrl = sandboxBaseUrl(sandboxHost, provider);
|
|
203
|
+
let id;
|
|
204
|
+
let finalDetail;
|
|
205
|
+
let failure;
|
|
206
|
+
|
|
207
|
+
step(scope, `1/7 获取 Offering(${baseUrl})`);
|
|
208
|
+
const offerings = await apiRequest(apiKey, baseUrl, '/v1/offerings');
|
|
209
|
+
assert(Array.isArray(offerings) && offerings.length > 0, 'no Sandbox offerings returned');
|
|
210
|
+
|
|
211
|
+
try {
|
|
212
|
+
step(scope, '2/7 按 exec/files 能力和价格上限获取报价');
|
|
213
|
+
const quote = await apiRequest(apiKey, baseUrl, '/v1/quotes', {
|
|
214
|
+
method: 'POST',
|
|
215
|
+
body: JSON.stringify({
|
|
216
|
+
requirements: { capabilities: ['exec', 'files'] },
|
|
217
|
+
maxEstimatedHourlyUsd: maxHourlyUsd.toFixed(8),
|
|
218
|
+
}),
|
|
219
|
+
});
|
|
220
|
+
assert(quote.quoteId, 'quote response did not include quoteId');
|
|
221
|
+
|
|
222
|
+
step(scope, '3/7 使用 quoteId 和幂等键创建实例');
|
|
223
|
+
const created = await apiRequest(apiKey, baseUrl, '/v1/sandboxes', {
|
|
224
|
+
method: 'POST',
|
|
225
|
+
body: JSON.stringify({
|
|
226
|
+
selection: { quoteId: quote.quoteId },
|
|
227
|
+
metadata: { client: 'sandbox-api-cli-openai-demo', scenario: 'direct-api' },
|
|
228
|
+
idempotencyKey: `demo:api:create:${randomUUID()}`,
|
|
229
|
+
}),
|
|
230
|
+
});
|
|
231
|
+
id = created.id;
|
|
232
|
+
assert(id, 'create response did not include sandbox id');
|
|
233
|
+
|
|
234
|
+
step(scope, `4/7 等待实例 ${id} 进入 RUNNING`);
|
|
235
|
+
await waitForState(apiKey, baseUrl, id, ['RUNNING']);
|
|
236
|
+
|
|
237
|
+
step(scope, '5/7 执行真实 Shell 命令并验证 marker');
|
|
238
|
+
const result = await apiRequest(apiKey, baseUrl, `/v1/sandboxes/${encodeURIComponent(id)}/commands`, {
|
|
239
|
+
method: 'POST',
|
|
240
|
+
body: JSON.stringify({ command: 'printf "API_DEMO_OK=42\\n"', timeoutSeconds: 60 }),
|
|
241
|
+
});
|
|
242
|
+
assert(result.exitCode === 0, `remote exit code was ${result.exitCode}`);
|
|
243
|
+
assert(String(result.stdout).includes('API_DEMO_OK=42'), 'API marker was not returned');
|
|
244
|
+
} catch (error) {
|
|
245
|
+
failure = error;
|
|
246
|
+
} finally {
|
|
247
|
+
if (id) {
|
|
248
|
+
step(scope, '6/7 finally 终止实例并等待服务端终态');
|
|
249
|
+
try { finalDetail = await terminateApiSandbox(apiKey, baseUrl, id); }
|
|
250
|
+
catch (error) { failure ||= error; }
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (failure) throw failure;
|
|
255
|
+
assert(finalDetail?.observedState === 'TERMINATED', `API instance ended in ${finalDetail?.observedState}`);
|
|
256
|
+
assert(finalDetail.totalCost !== undefined, 'API final cost is missing');
|
|
257
|
+
|
|
258
|
+
step(scope, '7/7 验证操作、事件、用量、账单与最终费用');
|
|
259
|
+
const audits = await readAndVerifyAudit(apiKey, baseUrl, id);
|
|
260
|
+
return {
|
|
261
|
+
instanceId: id,
|
|
262
|
+
marker: 'API_DEMO_OK=42',
|
|
263
|
+
finalState: finalDetail.observedState,
|
|
264
|
+
totalCost: finalDetail.totalCost,
|
|
265
|
+
auditCounts: audits,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async function runProcess(command, args, env = process.env) {
|
|
270
|
+
return new Promise((resolveRun, rejectRun) => {
|
|
271
|
+
const child = spawn(command, args, { env, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
272
|
+
let stdout = '';
|
|
273
|
+
let stderr = '';
|
|
274
|
+
child.stdout.on('data', (chunk) => { stdout += chunk; });
|
|
275
|
+
child.stderr.on('data', (chunk) => { stderr += chunk; });
|
|
276
|
+
child.once('error', rejectRun);
|
|
277
|
+
child.once('close', (code) => resolveRun({ code, stdout, stderr }));
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async function runCliDemo(apiKey) {
|
|
282
|
+
const scope = 'CLI';
|
|
283
|
+
step(scope, '调用本地构建的 xapi-to sandbox run');
|
|
284
|
+
const executed = await runProcess(process.execPath, [
|
|
285
|
+
CLI,
|
|
286
|
+
'sandbox', 'run',
|
|
287
|
+
'--host', sandboxHost,
|
|
288
|
+
'--provider', provider,
|
|
289
|
+
'--max-hourly-usd', maxHourlyUsd.toFixed(2),
|
|
290
|
+
'--command', 'printf "CLI_DEMO_OK=42\\n"',
|
|
291
|
+
], { ...process.env, XAPI_KEY: apiKey, XAPI_API_KEY: '' });
|
|
292
|
+
if (executed.code !== 0) {
|
|
293
|
+
throw new Error(`CLI exited ${executed.code}: ${executed.stderr || executed.stdout}`);
|
|
294
|
+
}
|
|
295
|
+
const result = JSON.parse(executed.stdout);
|
|
296
|
+
assert(result.result?.exitCode === 0, 'CLI remote command failed');
|
|
297
|
+
assert(String(result.result?.stdout).includes('CLI_DEMO_OK=42'), 'CLI marker was not returned');
|
|
298
|
+
assert(result.finalState === 'TERMINATED', `CLI instance ended in ${result.finalState}`);
|
|
299
|
+
assert(result.cleanup?.operationStatus === 'SUCCEEDED', 'CLI cleanup operation did not succeed');
|
|
300
|
+
assert(result.cleanup?.state === 'TERMINATED', 'CLI cleanup did not reach TERMINATED');
|
|
301
|
+
assert(result.totalCost !== undefined, 'CLI final cost is missing');
|
|
302
|
+
const auditCounts = await readAndVerifyAudit(
|
|
303
|
+
apiKey,
|
|
304
|
+
sandboxBaseUrl(sandboxHost, provider),
|
|
305
|
+
result.instanceId,
|
|
306
|
+
);
|
|
307
|
+
return {
|
|
308
|
+
instanceId: result.instanceId,
|
|
309
|
+
clientIdempotencyKey: result.clientIdempotencyKey,
|
|
310
|
+
marker: 'CLI_DEMO_OK=42',
|
|
311
|
+
finalState: result.finalState,
|
|
312
|
+
cleanup: result.cleanup,
|
|
313
|
+
totalCost: result.totalCost,
|
|
314
|
+
auditCounts,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
async function runOpenAiDemo(sandboxKey, aiKey) {
|
|
319
|
+
const scope = 'OPENAI';
|
|
320
|
+
const sandbox = new XapiAgentsSandboxClient({
|
|
321
|
+
apiKey: sandboxKey,
|
|
322
|
+
sandboxHost,
|
|
323
|
+
provider,
|
|
324
|
+
maxHourlyUsd,
|
|
325
|
+
model,
|
|
326
|
+
});
|
|
327
|
+
const modelProvider = new OpenAIProvider({
|
|
328
|
+
apiKey: aiKey,
|
|
329
|
+
baseURL: 'https://ai.xapi.to/v1',
|
|
330
|
+
useResponses: false,
|
|
331
|
+
strictFeatureValidation: true,
|
|
332
|
+
});
|
|
333
|
+
const runner = new Runner({ modelProvider, tracingDisabled: true });
|
|
334
|
+
const agent = new SandboxAgent({
|
|
335
|
+
name: 'xAPI DeepSeek demo agent',
|
|
336
|
+
model,
|
|
337
|
+
defaultManifest: new Manifest({ root: sandbox.workspaceRoot }),
|
|
338
|
+
capabilities: [shell()],
|
|
339
|
+
instructions: [
|
|
340
|
+
'Work only inside the sandbox workspace.',
|
|
341
|
+
'Use one shell call to write exactly SDK_OK=42 to result.txt.',
|
|
342
|
+
'Use a second shell call to read result.txt.',
|
|
343
|
+
'After the output is verified, reply exactly SDK_OK=42.',
|
|
344
|
+
].join(' '),
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
let finalOutput = '';
|
|
348
|
+
let failure;
|
|
349
|
+
step(scope, `DeepSeek (${model}) 通过 ai.xapi.to 驱动 SandboxAgent`);
|
|
350
|
+
try {
|
|
351
|
+
const result = await runner.run(agent, 'Create and verify result.txt now.', {
|
|
352
|
+
maxTurns: 8,
|
|
353
|
+
sandbox: { client: sandbox },
|
|
354
|
+
});
|
|
355
|
+
finalOutput = String(result.finalOutput || '');
|
|
356
|
+
assert(finalOutput.includes('SDK_OK=42'), 'OpenAI agent final marker was not returned');
|
|
357
|
+
// One call prepares the workspace; the Agent must add separate write/read calls.
|
|
358
|
+
assert(sandbox.evidence.execCount >= 3, 'OpenAI agent did not perform separate write/read shell calls');
|
|
359
|
+
assert(sandbox.evidence.shellMarkerSeen, 'OpenAI agent shell output did not contain the marker');
|
|
360
|
+
} catch (error) {
|
|
361
|
+
failure = error;
|
|
362
|
+
} finally {
|
|
363
|
+
step(scope, '关闭 SDK Session,终止实例并读取审计与费用');
|
|
364
|
+
try { await sandbox.lastSession?.close(); }
|
|
365
|
+
catch (error) { failure ||= error; }
|
|
366
|
+
}
|
|
367
|
+
if (failure) throw failure;
|
|
368
|
+
assert(sandbox.evidence.finalState === 'TERMINATED', `OpenAI instance ended in ${sandbox.evidence.finalState}`);
|
|
369
|
+
assert(sandbox.evidence.totalCost !== undefined, 'OpenAI final cost is missing');
|
|
370
|
+
const auditCounts = await readAndVerifyAudit(
|
|
371
|
+
sandboxKey,
|
|
372
|
+
sandboxBaseUrl(sandboxHost, provider),
|
|
373
|
+
sandbox.evidence.instanceId,
|
|
374
|
+
);
|
|
375
|
+
return {
|
|
376
|
+
modelGateway: 'https://ai.xapi.to/v1',
|
|
377
|
+
model,
|
|
378
|
+
tracingDisabled: true,
|
|
379
|
+
finalOutput,
|
|
380
|
+
...sandbox.evidence,
|
|
381
|
+
auditCounts,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
async function finalActiveGate(apiKey, testInstanceIds) {
|
|
386
|
+
const baseUrl = sandboxBaseUrl(sandboxHost, 'auto');
|
|
387
|
+
const active = await apiRequest(apiKey, baseUrl, '/v1/sandbox-history?state=ACTIVE&page=1&pageSize=100');
|
|
388
|
+
const count = Number(active.total ?? itemCount(active));
|
|
389
|
+
const activeIds = new Set(items(active).map((item) => item.id).filter(Boolean));
|
|
390
|
+
const testCreatedActiveInstances = testInstanceIds.filter((id) => activeIds.has(id));
|
|
391
|
+
return {
|
|
392
|
+
accountActiveInstances: count,
|
|
393
|
+
testCreatedActiveInstances,
|
|
394
|
+
stateCounts: active.stateCounts,
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const aiCredential = await loadAiApiKey();
|
|
399
|
+
const report = {
|
|
400
|
+
status: 'running',
|
|
401
|
+
mode,
|
|
402
|
+
sandboxHost,
|
|
403
|
+
provider,
|
|
404
|
+
model,
|
|
405
|
+
environment: {
|
|
406
|
+
sandbox: sandboxHost.includes('.test.') ? 'test' : 'production',
|
|
407
|
+
modelGateway: 'production (ai.xapi.to has no test environment)',
|
|
408
|
+
},
|
|
409
|
+
credentials: {
|
|
410
|
+
sandbox: { source: sandboxCredentialSource },
|
|
411
|
+
ai: { source: aiCredential.source },
|
|
412
|
+
},
|
|
413
|
+
startedAt: new Date().toISOString(),
|
|
414
|
+
results: {},
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
try {
|
|
418
|
+
if (mode === 'all' || mode === 'api') {
|
|
419
|
+
report.results.api = await runApiDemo(effectiveSandboxApiKey);
|
|
420
|
+
}
|
|
421
|
+
if (mode === 'all' || mode === 'cli') {
|
|
422
|
+
report.results.cli = await runCliDemo(effectiveSandboxApiKey);
|
|
423
|
+
}
|
|
424
|
+
if (mode === 'all' || mode === 'openai') {
|
|
425
|
+
report.results.openai = await runOpenAiDemo(effectiveSandboxApiKey, aiCredential.apiKey);
|
|
426
|
+
}
|
|
427
|
+
report.status = 'passed';
|
|
428
|
+
} catch (error) {
|
|
429
|
+
report.status = 'failed';
|
|
430
|
+
report.error = error instanceof Error ? error.message : String(error);
|
|
431
|
+
process.exitCode = 1;
|
|
432
|
+
} finally {
|
|
433
|
+
try {
|
|
434
|
+
const testInstanceIds = Object.values(report.results)
|
|
435
|
+
.map((result) => result?.instanceId)
|
|
436
|
+
.filter(Boolean);
|
|
437
|
+
report.finalGate = await finalActiveGate(effectiveSandboxApiKey, testInstanceIds);
|
|
438
|
+
if (report.finalGate.testCreatedActiveInstances.length !== 0) {
|
|
439
|
+
report.status = 'failed';
|
|
440
|
+
report.error ||= `${report.finalGate.testCreatedActiveInstances.length} demo Sandbox instances remain active`;
|
|
441
|
+
process.exitCode = 1;
|
|
442
|
+
}
|
|
443
|
+
} catch (error) {
|
|
444
|
+
report.status = 'failed';
|
|
445
|
+
report.error ||= `final active-instance gate failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
446
|
+
process.exitCode = 1;
|
|
447
|
+
}
|
|
448
|
+
report.finishedAt = new Date().toISOString();
|
|
449
|
+
console.log(`\n${JSON.stringify(report, null, 2)}`);
|
|
450
|
+
}
|
package/package.json
CHANGED
|
@@ -1,27 +1,56 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xapi-to",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.20",
|
|
4
4
|
"description": "Agent-friendly CLI for xapi - discover and call capabilities and APIs",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"xapi": "dist/index.js",
|
|
8
8
|
"xapi-to": "dist/index.js"
|
|
9
9
|
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./dist/index.js",
|
|
12
|
+
"./openai-sandbox": {
|
|
13
|
+
"types": "./dist/openai-sandbox-client.d.ts",
|
|
14
|
+
"import": "./dist/openai-sandbox-client.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
10
17
|
"files": [
|
|
11
18
|
"dist",
|
|
12
|
-
"
|
|
19
|
+
"examples",
|
|
20
|
+
"scripts/openai-sandbox-agent-e2e.ts",
|
|
21
|
+
"scripts/sandbox-playground-e2e.mjs",
|
|
22
|
+
"src/client.ts",
|
|
23
|
+
"src/config.ts",
|
|
24
|
+
"src/openai-sandbox-client.ts",
|
|
25
|
+
"src/sandbox-client.ts",
|
|
26
|
+
"README.md",
|
|
27
|
+
"skills"
|
|
13
28
|
],
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"homepage": "https://xapi.to",
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "git+https://github.com/xapi-labs/xapi-cli.git"
|
|
34
|
+
},
|
|
14
35
|
"scripts": {
|
|
15
|
-
"build": "tsup src/index.ts --format esm --target node18 --clean --out-dir dist --tsconfig tsconfig.build.json",
|
|
36
|
+
"build": "tsup src/index.ts src/openai-sandbox-client.ts --format esm --target node18 --clean --out-dir dist --tsconfig tsconfig.build.json --dts",
|
|
16
37
|
"typecheck": "tsc --noEmit",
|
|
17
38
|
"start": "node dist/index.js",
|
|
18
39
|
"dev": "XAPI_ACTION_HOST=localhost:3003 bun run src/index.ts",
|
|
19
40
|
"prepublishOnly": "npm run build",
|
|
20
|
-
"test": "bun test src/tests"
|
|
41
|
+
"test": "bun test src/tests",
|
|
42
|
+
"demo:sandbox": "npm run build && node examples/sandbox-api-cli-openai.mjs",
|
|
43
|
+
"example:sandbox:openai": "bun run examples/openai-agents-sandbox-local.ts",
|
|
44
|
+
"test:sandbox:playground": "npm run build && node scripts/sandbox-playground-e2e.mjs",
|
|
45
|
+
"test:sandbox:openai": "bun run scripts/openai-sandbox-agent-e2e.ts"
|
|
21
46
|
},
|
|
22
47
|
"engines": {
|
|
23
48
|
"node": ">=18"
|
|
24
49
|
},
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"@openai/agents": "0.15.0",
|
|
52
|
+
"zod": "^4.0.0"
|
|
53
|
+
},
|
|
25
54
|
"devDependencies": {
|
|
26
55
|
"@types/bun": "^1.3.9",
|
|
27
56
|
"@types/node": "^18",
|