xapi-to 0.1.20 → 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.
- package/README.md +41 -0
- package/dist/{chunk-TYY6JR6O.js → chunk-UEQCIJ7T.js} +73 -21
- package/dist/index.js +787 -39
- package/dist/openai-sandbox-client.js +1 -1
- package/package.json +1 -1
- package/skills/xapi/SKILL.md +1 -2
- package/skills/xapi/guides/provider.md +198 -0
- package/skills/xapi/guides/sandbox.md +100 -46
- package/src/client.ts +56 -5
- package/src/sandbox-client.ts +36 -16
package/package.json
CHANGED
package/skills/xapi/SKILL.md
CHANGED
|
@@ -466,8 +466,6 @@ The full catalog also spans many other categories — crypto/on-chain data, CEX
|
|
|
466
466
|
|
|
467
467
|
The CLI retries idempotent metadata reads and `task poll` for transient timeouts, network failures, `408`, `429`, and `502`–`504`. It does not automatically retry arbitrary `call` actions because the upstream may already have completed a write; confirm the result before manually retrying posts, payments, or other mutations. Ordinary JSON execution has a 60-second request ceiling. HTTP SSE streams and raw downloads instead use a 60-second no-data timeout, reset whenever a chunk arrives; override it with `XAPI_TRANSFER_IDLE_TIMEOUT_MS` when an upstream legitimately pauses longer.
|
|
468
468
|
|
|
469
|
-
## Tips
|
|
470
|
-
|
|
471
469
|
- Use `--page` and `--page-size` for pagination on `list`, `search`, and `services`.
|
|
472
470
|
|
|
473
471
|
## Specialized Guides
|
|
@@ -489,6 +487,7 @@ When the user's task involves these workflows, read the corresponding guide file
|
|
|
489
487
|
- **`guides/ws_gateway.md`** — xAPI WebSocket Gateway: OpenAI Realtime, streaming ASR/TTS, simultaneous interpretation, podcast generation, service/path routing, browser authentication, native binary protocols, limits, billing, close codes, and reconnects
|
|
490
488
|
- **`guides/sandbox.md`** — managed Sandbox compute: AI tool selection, one-shot and multi-step lifecycles, provider pinning, files, Cloudflare Web previews, suspension, GPU jobs, parallel agents, cleanup recovery, audit/history, and billing verification
|
|
491
489
|
- **`guides/sms.md`** — SMS verification: buy virtual phone numbers, receive verification codes, finish/cancel orders (5SIM)
|
|
490
|
+
- **`guides/provider.md`** — Provider management: create/update services, About/changelog, version lifecycle, metrics/events and request receipts, Skill upload/linking, rollback/delete, earnings transfer
|
|
492
491
|
|
|
493
492
|
## Security
|
|
494
493
|
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# Provider service management
|
|
2
|
+
|
|
3
|
+
Read this guide for provider-side service authoring and operations. These
|
|
4
|
+
commands use `XAPI-KEY` directly and never exchange it for a broad JWT session.
|
|
5
|
+
The key must belong to the service owner and carry the scope named by the
|
|
6
|
+
operation.
|
|
7
|
+
|
|
8
|
+
## Inspect capabilities and scopes
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npx xapi-to provider --help
|
|
12
|
+
npx xapi-to skill --help
|
|
13
|
+
npx xapi-to skill spec
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Use narrowly scoped keys. Common scopes are:
|
|
17
|
+
|
|
18
|
+
- `service:create`, `service:read`, `service:update`
|
|
19
|
+
- `version:create`, `service:publish`, `service:rollback`
|
|
20
|
+
- `observability:read`
|
|
21
|
+
- `skill:read`, `skill:submit`
|
|
22
|
+
- `earnings:read`; `earnings:transfer` only when reinvestment is intended
|
|
23
|
+
- `service:delete` only for deliberate removal workflows
|
|
24
|
+
|
|
25
|
+
Scope permission and ownership are independent. A key with a scope still cannot
|
|
26
|
+
manage another provider's service or Skill.
|
|
27
|
+
|
|
28
|
+
## Create and describe a service
|
|
29
|
+
|
|
30
|
+
Create uses the backend service DTO as JSON so credentials and endpoint
|
|
31
|
+
contracts do not have to appear in shell history:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npx xapi-to provider create --file ./service.json
|
|
35
|
+
npx xapi-to provider list
|
|
36
|
+
npx xapi-to provider get <service-id>
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Keep the three service content layers distinct:
|
|
40
|
+
|
|
41
|
+
- `description` is the short marketplace-card summary.
|
|
42
|
+
- `aboutMarkdown` is the long About tab.
|
|
43
|
+
- `website` is a public HTTP(S) link.
|
|
44
|
+
|
|
45
|
+
Prefer files for long text:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npx xapi-to provider update <service-id> \
|
|
49
|
+
--description "Short marketplace summary" \
|
|
50
|
+
--about-file ./ABOUT.md \
|
|
51
|
+
--website https://example.com
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Use `--clear-about` or `--clear-website` to clear a value. Provider metadata
|
|
55
|
+
updates cannot modify the version contract or upstream credentials; use the
|
|
56
|
+
version command for those fields.
|
|
57
|
+
|
|
58
|
+
## Edit and publish a revision
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
# Inspect current majors and revisions
|
|
62
|
+
npx xapi-to provider versions <service-id>
|
|
63
|
+
|
|
64
|
+
# Either create a new major or pull a working revision from an existing major
|
|
65
|
+
npx xapi-to provider major create <service-id>
|
|
66
|
+
npx xapi-to provider revision start <service-id> <major>
|
|
67
|
+
|
|
68
|
+
# Merge a partial version-contract update; add --replace for full replacement
|
|
69
|
+
npx xapi-to provider version update \
|
|
70
|
+
<service-id> <version-id> --file ./contract.json
|
|
71
|
+
|
|
72
|
+
# Inspect before publishing
|
|
73
|
+
npx xapi-to provider diff <service-id> <major>
|
|
74
|
+
|
|
75
|
+
# Submit through the normal review gate with public release notes
|
|
76
|
+
npx xapi-to provider publish \
|
|
77
|
+
<service-id> <revision-id> --changelog-file ./CHANGELOG.md
|
|
78
|
+
|
|
79
|
+
# Inspect the review result
|
|
80
|
+
npx xapi-to provider review <service-id> <revision-id>
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
The changelog is provider-authored, public release information associated with
|
|
84
|
+
that revision. It is not a system deployment log. Build, review, and runtime
|
|
85
|
+
events are generated by the platform and should only be read, never uploaded as
|
|
86
|
+
if they were evidence.
|
|
87
|
+
|
|
88
|
+
`publish` can change live service behavior after review. The CLI does not
|
|
89
|
+
automatically retry this write after an ambiguous transport failure. Read the
|
|
90
|
+
version overview and review state before deciding whether to submit again.
|
|
91
|
+
|
|
92
|
+
## Create, upload, and link the usage Skill
|
|
93
|
+
|
|
94
|
+
Generate a service-specific starting point from the currently serving endpoints:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
npx xapi-to provider skill scaffold \
|
|
98
|
+
<service-id> --output ./my-service/SKILL.md
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
The scaffold command refuses to overwrite an existing file unless `--force` is
|
|
102
|
+
explicitly supplied. Complete the instructions and metadata, then submit either
|
|
103
|
+
a local directory or a public GitHub tree:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
npx xapi-to skill submit --dir ./my-service
|
|
107
|
+
|
|
108
|
+
npx xapi-to skill submit \
|
|
109
|
+
--github https://github.com/org/repo/tree/main/skills/my-service \
|
|
110
|
+
--version 1.0.0
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Local submission skips symlinks, `.git`, and `node_modules`; requires a root
|
|
114
|
+
`SKILL.md`; permits at most 100 files; limits each file to 512 KiB and the encoded
|
|
115
|
+
package to 2 MiB. The server still performs manifest validation and secret
|
|
116
|
+
scanning. A successful upload creates or updates the owned Skill version and
|
|
117
|
+
submits it for review; it is not immediately public.
|
|
118
|
+
|
|
119
|
+
Use the returned submission ID:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
npx xapi-to skill status <submission-id>
|
|
123
|
+
npx xapi-to skill wait <submission-id> --timeout 10m
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
After the Skill is published, bind it as the service's primary tutorial and
|
|
127
|
+
record the serving-contract fingerprint:
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
npx xapi-to provider skill link <service-id> <skill-id>
|
|
131
|
+
npx xapi-to provider skill fingerprint \
|
|
132
|
+
<service-id> --skill-version-id <skill-version-id>
|
|
133
|
+
npx xapi-to provider skill context <service-id>
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Only a Skill owned by the same provider can be linked, and one Skill can be the
|
|
137
|
+
primary Skill of only one service. The backend permits linking a pending Skill,
|
|
138
|
+
but the public marketplace exposes only a published version; wait for publication
|
|
139
|
+
unless intentionally preparing the association early. Use `provider skill unlink`
|
|
140
|
+
to remove the primary association. The context response reports drift when host,
|
|
141
|
+
major version, or serving endpoints no longer match the stored fingerprint; update
|
|
142
|
+
and resubmit the Skill rather than merely overwriting the fingerprint.
|
|
143
|
+
|
|
144
|
+
## Observe and recover
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
npx xapi-to provider metrics --days 30
|
|
148
|
+
npx xapi-to provider metrics <service-id> --days 7
|
|
149
|
+
npx xapi-to provider events --limit 50
|
|
150
|
+
npx xapi-to provider events --after '<opaque-next-cursor>' --limit 50
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Pass event cursors back unchanged. Metrics and events are owner-scoped; usage
|
|
154
|
+
events are also restricted to the current key where applicable.
|
|
155
|
+
|
|
156
|
+
Verify the finalized cost of a canary or provider call with its receipt ID. Use
|
|
157
|
+
the `X-XAPI-Request-Id` response header or the final `xapi.usage` SSE event, and
|
|
158
|
+
wait when asynchronous billing has not finalized yet:
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
npx xapi-to usage <request-id>
|
|
162
|
+
npx xapi-to usage wait <request-id> --timeout 1m
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Receipt reads are idempotent. The wait command applies one total deadline,
|
|
166
|
+
retries not-found and transient transport failures within that deadline, and
|
|
167
|
+
fails immediately for permanent authorization or validation errors.
|
|
168
|
+
|
|
169
|
+
Rollback and default-major changes affect live routing:
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
npx xapi-to provider rollback \
|
|
173
|
+
<service-id> <major> --revision <published-revision-id> \
|
|
174
|
+
--reason "Restore the last known-good contract"
|
|
175
|
+
npx xapi-to provider default-major <service-id> <major>
|
|
176
|
+
npx xapi-to provider deprecate <service-id> <major>
|
|
177
|
+
npx xapi-to provider restore <service-id> <major>
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Inspect the target revision before rollback. Do not automatically retry an
|
|
181
|
+
ambiguous rollback response. Deletion requires both `service:delete` and an
|
|
182
|
+
explicit service name or ID confirmation:
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
npx xapi-to provider delete <service-id> --confirm <service-name-or-id>
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Earnings reinvestment
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
npx xapi-to earnings
|
|
192
|
+
npx xapi-to earnings list --status SETTLED
|
|
193
|
+
npx xapi-to earnings transfer 1 --idempotency-key <stable-operation-key>
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Transfer is one-way: it converts settled provider earnings into spendable xAPI
|
|
197
|
+
balance. Confirm the amount and available settled balance first. A transfer may
|
|
198
|
+
be retried only with the same idempotency key and the same amount.
|
|
@@ -7,32 +7,45 @@ not an ordinary per-call action, so cleanup and audit are part of task success.
|
|
|
7
7
|
|
|
8
8
|
## Contents
|
|
9
9
|
|
|
10
|
+
- [Product boundary](#product-boundary)
|
|
10
11
|
- [Choose the shortest safe lifecycle](#choose-the-shortest-safe-lifecycle)
|
|
11
12
|
- [Authentication and gateway selection](#authentication-and-gateway-selection)
|
|
12
13
|
- [Inspect offerings and quote first](#inspect-offerings-and-quote-first)
|
|
13
14
|
- [One-shot execution](#one-shot-execution)
|
|
14
|
-
- [Multi-step
|
|
15
|
+
- [Multi-step client lifecycle](#multi-step-client-lifecycle)
|
|
15
16
|
- [Files and artifacts](#files-and-artifacts)
|
|
16
17
|
- [Web preview and background processes](#web-preview-and-background-processes)
|
|
17
18
|
- [Suspend and resume](#suspend-and-resume)
|
|
18
19
|
- [GPU jobs](#gpu-jobs)
|
|
19
|
-
- [Parallel
|
|
20
|
-
- [OpenAI SandboxAgent
|
|
20
|
+
- [Parallel isolated instances](#parallel-isolated-instances)
|
|
21
|
+
- [OpenAI SandboxAgent integration example](#openai-sandboxagent-integration-example)
|
|
21
22
|
- [Audit, history, and billing](#audit-history-and-billing)
|
|
22
|
-
- [Run the real Playground acceptance suite](#run-the-real-playground-acceptance-suite)
|
|
23
|
+
- [Run the real Playground recipe acceptance suite](#run-the-real-playground-recipe-acceptance-suite)
|
|
23
24
|
- [Failure and interruption recovery](#failure-and-interruption-recovery)
|
|
24
25
|
- [AI operating rules](#ai-operating-rules)
|
|
25
26
|
|
|
27
|
+
## Product boundary
|
|
28
|
+
|
|
29
|
+
xAPI is the Sandbox resource and capability provider. The CLI is a thin client
|
|
30
|
+
for discovery, quote, lifecycle, exec, files, ports, provider extensions,
|
|
31
|
+
history, audit, and billing. It does not implement prompts, model loops, memory,
|
|
32
|
+
multi-agent orchestration, job DAGs, queues, or human approval workflows.
|
|
33
|
+
|
|
34
|
+
Agent, CI, browser, and data examples in this guide are client-side recipes that
|
|
35
|
+
compose Sandbox primitives. They are not additional Gateway workflow APIs.
|
|
36
|
+
Provider-native features remain valid Sandbox extensions when the live Offering
|
|
37
|
+
declares their schemas, limits, state effects, and billing behavior.
|
|
38
|
+
|
|
26
39
|
## Choose the shortest safe lifecycle
|
|
27
40
|
|
|
28
|
-
| Need
|
|
29
|
-
|
|
30
|
-
| Run one command and get stdout | `sandbox run`
|
|
31
|
-
| Several exec/file calls
|
|
32
|
-
| Inspect price/capabilities
|
|
33
|
-
| Publish a temporary port
|
|
34
|
-
| Pause a reusable workspace
|
|
35
|
-
| Inspect prior work/cost
|
|
41
|
+
| Need | Preferred command | Cleanup behavior |
|
|
42
|
+
| ------------------------------ | ------------------------------ | ------------------------ |
|
|
43
|
+
| Run one command and get stdout | `sandbox run` | Terminates automatically |
|
|
44
|
+
| Several exec/file calls | `create` + primitives | Agent must terminate |
|
|
45
|
+
| Inspect price/capabilities | `offerings`, `quote` | No instance created |
|
|
46
|
+
| Publish a temporary port | `port` after starting a server | Terminate afterward |
|
|
47
|
+
| Pause a reusable workspace | `suspend` | Storage may keep billing |
|
|
48
|
+
| Inspect prior work/cost | `history`, `get`, `audit` | Read-only |
|
|
36
49
|
|
|
37
50
|
Prefer `sandbox run` whenever the task fits one remote shell command. A shorter
|
|
38
51
|
lifecycle reduces orphan risk and returns one machine-readable JSON result.
|
|
@@ -79,6 +92,7 @@ npx xapi-to sandbox quote \
|
|
|
79
92
|
--capabilities exec,files \
|
|
80
93
|
--cpu 2 \
|
|
81
94
|
--memory 4 \
|
|
95
|
+
--min-runtime 24h \
|
|
82
96
|
--max-hourly-usd 0.20 \
|
|
83
97
|
--format pretty
|
|
84
98
|
```
|
|
@@ -121,9 +135,9 @@ shells and AI runners can detect failure without parsing stdout.
|
|
|
121
135
|
`--keep` suppresses automatic termination. Use it only after the user explicitly
|
|
122
136
|
asks to retain the instance and understands that billing continues.
|
|
123
137
|
|
|
124
|
-
## Multi-step
|
|
138
|
+
## Multi-step client lifecycle
|
|
125
139
|
|
|
126
|
-
Use granular commands when
|
|
140
|
+
Use granular commands when a client must alternate between files and commands.
|
|
127
141
|
Capture the instance ID without logging credentials:
|
|
128
142
|
|
|
129
143
|
```bash
|
|
@@ -205,15 +219,17 @@ port response contains `headers`, include them in external requests; they can
|
|
|
205
219
|
carry a provider preview token. Do not emulate this mode with `nohup ... &` on
|
|
206
220
|
an Offering that does not declare `backgroundExec`.
|
|
207
221
|
|
|
208
|
-
Cloudflare
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
222
|
+
Cloudflare declares the standard background command surface and maps it to its
|
|
223
|
+
native managed process API. Pin `cf-edge` only when the user explicitly wants
|
|
224
|
+
Cloudflare. For a one-day workspace, require `--min-runtime 24h` and explicitly
|
|
225
|
+
enable `cloudflare.set_keep_alive`; the runtime requirement filters offerings,
|
|
226
|
+
while keepAlive prevents the default ten-minute idle reset:
|
|
212
227
|
|
|
213
228
|
```bash
|
|
214
229
|
box_json="$(npx xapi-to sandbox create \
|
|
215
230
|
--provider cf-edge \
|
|
216
|
-
--capabilities exec,files,ports \
|
|
231
|
+
--capabilities exec,backgroundExec,files,ports,lifecycle.keep_alive \
|
|
232
|
+
--min-runtime 24h \
|
|
217
233
|
--wait)"
|
|
218
234
|
box_id="$(printf '%s' "$box_json" | jq -r '.id')"
|
|
219
235
|
port=8080
|
|
@@ -225,11 +241,11 @@ npx xapi-to sandbox file write "$box_id" index.html \
|
|
|
225
241
|
--provider cf-edge \
|
|
226
242
|
--content '<!doctype html><h1>xAPI preview</h1>'
|
|
227
243
|
|
|
228
|
-
npx xapi-to sandbox
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
244
|
+
npx xapi-to sandbox extension "$box_id" cloudflare.set_keep_alive \
|
|
245
|
+
--provider cf-edge --input '{"keepAlive":true}'
|
|
246
|
+
|
|
247
|
+
npx xapi-to sandbox exec "$box_id" --provider cf-edge --background --command \
|
|
248
|
+
"python3 -m http.server $port --directory /workspace"
|
|
233
249
|
|
|
234
250
|
npx xapi-to sandbox port "$box_id" "$port" --provider cf-edge
|
|
235
251
|
```
|
|
@@ -242,6 +258,37 @@ terminate instead of leaving the instance billing. The URL stops working after
|
|
|
242
258
|
termination. Quick Tunnels are for previews; use a stable, supported named
|
|
243
259
|
tunnel or application deployment for production traffic.
|
|
244
260
|
|
|
261
|
+
Cloudflare extensions also expose managed process logs/readiness, persistent
|
|
262
|
+
shell sessions, stateful Python/JavaScript/TypeScript code contexts, Git
|
|
263
|
+
checkout, file-change cursors, bucket mounts, and R2 backup/restore. Inspect
|
|
264
|
+
`capabilities.extensionIds` before calling them. A default idle reset starts a
|
|
265
|
+
fresh container and does not retain files, processes, sessions, or interpreter
|
|
266
|
+
state. `keepAlive` removes that idle cutoff but does not guarantee that platform
|
|
267
|
+
maintenance can never restart the host. Use R2 backup or external storage for
|
|
268
|
+
state that must survive restarts, disable keepAlive in `finally`, and terminate.
|
|
269
|
+
|
|
270
|
+
If the Offering declares `cloudflare.browser.*`, Cloudflare Browser Run can be
|
|
271
|
+
used through the same generic extension command. Prefer a Quick Action for
|
|
272
|
+
read-only page understanding before paying for a multi-step browser session:
|
|
273
|
+
|
|
274
|
+
```bash
|
|
275
|
+
npx xapi-to sandbox extension "$box_id" cloudflare.browser.snapshot \
|
|
276
|
+
--provider cf-edge \
|
|
277
|
+
--input '{"url":"https://example.com/","formats":["screenshot","markdown","accessibilityTree"]}'
|
|
278
|
+
|
|
279
|
+
npx xapi-to sandbox extension "$box_id" cloudflare.browser.automate \
|
|
280
|
+
--provider cf-edge \
|
|
281
|
+
--input '{"url":"https://demo.playwright.dev/todomvc/","actions":[{"type":"fill","selector":".new-todo","value":"xAPI task"},{"type":"press","selector":".new-todo","key":"Enter"}],"extract":[{"name":"todos","selector":".todo-list li label","all":true}],"screenshot":{"type":"png","fullPage":true}}'
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
Browser Run and the Sandbox container do not share a filesystem. Write the
|
|
285
|
+
returned page data into the sandbox through `sandbox file write` if a later
|
|
286
|
+
container command must analyze it. Browser operations have separate operation
|
|
287
|
+
prices; inspect the quote instead of assuming the container hourly cap includes
|
|
288
|
+
them. The current automation extension is one-shot and closes the browser.
|
|
289
|
+
Persistent CDP/Live View/HITL are not supported until xAPI owns and authorizes
|
|
290
|
+
the session and proxies its WebSocket without exposing Cloudflare credentials.
|
|
291
|
+
|
|
245
292
|
## Suspend and resume
|
|
246
293
|
|
|
247
294
|
Check offering lifecycle fields first because not every provider supports an
|
|
@@ -290,58 +337,65 @@ npx xapi-to sandbox terminate <id> --provider runpod
|
|
|
290
337
|
GPU work is usually more expensive. Quote first, set a deliberate ceiling, use
|
|
291
338
|
a command timeout, and terminate immediately after artifacts are retrieved.
|
|
292
339
|
|
|
293
|
-
## Parallel
|
|
340
|
+
## Parallel isolated instances
|
|
294
341
|
|
|
295
|
-
Give each agent a separate instance. Do not share a mutable
|
|
296
|
-
goal is isolation. Use unique idempotency keys and record every instance ID.
|
|
342
|
+
Give each concurrent worker or agent a separate instance. Do not share a mutable
|
|
343
|
+
workspace when the goal is isolation. Use unique idempotency keys and record every instance ID.
|
|
297
344
|
Run cleanup for all IDs even if one agent fails; then verify `sandbox list` has
|
|
298
345
|
no active instance from the job.
|
|
299
346
|
|
|
300
347
|
Limit concurrency based on budget. Parallel creation multiplies reservation and
|
|
301
348
|
running cost, even when the individual hourly quote is small.
|
|
302
349
|
|
|
303
|
-
## OpenAI SandboxAgent
|
|
350
|
+
## OpenAI SandboxAgent integration example
|
|
304
351
|
|
|
305
352
|
The OpenAI Agents SDK keeps the model provider and sandbox provider separate.
|
|
306
353
|
Use the SDK's OpenAI-compatible model provider for DeepSeek through
|
|
307
354
|
`https://ai.xapi.to/v1`, and the xAPI adapter for Sandbox compute:
|
|
308
355
|
|
|
356
|
+
The Agents SDK owns the model loop, tool choice, prompt, and conversation state.
|
|
357
|
+
xAPI owns only the Sandbox resource, execution, lifecycle, audit, and billing.
|
|
358
|
+
|
|
309
359
|
```ts
|
|
310
|
-
import { OpenAIProvider, Runner } from
|
|
311
|
-
import { Manifest, SandboxAgent, shell } from
|
|
312
|
-
import { XapiAgentsSandboxClient } from
|
|
360
|
+
import { OpenAIProvider, Runner } from "@openai/agents";
|
|
361
|
+
import { Manifest, SandboxAgent, shell } from "@openai/agents/sandbox";
|
|
362
|
+
import { XapiAgentsSandboxClient } from "xapi-to/openai-sandbox";
|
|
313
363
|
|
|
314
364
|
const sandboxApiKey = process.env.XAPI_SANDBOX_KEY;
|
|
315
365
|
const aiApiKey = process.env.XAPI_AI_KEY;
|
|
316
|
-
if (!sandboxApiKey) throw new Error(
|
|
317
|
-
if (!aiApiKey) throw new Error(
|
|
366
|
+
if (!sandboxApiKey) throw new Error("XAPI_SANDBOX_KEY is required");
|
|
367
|
+
if (!aiApiKey) throw new Error("XAPI_AI_KEY is required");
|
|
318
368
|
|
|
319
369
|
const sandbox = new XapiAgentsSandboxClient({
|
|
320
370
|
apiKey: sandboxApiKey,
|
|
321
|
-
sandboxHost:
|
|
322
|
-
provider:
|
|
323
|
-
model:
|
|
371
|
+
sandboxHost: "sandbox.test.xapi.to",
|
|
372
|
+
provider: "daytona",
|
|
373
|
+
model: "deepseek-v4-pro",
|
|
324
374
|
});
|
|
325
375
|
const modelProvider = new OpenAIProvider({
|
|
326
376
|
apiKey: aiApiKey,
|
|
327
|
-
baseURL:
|
|
377
|
+
baseURL: "https://ai.xapi.to/v1",
|
|
328
378
|
useResponses: false,
|
|
329
379
|
strictFeatureValidation: true,
|
|
330
380
|
});
|
|
331
381
|
const runner = new Runner({ modelProvider, tracingDisabled: true });
|
|
332
382
|
const agent = new SandboxAgent({
|
|
333
|
-
name:
|
|
334
|
-
model:
|
|
383
|
+
name: "xAPI DeepSeek sandbox agent",
|
|
384
|
+
model: "deepseek-v4-pro",
|
|
335
385
|
defaultManifest: new Manifest({ root: sandbox.workspaceRoot }),
|
|
336
386
|
capabilities: [shell()],
|
|
337
|
-
instructions:
|
|
387
|
+
instructions: "Use shell to complete and verify the task.",
|
|
338
388
|
});
|
|
339
389
|
|
|
340
390
|
try {
|
|
341
|
-
const result = await runner.run(
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
391
|
+
const result = await runner.run(
|
|
392
|
+
agent,
|
|
393
|
+
"Write SDK_OK=42 to result.txt and read it.",
|
|
394
|
+
{
|
|
395
|
+
maxTurns: 8,
|
|
396
|
+
sandbox: { client: sandbox },
|
|
397
|
+
},
|
|
398
|
+
);
|
|
345
399
|
console.log(result.finalOutput);
|
|
346
400
|
} finally {
|
|
347
401
|
await sandbox.lastSession?.close();
|
|
@@ -406,9 +460,9 @@ For acceptance, verify:
|
|
|
406
460
|
|
|
407
461
|
Use the returned billing data rather than recomputing cost from wall-clock time.
|
|
408
462
|
|
|
409
|
-
## Run the real Playground acceptance suite
|
|
463
|
+
## Run the real Playground recipe acceptance suite
|
|
410
464
|
|
|
411
|
-
From an xapi-cli development checkout, run the same nine
|
|
465
|
+
From an xapi-cli development checkout, run the same nine client recipes shown
|
|
412
466
|
in the Web Playground. The suite uses normal CLI configuration, never accepts a
|
|
413
467
|
key on argv, records audit/billing evidence, terminates every tracked instance
|
|
414
468
|
in `finally`, and fails if any instance created after its baseline remains
|
package/src/client.ts
CHANGED
|
@@ -26,6 +26,13 @@ export interface ClientOptions {
|
|
|
26
26
|
apiKey?: string;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
export interface ApiKeyApiRequestOptions {
|
|
30
|
+
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
31
|
+
body?: unknown;
|
|
32
|
+
timeoutMs?: number;
|
|
33
|
+
retries?: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
29
36
|
export class HttpError extends Error {
|
|
30
37
|
constructor(
|
|
31
38
|
public readonly status: number,
|
|
@@ -100,8 +107,28 @@ function parseRetryAfterMs(res: Response): number | undefined {
|
|
|
100
107
|
return Number.isFinite(at) ? Math.max(0, at - Date.now()) : undefined;
|
|
101
108
|
}
|
|
102
109
|
|
|
103
|
-
function
|
|
104
|
-
|
|
110
|
+
function abortError(signal?: AbortSignal | null): Error {
|
|
111
|
+
const reason = signal?.reason;
|
|
112
|
+
return reason instanceof Error
|
|
113
|
+
? reason
|
|
114
|
+
: new DOMException('The operation was aborted', 'AbortError');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function sleep(ms: number, signal?: AbortSignal | null): Promise<void> {
|
|
118
|
+
if (signal?.aborted) return Promise.reject(abortError(signal));
|
|
119
|
+
return new Promise((resolve, reject) => {
|
|
120
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
121
|
+
const onAbort = () => {
|
|
122
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
123
|
+
signal?.removeEventListener('abort', onAbort);
|
|
124
|
+
reject(abortError(signal));
|
|
125
|
+
};
|
|
126
|
+
timer = setTimeout(() => {
|
|
127
|
+
signal?.removeEventListener('abort', onAbort);
|
|
128
|
+
resolve();
|
|
129
|
+
}, ms);
|
|
130
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
131
|
+
});
|
|
105
132
|
}
|
|
106
133
|
|
|
107
134
|
export async function request<T>(
|
|
@@ -138,7 +165,7 @@ export async function request<T>(
|
|
|
138
165
|
if (isRetryableStatus(res.status) && attempt < retries) {
|
|
139
166
|
await res.text().catch(() => ''); // drain body so the socket can be reused
|
|
140
167
|
clearTimeout(timer);
|
|
141
|
-
await sleep(backoffDelayMs(attempt, retryAfterMs));
|
|
168
|
+
await sleep(backoffDelayMs(attempt, retryAfterMs), callerSignal);
|
|
142
169
|
attempt++;
|
|
143
170
|
continue;
|
|
144
171
|
}
|
|
@@ -174,7 +201,7 @@ export async function request<T>(
|
|
|
174
201
|
if (timedOut) {
|
|
175
202
|
const timeoutError = new RequestTimeoutError(timeoutMs);
|
|
176
203
|
if (attempt < retries) {
|
|
177
|
-
await sleep(backoffDelayMs(attempt));
|
|
204
|
+
await sleep(backoffDelayMs(attempt), callerSignal);
|
|
178
205
|
attempt++;
|
|
179
206
|
continue;
|
|
180
207
|
}
|
|
@@ -182,7 +209,7 @@ export async function request<T>(
|
|
|
182
209
|
}
|
|
183
210
|
if (isRetryableNetworkError(e) && attempt < retries) {
|
|
184
211
|
clearTimeout(timer);
|
|
185
|
-
await sleep(backoffDelayMs(attempt));
|
|
212
|
+
await sleep(backoffDelayMs(attempt), callerSignal);
|
|
186
213
|
attempt++;
|
|
187
214
|
continue;
|
|
188
215
|
}
|
|
@@ -200,6 +227,30 @@ function headers(apiKey?: string): Record<string, string> {
|
|
|
200
227
|
return h;
|
|
201
228
|
}
|
|
202
229
|
|
|
230
|
+
/** Call a scoped API-Key control-plane endpoint without exchanging for a JWT. */
|
|
231
|
+
export function apiKeyApiRequest<T>(
|
|
232
|
+
apiHost: string,
|
|
233
|
+
apiKey: string,
|
|
234
|
+
path: string,
|
|
235
|
+
options: ApiKeyApiRequestOptions = {},
|
|
236
|
+
): Promise<T> {
|
|
237
|
+
const method = options.method ?? 'GET';
|
|
238
|
+
const requestHeaders: Record<string, string> = { 'XAPI-KEY': apiKey };
|
|
239
|
+
if (options.body !== undefined) requestHeaders['Content-Type'] = 'application/json';
|
|
240
|
+
return request<T>(
|
|
241
|
+
`${scheme(apiHost)}://${apiHost}${path}`,
|
|
242
|
+
{
|
|
243
|
+
method,
|
|
244
|
+
headers: requestHeaders,
|
|
245
|
+
...(options.body !== undefined
|
|
246
|
+
? { body: JSON.stringify(options.body) }
|
|
247
|
+
: {}),
|
|
248
|
+
},
|
|
249
|
+
options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
250
|
+
options.retries ?? 0,
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
203
254
|
function baseUrl(opts: ClientOptions): string {
|
|
204
255
|
return `${scheme(opts.actionHost)}://${opts.actionHost}`;
|
|
205
256
|
}
|
package/src/sandbox-client.ts
CHANGED
|
@@ -26,6 +26,7 @@ export interface SandboxRequirements {
|
|
|
26
26
|
gpu?: { count?: number; model?: string };
|
|
27
27
|
regions?: string[];
|
|
28
28
|
capabilities?: string[];
|
|
29
|
+
minContinuousRuntimeSeconds?: number;
|
|
29
30
|
[key: string]: unknown;
|
|
30
31
|
}
|
|
31
32
|
|
|
@@ -263,24 +264,43 @@ export async function sandboxWait(
|
|
|
263
264
|
signal?: AbortSignal,
|
|
264
265
|
): Promise<SandboxDetail> {
|
|
265
266
|
const deadline = Date.now() + timeoutMs;
|
|
267
|
+
const deadlineController = new AbortController();
|
|
268
|
+
const abortFromCaller = () => deadlineController.abort();
|
|
269
|
+
const deadlineTimer = setTimeout(() => deadlineController.abort(), Math.max(0, timeoutMs));
|
|
270
|
+
if (signal?.aborted) deadlineController.abort();
|
|
271
|
+
else signal?.addEventListener('abort', abortFromCaller, { once: true });
|
|
266
272
|
let last: SandboxDetail | undefined;
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
273
|
+
try {
|
|
274
|
+
while (Date.now() < deadline) {
|
|
275
|
+
if (signal?.aborted) throw new Error(`sandbox wait interrupted while waiting for ${wanted.join(' or ')}`);
|
|
276
|
+
try {
|
|
277
|
+
last = await sandboxGet(opts, id, deadlineController.signal);
|
|
278
|
+
} catch (error) {
|
|
279
|
+
if (signal?.aborted) {
|
|
280
|
+
throw new Error(`sandbox wait interrupted while waiting for ${wanted.join(' or ')}`);
|
|
281
|
+
}
|
|
282
|
+
if (Date.now() >= deadline) break;
|
|
283
|
+
throw error;
|
|
284
|
+
}
|
|
285
|
+
if (Date.now() >= deadline) break;
|
|
286
|
+
const state = String(last.observedState || '');
|
|
287
|
+
if (wanted.includes(state)) return last;
|
|
288
|
+
if (['FAILED', 'TERMINATED'].includes(state) && !wanted.includes(state)) {
|
|
289
|
+
throw new Error(`sandbox ${id} entered ${state} while waiting for ${wanted.join(' or ')}`);
|
|
290
|
+
}
|
|
291
|
+
await new Promise<void>((resolve) => {
|
|
292
|
+
const done = () => {
|
|
293
|
+
clearTimeout(timer);
|
|
294
|
+
signal?.removeEventListener('abort', done);
|
|
295
|
+
resolve();
|
|
296
|
+
};
|
|
297
|
+
const timer = setTimeout(done, Math.min(intervalMs, Math.max(0, deadline - Date.now())));
|
|
298
|
+
signal?.addEventListener('abort', done, { once: true });
|
|
299
|
+
});
|
|
274
300
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
signal?.removeEventListener('abort', done);
|
|
279
|
-
resolve();
|
|
280
|
-
};
|
|
281
|
-
const timer = setTimeout(done, Math.min(intervalMs, Math.max(0, deadline - Date.now())));
|
|
282
|
-
signal?.addEventListener('abort', done, { once: true });
|
|
283
|
-
});
|
|
301
|
+
} finally {
|
|
302
|
+
clearTimeout(deadlineTimer);
|
|
303
|
+
signal?.removeEventListener('abort', abortFromCaller);
|
|
284
304
|
}
|
|
285
305
|
throw new Error(
|
|
286
306
|
`sandbox ${id} did not enter ${wanted.join(' or ')} within ${timeoutMs}ms` +
|