xapi-to 0.1.19 → 0.1.21

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,85 @@
1
+ import { SandboxClient, SandboxSessionState, SandboxSession, ExecCommandArgs, SandboxExecResult, SandboxClientCreateArgs, Manifest } from '@openai/agents/sandbox';
2
+
3
+ /**
4
+ * xAPI Sandbox Gateway client.
5
+ *
6
+ * State-changing calls are never retried blindly. A lost POST response may have
7
+ * created, executed, or terminated a real billable instance. Read-only calls and
8
+ * quotes opt into the shared client's conservative transient retry policy.
9
+ */
10
+ interface SandboxClientOptions {
11
+ sandboxHost: string;
12
+ apiKey: string;
13
+ provider?: string;
14
+ }
15
+
16
+ /** OpenAI Sandbox Agents SDK client backed by the xAPI Sandbox Gateway. */
17
+
18
+ type XapiAgentsSandboxOptions = {
19
+ apiKey: string;
20
+ sandboxHost?: string;
21
+ provider?: string;
22
+ maxHourlyUsd?: number;
23
+ model?: string;
24
+ workspaceRoot?: string;
25
+ };
26
+ type XapiAgentsSandboxState = SandboxSessionState & {
27
+ instanceId: string;
28
+ provider: string;
29
+ };
30
+ type XapiAgentsSandboxEvidence = {
31
+ instanceId?: string;
32
+ provider?: string;
33
+ execCount: number;
34
+ shellMarkerSeen: boolean;
35
+ finalState?: string;
36
+ totalCost?: string | number;
37
+ auditCounts?: Record<string, number>;
38
+ auditStatuses?: Record<string, string[]>;
39
+ auditVerified?: boolean;
40
+ };
41
+ type RunOptions = {
42
+ provider?: string;
43
+ maxHourlyUsd?: number;
44
+ };
45
+ declare class XapiAgentsSandboxSession implements SandboxSession<XapiAgentsSandboxState> {
46
+ private readonly options;
47
+ private readonly owner;
48
+ readonly state: XapiAgentsSandboxState;
49
+ private closed;
50
+ private closePromise?;
51
+ constructor(state: XapiAgentsSandboxState, options: SandboxClientOptions, owner: XapiAgentsSandboxClient);
52
+ running(): Promise<boolean>;
53
+ exec(args: ExecCommandArgs): Promise<SandboxExecResult>;
54
+ execCommand(args: ExecCommandArgs): Promise<string>;
55
+ stop(): Promise<void>;
56
+ shutdown(): Promise<void>;
57
+ delete(): Promise<void>;
58
+ close(): Promise<void>;
59
+ }
60
+ /**
61
+ * Minimal provider adapter for Shell-based SandboxAgent examples.
62
+ *
63
+ * It intentionally rejects materialized Manifest entries and environment
64
+ * values. A production adapter should add file/mount/snapshot translations
65
+ * instead of pretending those optional surfaces work.
66
+ */
67
+ declare class XapiAgentsSandboxClient implements SandboxClient<RunOptions, XapiAgentsSandboxState> {
68
+ readonly backendId = "xapi-sandbox";
69
+ readonly supportsDefaultOptions = true;
70
+ readonly evidence: XapiAgentsSandboxEvidence;
71
+ readonly workspaceRoot: string;
72
+ lastSession?: XapiAgentsSandboxSession;
73
+ private readonly apiKey;
74
+ private readonly sandboxHost;
75
+ private readonly provider;
76
+ private readonly maxHourlyUsd;
77
+ private readonly model;
78
+ private readonly terminationKeys;
79
+ constructor(options: XapiAgentsSandboxOptions);
80
+ create(args?: SandboxClientCreateArgs<RunOptions> | Manifest, legacyOptions?: RunOptions): Promise<XapiAgentsSandboxSession>;
81
+ delete(state: XapiAgentsSandboxState): Promise<void>;
82
+ terminate(state: XapiAgentsSandboxState): Promise<void>;
83
+ }
84
+
85
+ export { XapiAgentsSandboxClient, type XapiAgentsSandboxEvidence, type XapiAgentsSandboxOptions, type XapiAgentsSandboxState };
@@ -0,0 +1,285 @@
1
+ import {
2
+ HttpError,
3
+ sandboxAudit,
4
+ sandboxCreate,
5
+ sandboxExec,
6
+ sandboxGet,
7
+ sandboxQuote,
8
+ sandboxStateAction,
9
+ sandboxWait
10
+ } from "./chunk-UEQCIJ7T.js";
11
+
12
+ // src/openai-sandbox-client.ts
13
+ import { randomUUID } from "crypto";
14
+ import {
15
+ normalizeSandboxClientCreateArgs
16
+ } from "@openai/agents/sandbox";
17
+ function countItems(value) {
18
+ if (Array.isArray(value)) return value.length;
19
+ const item = value;
20
+ return item?.items?.length ?? item?.data?.length ?? 0;
21
+ }
22
+ function items(value) {
23
+ if (Array.isArray(value)) return value;
24
+ const page = value;
25
+ return page?.items ?? page?.data ?? [];
26
+ }
27
+ function positivePrice(value, name) {
28
+ if (!Number.isFinite(value) || value <= 0) throw new Error(`${name} must be a positive finite number`);
29
+ return value;
30
+ }
31
+ function shellQuote(value) {
32
+ if (!value || value.length > 4096 || value.includes("\0")) {
33
+ throw new Error("workspaceRoot must be a non-empty path no longer than 4096 characters");
34
+ }
35
+ return `'${value.replaceAll("'", "'\\''")}'`;
36
+ }
37
+ function sleep(ms) {
38
+ return new Promise((resolve) => setTimeout(resolve, ms));
39
+ }
40
+ function defaultWorkspaceRoot(provider) {
41
+ if (provider === "daytona") return "/home/daytona/openai-xapi";
42
+ return "/tmp/openai-xapi";
43
+ }
44
+ var XapiAgentsSandboxSession = class {
45
+ constructor(state, options, owner) {
46
+ this.options = options;
47
+ this.owner = owner;
48
+ this.state = state;
49
+ }
50
+ options;
51
+ owner;
52
+ state;
53
+ closed = false;
54
+ closePromise;
55
+ async running() {
56
+ const detail = await sandboxGet(this.options, this.state.instanceId);
57
+ return detail.observedState === "RUNNING";
58
+ }
59
+ async exec(args) {
60
+ const before = Date.now();
61
+ const result = await sandboxExec(this.options, this.state.instanceId, {
62
+ command: args.cmd,
63
+ ...args.workdir ? { cwd: args.workdir } : {},
64
+ timeoutSeconds: 120
65
+ });
66
+ const stdout = String(result.stdout || "");
67
+ const stderr = String(result.stderr || "");
68
+ this.owner.evidence.execCount += 1;
69
+ if (/(?:OPENAI_XAPI_SANDBOX_OK|SDK_OK)=42/.test(`${stdout}
70
+ ${stderr}`)) {
71
+ this.owner.evidence.shellMarkerSeen = true;
72
+ }
73
+ return {
74
+ output: [stdout, stderr].filter(Boolean).join("\n"),
75
+ stdout,
76
+ stderr,
77
+ exitCode: typeof result.exitCode === "number" ? result.exitCode : null,
78
+ wallTimeSeconds: (Date.now() - before) / 1e3
79
+ };
80
+ }
81
+ async execCommand(args) {
82
+ return (await this.exec(args)).output;
83
+ }
84
+ async stop() {
85
+ await this.close();
86
+ }
87
+ async shutdown() {
88
+ await this.close();
89
+ }
90
+ async delete() {
91
+ await this.close();
92
+ }
93
+ async close() {
94
+ if (this.closed) return;
95
+ if (this.closePromise) return this.closePromise;
96
+ this.closePromise = this.owner.terminate(this.state);
97
+ try {
98
+ await this.closePromise;
99
+ this.closed = true;
100
+ } finally {
101
+ this.closePromise = void 0;
102
+ }
103
+ }
104
+ };
105
+ var XapiAgentsSandboxClient = class {
106
+ backendId = "xapi-sandbox";
107
+ supportsDefaultOptions = true;
108
+ evidence = { execCount: 0, shellMarkerSeen: false };
109
+ workspaceRoot;
110
+ lastSession;
111
+ apiKey;
112
+ sandboxHost;
113
+ provider;
114
+ maxHourlyUsd;
115
+ model;
116
+ terminationKeys = /* @__PURE__ */ new Map();
117
+ constructor(options) {
118
+ if (!options.apiKey) throw new Error("XapiAgentsSandboxClient requires apiKey");
119
+ this.apiKey = options.apiKey;
120
+ this.sandboxHost = options.sandboxHost || "sandbox.xapi.to";
121
+ this.provider = options.provider || "daytona";
122
+ this.maxHourlyUsd = positivePrice(options.maxHourlyUsd ?? 0.2, "maxHourlyUsd");
123
+ this.model = options.model || "deepseek-v4-pro";
124
+ this.workspaceRoot = options.workspaceRoot || defaultWorkspaceRoot(this.provider);
125
+ shellQuote(this.workspaceRoot);
126
+ }
127
+ async create(args = {}, legacyOptions) {
128
+ const normalized = normalizeSandboxClientCreateArgs(args, legacyOptions);
129
+ const provider = normalized.options?.provider || this.provider;
130
+ const maxHourlyUsd = positivePrice(
131
+ normalized.options?.maxHourlyUsd ?? this.maxHourlyUsd,
132
+ "maxHourlyUsd"
133
+ );
134
+ const manifest = normalized.manifest;
135
+ if (Object.keys(manifest.validatedEntries()).length !== 0) {
136
+ throw new Error("XapiAgentsSandboxClient example currently supports an empty Manifest only");
137
+ }
138
+ if (Object.keys(manifest.environment).length !== 0) {
139
+ throw new Error("XapiAgentsSandboxClient keeps credentials/environment out of the Manifest");
140
+ }
141
+ const options = {
142
+ sandboxHost: this.sandboxHost,
143
+ apiKey: this.apiKey,
144
+ provider
145
+ };
146
+ const quote = await sandboxQuote(options, {
147
+ requirements: { capabilities: ["exec", "files"] },
148
+ maxEstimatedHourlyUsd: maxHourlyUsd.toFixed(8)
149
+ });
150
+ if (!quote?.quoteId) throw new Error("xAPI Sandbox quote did not return quoteId");
151
+ const created = await sandboxCreate(options, {
152
+ selection: { quoteId: quote.quoteId },
153
+ metadata: { client: "openai-agents-sdk", modelGateway: "ai.xapi.to", model: this.model },
154
+ idempotencyKey: `openai-agents-sdk:${randomUUID()}`
155
+ });
156
+ if (!created.id) throw new Error("xAPI Sandbox create did not return instance id");
157
+ const state = {
158
+ manifest,
159
+ workspaceReady: false,
160
+ instanceId: created.id,
161
+ provider
162
+ };
163
+ const session = new XapiAgentsSandboxSession(state, options, this);
164
+ this.lastSession = session;
165
+ this.evidence.instanceId = created.id;
166
+ this.evidence.provider = provider;
167
+ try {
168
+ await sandboxWait(options, created.id, ["RUNNING"], 36e4, 2e3);
169
+ const prepared = await session.exec({ cmd: `mkdir -p -- ${shellQuote(manifest.root)}` });
170
+ if (prepared.exitCode !== 0) throw new Error(`could not prepare ${manifest.root}`);
171
+ state.workspaceReady = true;
172
+ return session;
173
+ } catch (error) {
174
+ await this.terminate(state).catch(() => void 0);
175
+ throw error;
176
+ }
177
+ }
178
+ async delete(state) {
179
+ await this.terminate(state);
180
+ }
181
+ async terminate(state) {
182
+ const options = {
183
+ sandboxHost: this.sandboxHost,
184
+ apiKey: this.apiKey,
185
+ provider: state.provider
186
+ };
187
+ const aggregateOptions = { ...options, provider: void 0 };
188
+ const readDetail = async () => {
189
+ try {
190
+ return await sandboxGet(options, state.instanceId);
191
+ } catch (error) {
192
+ if (!(error instanceof HttpError) || error.status !== 404 || !options.provider) throw error;
193
+ return sandboxGet(aggregateOptions, state.instanceId);
194
+ }
195
+ };
196
+ const deadline = Date.now() + 36e4;
197
+ const intervalMs = 2e3;
198
+ const terminationKey = this.terminationKeys.get(state.instanceId) || `openai-agents-sdk:terminate:${randomUUID()}`;
199
+ this.terminationKeys.set(state.instanceId, terminationKey);
200
+ let detail = await readDetail();
201
+ while (!["TERMINATED", "FAILED"].includes(String(detail.observedState)) && Date.now() < deadline) {
202
+ try {
203
+ await sandboxStateAction(options, state.instanceId, "terminate", {
204
+ idempotencyKey: terminationKey
205
+ });
206
+ break;
207
+ } catch (error) {
208
+ if (!(error instanceof HttpError) || error.status !== 409) throw error;
209
+ await sleep(intervalMs);
210
+ detail = await readDetail();
211
+ }
212
+ }
213
+ if (!["TERMINATED", "FAILED"].includes(String(detail.observedState))) {
214
+ try {
215
+ detail = await sandboxWait(
216
+ options,
217
+ state.instanceId,
218
+ ["TERMINATED", "FAILED"],
219
+ Math.max(1, deadline - Date.now()),
220
+ intervalMs
221
+ );
222
+ } catch (error) {
223
+ if (!(error instanceof HttpError) || error.status !== 404 || !options.provider) throw error;
224
+ detail = await sandboxWait(
225
+ aggregateOptions,
226
+ state.instanceId,
227
+ ["TERMINATED", "FAILED"],
228
+ Math.max(1, deadline - Date.now()),
229
+ intervalMs
230
+ );
231
+ }
232
+ }
233
+ const auditDeadline = Date.now() + 6e4;
234
+ let audit = {};
235
+ let auditError = "audit has not settled";
236
+ while (Date.now() < auditDeadline) {
237
+ detail = await sandboxGet(aggregateOptions, state.instanceId);
238
+ audit = {};
239
+ for (const kind of ["operations", "events", "usageSegments", "billingPeriods"]) {
240
+ audit[kind] = await sandboxAudit(aggregateOptions, state.instanceId, kind);
241
+ }
242
+ const operations = items(audit.operations);
243
+ const events = items(audit.events);
244
+ const usageSegments = items(audit.usageSegments);
245
+ const billingPeriods = items(audit.billingPeriods);
246
+ const operationStatuses = operations.map((item) => String(item.status || "UNKNOWN"));
247
+ const operationSettled = operations.length > 0 && operationStatuses.every((status) => ["SUCCEEDED", "FAILED"].includes(status));
248
+ const eventSettled = events.some((item) => ["TERMINATED", "FAILED"].includes(String(item.currentState)));
249
+ const usageSettled = usageSegments.length > 0 && usageSegments.every((item) => item.status === "SETTLED" && item.endsAt);
250
+ const billingSettled = billingPeriods.length > 0 && billingPeriods.every((item) => item.status === "SETTLED" && item.endedAt);
251
+ const billed = billingPeriods.reduce((sum, item) => sum + Number(item.amount || 0), 0);
252
+ const totalCost = Number(detail.totalCost);
253
+ const costMatches = Number.isFinite(totalCost) && Number.isFinite(billed) && Math.abs(totalCost - billed) <= 1e-9;
254
+ if (operationSettled && eventSettled && usageSettled && billingSettled && costMatches) {
255
+ auditError = "";
256
+ break;
257
+ }
258
+ auditError = [
259
+ !operationSettled && "operations are not terminal",
260
+ !eventSettled && "terminal event is missing",
261
+ !usageSettled && "usage is not settled",
262
+ !billingSettled && "billing is not settled",
263
+ !costMatches && `billing sum ${billed} does not match totalCost ${detail.totalCost}`
264
+ ].filter(Boolean).join("; ");
265
+ await sleep(1e3);
266
+ }
267
+ if (auditError) throw new Error(`xAPI Sandbox audit verification failed: ${auditError}`);
268
+ const auditCounts = {};
269
+ const auditStatuses = {};
270
+ for (const kind of ["operations", "events", "usageSegments", "billingPeriods"]) {
271
+ auditCounts[kind] = countItems(audit[kind]);
272
+ auditStatuses[kind] = items(audit[kind]).map((item) => String(
273
+ item.status || item.currentState || "UNKNOWN"
274
+ ));
275
+ }
276
+ this.evidence.finalState = detail.observedState;
277
+ this.evidence.totalCost = detail.totalCost;
278
+ this.evidence.auditCounts = auditCounts;
279
+ this.evidence.auditStatuses = auditStatuses;
280
+ this.evidence.auditVerified = true;
281
+ }
282
+ };
283
+ export {
284
+ XapiAgentsSandboxClient
285
+ };
@@ -0,0 +1,131 @@
1
+ #!/usr/bin/env bun
2
+
3
+ /**
4
+ * Local OpenAI SandboxAgent + xAPI example.
5
+ *
6
+ * This file imports the xAPI adapter from ../src, so it works before the next
7
+ * xapi-to package release. The model call uses DeepSeek through ai.xapi.to;
8
+ * shell execution uses the xAPI Sandbox test gateway by default.
9
+ */
10
+
11
+ import { OpenAIProvider, Runner } from '@openai/agents';
12
+ import { Manifest, SandboxAgent, shell } from '@openai/agents/sandbox';
13
+ import { getConfig } from '../src/config.ts';
14
+ import { XapiAgentsSandboxClient } from '../src/openai-sandbox-client.ts';
15
+
16
+ const configuredApiKey = process.env.XAPI_KEY || process.env.XAPI_API_KEY || getConfig().apiKey;
17
+ const sandboxApiKey =
18
+ process.env.XAPI_SANDBOX_KEY || process.env.XAPI_TEST_API_KEY || configuredApiKey;
19
+ const aiApiKey = process.env.XAPI_AI_KEY || configuredApiKey;
20
+
21
+ if (!sandboxApiKey) {
22
+ throw new Error(
23
+ 'Sandbox key is required. Set XAPI_SANDBOX_KEY (or XAPI_TEST_API_KEY/XAPI_KEY).',
24
+ );
25
+ }
26
+ if (!aiApiKey) {
27
+ throw new Error(
28
+ 'AI Gateway key is required. Set XAPI_AI_KEY (or configure a production xAPI key).',
29
+ );
30
+ }
31
+
32
+ const sandboxHost = process.env.XAPI_SANDBOX_HOST || 'sandbox.test.xapi.to';
33
+ const provider = process.env.XAPI_SANDBOX_PROVIDER || 'daytona';
34
+ const model = process.env.XAPI_MODEL || 'deepseek-v4-pro';
35
+ const maxHourlyUsd = Number(process.env.XAPI_SANDBOX_MAX_HOURLY_USD || '0.20');
36
+
37
+ if (!Number.isFinite(maxHourlyUsd) || maxHourlyUsd <= 0) {
38
+ throw new Error('XAPI_SANDBOX_MAX_HOURLY_USD must be a positive number');
39
+ }
40
+
41
+ // Compute plane: xAPI quotes, creates, executes, audits, bills, and terminates.
42
+ const sandbox = new XapiAgentsSandboxClient({
43
+ apiKey: sandboxApiKey,
44
+ sandboxHost,
45
+ provider,
46
+ maxHourlyUsd,
47
+ model,
48
+ });
49
+
50
+ // Model plane: DeepSeek through xAPI's OpenAI Chat Completions-compatible API.
51
+ const modelProvider = new OpenAIProvider({
52
+ apiKey: aiApiKey,
53
+ baseURL: 'https://ai.xapi.to/v1',
54
+ useResponses: false,
55
+ strictFeatureValidation: true,
56
+ });
57
+
58
+ // The xAPI key is not an OpenAI telemetry credential.
59
+ const runner = new Runner({ modelProvider, tracingDisabled: true });
60
+
61
+ const agent = new SandboxAgent({
62
+ name: 'xAPI DeepSeek local sandbox agent',
63
+ model,
64
+ defaultManifest: new Manifest({ root: sandbox.workspaceRoot }),
65
+ capabilities: [shell()],
66
+ instructions: [
67
+ 'Work only inside the sandbox workspace.',
68
+ 'Use shell to complete the task.',
69
+ 'Use two separate shell calls: first write the artifact, then read it back.',
70
+ 'Verify the shell output before reporting success.',
71
+ ].join(' '),
72
+ });
73
+
74
+ let finalOutput = '';
75
+ let failure: unknown;
76
+ let cleanupError: string | undefined;
77
+ const startedAt = new Date();
78
+
79
+ try {
80
+ const result = await runner.run(
81
+ agent,
82
+ 'Write exactly SDK_OK=42 to result.txt. Then read result.txt and verify it. ' +
83
+ 'Only after the shell output confirms the marker, reply exactly SDK_OK=42.',
84
+ {
85
+ maxTurns: 8,
86
+ sandbox: { client: sandbox },
87
+ },
88
+ );
89
+
90
+ finalOutput = String(result.finalOutput || '');
91
+ if (!finalOutput.includes('SDK_OK=42')) {
92
+ throw new Error(`agent final output did not contain SDK_OK=42: ${finalOutput}`);
93
+ }
94
+ if (sandbox.evidence.execCount < 2) {
95
+ throw new Error(`expected at least 2 shell calls, got ${sandbox.evidence.execCount}`);
96
+ }
97
+ } catch (error) {
98
+ failure = error;
99
+ } finally {
100
+ try {
101
+ // close() terminates the instance, waits for a terminal state, then reads
102
+ // operations, events, usage segments, billing periods, and final cost.
103
+ await sandbox.lastSession?.close();
104
+ } catch (error) {
105
+ cleanupError = error instanceof Error ? error.message : String(error);
106
+ failure ||= error;
107
+ }
108
+ }
109
+
110
+ const report = {
111
+ status: failure ? 'failed' : 'passed',
112
+ startedAt: startedAt.toISOString(),
113
+ finishedAt: new Date().toISOString(),
114
+ modelGateway: 'https://ai.xapi.to/v1',
115
+ model,
116
+ sandboxHost,
117
+ provider,
118
+ maxHourlyUsd,
119
+ finalOutput,
120
+ sandbox: sandbox.evidence,
121
+ ...(cleanupError ? { cleanupError } : {}),
122
+ ...(failure
123
+ ? { error: failure instanceof Error ? failure.message : String(failure) }
124
+ : {}),
125
+ };
126
+
127
+ console.log(JSON.stringify(report, null, 2));
128
+
129
+ if (failure) {
130
+ throw failure;
131
+ }