@veris-ai/daytona 0.2.0 → 0.3.0

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 CHANGED
@@ -20,8 +20,8 @@ Both, because `@daytona/sdk` is a peer dependency — that is what keeps
20
20
 
21
21
  | variable | where from |
22
22
  |---|---|
23
- | `DAYTONA_API_KEY` | [app.daytona.io/dashboard/keys](https://app.daytona.io/dashboard/keys) |
24
- | `VERIS_API_KEY` | [studio.veris.ai](https://studio.veris.ai) |
23
+ | `DAYTONA_API_KEY` | [app.daytona.io/dashboard/keys](https://app.daytona.io/dashboard/keys), with the **write and delete sandboxes** permissions — a write-only key provisions boxes it can never delete |
24
+ | `VERIS_API_KEY` | [studio.veris.ai](https://studio.veris.ai) — or leave it unset after `veris login`: the CLI's profile in `~/.veris/twin.yaml` is read instead, `VERIS_PROFILE` picks one, and the variable wins whenever both exist |
25
25
  | `VERIS_ENVIRONMENT_ID` | a Veris environment — it decides which vendor services your twin gets |
26
26
 
27
27
  ## Use
@@ -45,6 +45,148 @@ await sbx.delete() // deletes the twin too
45
45
  This package re-exports everything from `@daytona/sdk`, so it is the only import
46
46
  you need to change. No particular sandbox image is required.
47
47
 
48
+ ### Or skip the code: `veris-daytona run`
49
+
50
+ The same four steps as one command, for a test suite that already exists:
51
+
52
+ ```sh
53
+ npx @veris-ai/daytona run --setup 'pip install -e .' -- pytest tests/integration
54
+ ```
55
+
56
+ That uploads the current directory into a fresh sandbox (minus `node_modules`,
57
+ `.venv`, `.git` and the like), runs the setup command, runs the test command
58
+ streaming its output, prints the receipt, and deletes the sandbox and the twin.
59
+ The exit code is the test command's — except that a green suite whose twin
60
+ received nothing exits 1, because a pass without a receipt is not a pass.
61
+
62
+ ```
63
+ Veris receipt — twin sbx_a1b2c3
64
+ interception: gateway integrity: verified
65
+
66
+ 1 request(s) reached the twin:
67
+ stripe: 1 request(s)
68
+ POST /v1/customers -> 200
69
+ ```
70
+
71
+ | flag | |
72
+ |---|---|
73
+ | `--repo <url> [--ref <branch>]` | clone instead of uploading; `GITHUB_TOKEN` is used for a private repo |
74
+ | `--environment <id>` | instead of `VERIS_ENVIRONMENT_ID` |
75
+ | `--require-service <name>` | the receipt must show this service, repeatable |
76
+ | `--image <name>` / `--snapshot <name>` | what to run in; default is Daytona's default snapshot |
77
+ | `--env KEY=VALUE` | exported to both commands, repeatable |
78
+ | `--timeout <seconds>` | for the test command; default 1800 |
79
+ | `--keep` | leave the sandbox up afterwards, and the twin if `run` made it |
80
+
81
+ `veris-daytona run --help` lists everything.
82
+
83
+ ### Or hand the wired box to something else: `provision`, `push`, `exec`, `teardown`
84
+
85
+ `run` does the whole job in one command, and it stays. But when the thing that
86
+ installs the dependencies and runs the suite is another tool — the `veris` CLI,
87
+ a CI step, an agent — what you want from this package is the first half only:
88
+
89
+ ```sh
90
+ box=$(npx @veris-ai/daytona provision --sandbox sbx_a1b2c3 --image python:3.12)
91
+ ```
92
+
93
+ That creates a sandbox attached to a twin you already have, does every
94
+ Veris-shaped thing — the egress credential, the gateway pin, the outbound
95
+ proxy, the CA bundle, the canary, the trust variables — and
96
+ stops. Nothing is uploaded, nothing is run, nothing is deleted. One JSON object
97
+ goes to stdout and every human line to stderr, so `$box` is parseable:
98
+
99
+ ```json
100
+ {
101
+ "daytonaSandboxId": "e2a1…",
102
+ "verisSandboxId": "sbx_a1b2c3",
103
+ "verisEnvironmentId": "env_9f…",
104
+ "ownsTwin": false,
105
+ "workDir": "/home/daytona/veris-run",
106
+ "caBundlePath": "/tmp/veris-ca-bundle.crt",
107
+ "trustEnv": { "SSL_CERT_FILE": "/tmp/veris-ca-bundle.crt", "…": "…" },
108
+ "trustPrelude": "export SSL_CERT_FILE='/tmp/veris-ca-bundle.crt'; …",
109
+ "patchBundledCasCommand": "sh /tmp/veris-patch-bundled-cas.sh",
110
+ "pushCommand": "veris-daytona push e2a1…",
111
+ "execCommand": "veris-daytona exec e2a1… -- <command>",
112
+ "services": ["stripe", "github"],
113
+ "expiresAt": "2026-09-04T12:00:00.000Z",
114
+ "autoStopMinutes": 30,
115
+ "autoDeleteMinutes": 60
116
+ }
117
+ ```
118
+
119
+ | flag | |
120
+ |---|---|
121
+ | `--sandbox <twin-id>` | the twin to attach to — **required**; `veris up` prints its id |
122
+ | `--image <name>` / `--snapshot <name>` | what to run in; default is Daytona's default snapshot |
123
+ | `--env KEY=VALUE` | set as a sandbox environment variable, repeatable |
124
+
125
+ From there the box is yours. Reading the receipt and deciding what it proved is
126
+ the caller's job; that is the whole point of the split.
127
+
128
+ ### Getting code in and running it: `push` and `exec`
129
+
130
+ Daytona has no route into a box that already exists. Their CLI (v0.210.0) has no
131
+ upload, copy or sync command; `daytona ssh` takes exactly one argument, so there
132
+ is no `tar | ssh` and no scp or rsync behind it; `--context` is a Docker build
133
+ context that only exists on `create`, which `provision` owns; and `git clone`
134
+ inside the box is a clone, not an upload of what is on your disk. So two verbs
135
+ do it:
136
+
137
+ ```sh
138
+ id=$(echo "$box" | jq -r .daytonaSandboxId)
139
+
140
+ npx @veris-ai/daytona push "$id" # tars the cwd into workDir
141
+ npx @veris-ai/daytona exec "$id" -- pip install -e .
142
+ npx @veris-ai/daytona exec "$id" -- sh /tmp/veris-patch-bundled-cas.sh
143
+ npx @veris-ai/daytona exec "$id" -- python -m pytest tests/integration
144
+ ```
145
+
146
+ `push` uploads the current directory, minus what gets rebuilt inside
147
+ (`node_modules`, `.venv`, `dist`, `__pycache__`, …), and unpacks it in the same
148
+ `workDir` the JSON named — so the two chain without carrying the path between
149
+ them. `--repo <url> --ref <branch>` clones instead, using `GITHUB_TOKEN` for a
150
+ private one.
151
+
152
+ `exec` runs one command with `trustEnv` already exported, which is the part that
153
+ matters: `daytona exec` has no `--env` flag at all, so a command run through it
154
+ inherits Daytona's own CA file and fails on the gateway's certificate unless you
155
+ retype the trust prelude every single time. It streams output as it happens
156
+ rather than returning at the end, takes `--cwd`, repeatable `--env KEY=VALUE`
157
+ and `--timeout <seconds>`, and exits with the command's own status.
158
+
159
+ Neither reads a receipt or passes a verdict — take a watermark before and read
160
+ `veris sandbox trace --since` after. And once the dependencies are installed,
161
+ run `patchBundledCasCommand` **inside the box** to patch the CA bundles an SDK
162
+ ships with it; the script is already in there, so a shell caller needs nothing
163
+ from this package.
164
+
165
+ ### Taking it back: `teardown`
166
+
167
+ However it went:
168
+
169
+ ```sh
170
+ npx @veris-ai/daytona teardown "$(echo "$box" | jq -r .daytonaSandboxId)"
171
+ ```
172
+
173
+ `teardown` deletes the sandbox and says what it did about the twin: one this
174
+ package created goes with it, one it attached to — always the case after
175
+ `provision` — is yours and is left running. It exits 1 when there is no such
176
+ sandbox, saying plainly that no twin was touched.
177
+
178
+ Nothing deletes a provisioned box for you, so it comes up with its own brakes:
179
+ it stops after 30 idle minutes, Daytona deletes it an hour after that, and it
180
+ is destroyed 4 hours after creation whatever state it is in. The twin's TTL is
181
+ untouched — it belongs to whoever created the twin.
182
+
183
+ Deleting needs a Daytona key with the `delete:sandboxes` permission, and a key
184
+ made with "write sandboxes" alone does not have it. `teardown` then exits 1
185
+ saying so: which key, which permissions it has, that the box is still there,
186
+ when its own brakes stop and delete it, what happened to the twin, and where a
187
+ key that can delete comes from. `provision` and `run` read the key's
188
+ permissions first and warn about the same thing *before* creating a box.
189
+
48
190
  ### Why `assertTouched` and not just a green suite
49
191
 
50
192
  A test suite that skipped its integration and one that exercised it look
@@ -62,21 +204,54 @@ when it is empty.
62
204
  | `services()` | what the twin answers for, and where |
63
205
  | `manual(service)` | that service's manual: what it models, how its data is shaped |
64
206
  | `getDataPlaneEnv()` | `{ DATABASE_URL: … }` for non-HTTP twin services |
65
- | `getTrustEnv()` | the CA variables injected into the sandbox |
207
+ | `getTrustEnv()` | the CA variables, as a map for a process you start |
208
+ | `trustPrelude()` | the same variables, as one line of shell `export`s |
209
+ | `patchBundledCas()` | append the Veris CA to the CA bundles your SDKs ship |
210
+ | `environmentId` | the Veris environment the twin was deployed from |
66
211
  | `deliverTo(port \| url)` | point vendor webhooks back at your sandbox |
67
212
 
68
213
  `sbx.verisSandboxId` is the twin's id — not to be confused with `sbx.id`, which
69
214
  is Daytona's.
70
215
 
216
+ ### Trust, for a command you run yourself
217
+
218
+ Daytona overwrites `SSL_CERT_FILE`, `REQUESTS_CA_BUNDLE`, `CURL_CA_BUNDLE` and
219
+ `NODE_EXTRA_CA_CERTS` inside the sandbox with its own CA file, which cannot
220
+ verify the gateway's certificates. Anything not started with the right values
221
+ inherits the broken ones — `uv sync` dies with `invalid peer certificate:
222
+ UnknownIssuer`. Two shapes, because callers come in two shapes:
223
+
224
+ ```ts
225
+ // You control the process's environment:
226
+ await sbx.process.executeCommand('uv sync', cwd, sbx.veris.getTrustEnv())
227
+
228
+ // You can only prefix a command line (a session, someone else's runner):
229
+ await sbx.process.executeCommand(`${sbx.veris.trustPrelude()} uv sync`)
230
+ ```
231
+
232
+ And for an SDK that reads no variable because it ships its own CA file — the
233
+ measured case is stripe-python's `verify=stripe.ca_bundle_path`:
234
+
235
+ ```ts
236
+ await sbx.process.executeCommand('pip install -e .', cwd, sbx.veris.getTrustEnv())
237
+ console.log(await sbx.veris.patchBundledCas()) // ['…/stripe/data/ca-certificates.crt', …]
238
+ ```
239
+
240
+ Call it *after* installing dependencies — the bundles arrive with them. It is
241
+ idempotent and returns only the files it changed. `veris-daytona run` calls it
242
+ for you, between `--setup` and the command; a sandbox from `provision` carries
243
+ the same patcher as a script at `/tmp/veris-patch-bundled-cas.sh`, so whoever
244
+ installed the dependencies can run it with no SDK in hand.
245
+
71
246
  ## Options
72
247
 
73
248
  ```ts
74
249
  await daytona.create({
75
250
  image: 'node:22',
76
251
  veris: {
77
- egress: 'strict', // default. 'open' sets no allowlist — debugging only
78
- allowOut: ['internal.corp'],
79
- allowRegistries: true, // default. npm, PyPI, apt, crates…
252
+ egress: 'strict', // default: Daytona reaches only the gateway's address.
253
+ // 'open': no Daytona allowlist, for a control plane
254
+ // that has not published the gateway's addresses
80
255
  installCa: true, // default
81
256
  ttlMinutes: 60,
82
257
  attachSandboxId: 'sbx_…', // reuse an existing twin; delete() will not remove it
@@ -90,15 +265,21 @@ Coordinates can also come from `veris.apiKey` / `veris.environmentId` /
90
265
 
91
266
  ## How it works
92
267
 
93
- Every sandbox is created with two Daytona parameters: a `domainAllowList` (the
94
- vendor hostnames the twin answers for, the gateway, the twin's data planes, and
95
- package registries — nothing else leaves) and an `outboundProxyUrl` pointing at
96
- the Veris gateway over HTTP CONNECT.
268
+ Every sandbox is created with two Daytona parameters: a `networkAllowList`
269
+ holding the Veris gateway's IPv4 address as one `/32` and nothing else, and an
270
+ `outboundProxyUrl` pointing at that gateway over HTTP CONNECT.
271
+
272
+ Daytona chains them: sandbox traffic reaches Daytona's own proxy, which forwards
273
+ it to the gateway, which answers vendor hostnames from the twin and passes
274
+ public hosts — registries, git, a routeless twin's own URL — through untouched.
275
+ A process that ignores the proxy variables cannot dial out at all. Nothing of
276
+ ours runs inside the sandbox, which is why any image works.
97
277
 
98
- Daytona chains them: sandbox traffic reaches Daytona's own proxy, which drops
99
- anything not allowlisted and forwards the rest to the gateway, which answers
100
- vendor hostnames from the twin. Nothing of ours runs inside the sandbox, which
101
- is why any image works.
278
+ Daytona is pinned by address, never by a hostname list, because a
279
+ `domainAllowList` set beside the proxy URL turns Daytona's egress into a
280
+ TLS-inspecting proxy that rejects the gateway's certificate (measured; see the
281
+ repository README). The address comes from the control plane's egress
282
+ credential (`gateway_ips`); an older control plane gets one DNS lookup.
102
283
 
103
284
  Before `create()` resolves, a canary probe dials a reserved hostname only the
104
285
  gateway answers, whose body carries the twin id. It proves in one request that
@@ -122,7 +303,66 @@ four systems involved refused:
122
303
  cannot be used; `create()` then fails at `credential-mint` saying so.
123
304
  - **QUIC/HTTP3 and ECH are not intercepted.** The gateway relays TCP. Both are
124
305
  reported in the receipt's `leaks` rather than silently omitted.
306
+ - **The image needs `curl`.** The canary probe runs it; a slim image without
307
+ it fails `create()` in the `canary` phase.
308
+ - **Python 3.13+ needs a gateway that mints strict-verifier-safe leaves**
309
+ (services-sandbox#1044). Without it, `requests` fails with
310
+ `Missing Authority Key Identifier` while `curl` and Node succeed.
311
+ - **Daytona overwrites `REQUESTS_CA_BUNDLE`, `SSL_CERT_FILE` and
312
+ `CURL_CA_BUNDLE`** with its own CA file, which lacks the Veris CA. `run`
313
+ exports the Veris bundle on every command it runs; a command run through
314
+ `sandbox.process` yourself needs `sbx.veris.getTrustEnv()` as its env, or
315
+ `sbx.veris.trustPrelude()` in front of the command line.
316
+ - **An SDK that bundles its own CA reads no variable at all.**
317
+ `sbx.veris.patchBundledCas()` covers certifi, pip's vendored certifi,
318
+ botocore, stripe and httplib2; anything else fails with its own error naming
319
+ the file to add.
320
+ - **Strict mode needs the gateway's address.** It is read from the egress
321
+ credential (`gateway_ips`), else resolved once from the proxy host. When
322
+ neither yields an IPv4 address, `create()` fails at `credential-mint` naming
323
+ `veris.egress: 'open'`, which sets no Daytona allowlist and still blocks a
324
+ process that bypasses the proxy.
325
+ - **`teardown` needs `delete:sandboxes` on the Daytona key.** A write-only
326
+ key creates boxes it cannot delete; the refusal says when Daytona's own
327
+ auto-stop and auto-delete will, and `provision` warns before creating one.
328
+ - **A very long run's receipt is a floor.** The twin's log is read in pages up
329
+ to a budget; past it, `entry.capped` is true and `run` prints `≥N`.
125
330
 
126
331
  ## License
127
332
 
128
333
  Apache-2.0. Source: [veris-ai/veris-daytona](https://github.com/veris-ai/veris-daytona).
334
+
335
+ ## Receipts for an individual test run
336
+
337
+ The next release adds a baseline API; existing `receipt()` calls still read the
338
+ SDK's default receipt window. Capture immediately before the isolated command:
339
+
340
+ ```js
341
+ const baseline = await sandbox.veris.receiptBaseline()
342
+ // Run and await the application's own test command using this provider's SDK.
343
+ const receipt = await sandbox.veris.receiptSince(baseline, 'stripe')
344
+ const entry = receipt.services.stripe
345
+ if (!entry || entry.capped) throw new Error('Application evidence is incomplete')
346
+ ```
347
+
348
+ `ReceiptRequest.id` is stable within service history. `receiptSince` validates the
349
+ execution sandbox, twin, service set/control URLs, and a uniquely marked schema
350
+ read retained in the trace both before and after paging. Reset/erasure/replacement
351
+ invalidates the baseline. Services without retained control request headers cannot
352
+ establish this baseline; the call fails explicitly. Numeric IDs alone cannot detect
353
+ all resets. Use a fresh baseline after reconnect/reset, before re-executing the test.
354
+
355
+ Pages advance IDs within a newest-ID snapshot. `entry.requests` is an observed lower
356
+ bound when `capped` is true; `incompleteReason` explains page budget, failed read or
357
+ non-progress. A failed first read throws, while a successful empty window returns
358
+ zero. `sinceId`/`untilId` identify the observed window. Control/probe tiers and
359
+ `/veris/*` paths are excluded from application entries. Unmarked vendor probes and
360
+ concurrent runs remain indistinguishable: finish them before baseline capture and
361
+ retain application response/state assertions. Preserve mode, integrity and leaks.
362
+
363
+ `veris.control(service, resource, options)` supports `manual`, `schema`,
364
+ `operations`, `data`, and `requests`; `options` contains `method`, `query`, and
365
+ `body`. Only data supports `POST`/`PATCH` writes, using the service's schema-defined
366
+ `{data: {table: [rows]}}` envelope, including fault rows. Coordinates come from the
367
+ attached twin; lifecycle and arbitrary URLs are excluded. SDK callers own write
368
+ authorization; the OpenCode plugin applies its configured write permission.
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare function main(argv: readonly string[]): Promise<number>;