@veris-ai/daytona 0.2.1 → 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 +254 -14
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +2099 -0
- package/dist/cli.js.map +1 -0
- package/dist/control-plane.d.ts +13 -5
- package/dist/daytona-key.d.ts +28 -0
- package/dist/daytona.d.ts +86 -10
- package/dist/exec.d.ts +15 -0
- package/dist/gateway.d.ts +35 -1
- package/dist/index.cjs +579 -109
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +13 -5
- package/dist/index.js +554 -108
- package/dist/index.js.map +1 -1
- package/dist/network.d.ts +50 -38
- package/dist/profile.d.ts +70 -0
- package/dist/provision.d.ts +100 -0
- package/dist/push.d.ts +11 -0
- package/dist/receipt.d.ts +25 -3
- package/dist/run-receipt.d.ts +13 -0
- package/dist/run.d.ts +87 -0
- package/dist/service-control.d.ts +8 -0
- package/dist/teardown.d.ts +52 -0
- package/dist/trust.d.ts +100 -0
- package/dist/veris-api.d.ts +58 -0
- package/package.json +7 -1
package/dist/cli.js
ADDED
|
@@ -0,0 +1,2099 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { execFileSync } from "child_process";
|
|
5
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
6
|
+
import { DaytonaNotFoundError } from "@daytona/sdk";
|
|
7
|
+
|
|
8
|
+
// src/daytona.ts
|
|
9
|
+
import { Daytona as BaseDaytona } from "@daytona/sdk";
|
|
10
|
+
|
|
11
|
+
// src/errors.ts
|
|
12
|
+
function withCause(message, cause) {
|
|
13
|
+
const detail = cause instanceof Error ? cause.message : typeof cause === "string" ? cause : "";
|
|
14
|
+
if (!detail || message.includes(detail)) return message;
|
|
15
|
+
const trimmed = detail.length > 200 ? `${detail.slice(0, 200)}\u2026` : detail;
|
|
16
|
+
return `${message}: ${trimmed}`;
|
|
17
|
+
}
|
|
18
|
+
var VerisError = class extends Error {
|
|
19
|
+
phase;
|
|
20
|
+
/** The Veris twin's sandbox id, when one exists yet. */
|
|
21
|
+
verisSandboxId;
|
|
22
|
+
/** Verbatim control-plane response body, when the failure came from an API call. */
|
|
23
|
+
responseBody;
|
|
24
|
+
constructor(message, opts = {}) {
|
|
25
|
+
super(withCause(message, opts.cause), opts.cause !== void 0 ? { cause: opts.cause } : void 0);
|
|
26
|
+
this.name = new.target.name;
|
|
27
|
+
this.phase = opts.phase;
|
|
28
|
+
this.verisSandboxId = opts.verisSandboxId;
|
|
29
|
+
this.responseBody = opts.responseBody;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
var MissingCredentialsError = class extends VerisError {
|
|
33
|
+
};
|
|
34
|
+
var VerisGatewayUnreachableError = class extends VerisError {
|
|
35
|
+
};
|
|
36
|
+
var VerisGatewayNotOfferedError = class extends VerisError {
|
|
37
|
+
/** Server-announced minimum SDK version, when the refusal carried one. */
|
|
38
|
+
minSdk;
|
|
39
|
+
constructor(message, opts = {}) {
|
|
40
|
+
super(message, opts);
|
|
41
|
+
this.minSdk = opts.minSdk;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
var ReceiptIntegrityError = class extends VerisError {
|
|
45
|
+
};
|
|
46
|
+
var VerisUntouchedError = class extends VerisError {
|
|
47
|
+
service;
|
|
48
|
+
constructor(message, service, opts = {}) {
|
|
49
|
+
super(message, opts);
|
|
50
|
+
this.service = service;
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
var TwinExpiredError = class extends VerisError {
|
|
54
|
+
};
|
|
55
|
+
var SnapshotUnsupportedError = class extends VerisError {
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// src/control-plane.ts
|
|
59
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
60
|
+
var ControlPlane = class {
|
|
61
|
+
apiBase;
|
|
62
|
+
headers;
|
|
63
|
+
constructor(opts) {
|
|
64
|
+
this.apiBase = opts.apiBase.replace(/\/$/, "");
|
|
65
|
+
this.headers = {
|
|
66
|
+
"X-API-Key": opts.apiKey,
|
|
67
|
+
"X-Veris-SDK": opts.sdkVersion,
|
|
68
|
+
"Content-Type": "application/json"
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
async request(method, path, body) {
|
|
72
|
+
let res;
|
|
73
|
+
try {
|
|
74
|
+
res = await fetch(`${this.apiBase}${path}`, {
|
|
75
|
+
method,
|
|
76
|
+
headers: this.headers,
|
|
77
|
+
body: body === void 0 ? void 0 : JSON.stringify(body)
|
|
78
|
+
});
|
|
79
|
+
} catch (cause) {
|
|
80
|
+
throw new VerisError(`Veris control plane unreachable (${method} ${path})`, { cause });
|
|
81
|
+
}
|
|
82
|
+
return res;
|
|
83
|
+
}
|
|
84
|
+
async json(res, context, phase) {
|
|
85
|
+
const text = await res.text();
|
|
86
|
+
let parsed;
|
|
87
|
+
try {
|
|
88
|
+
parsed = text ? JSON.parse(text) : void 0;
|
|
89
|
+
} catch {
|
|
90
|
+
parsed = text;
|
|
91
|
+
}
|
|
92
|
+
if (!res.ok) {
|
|
93
|
+
throw new VerisError(`${context}: ${res.status}`, { phase, responseBody: parsed });
|
|
94
|
+
}
|
|
95
|
+
if (parsed === void 0) {
|
|
96
|
+
throw new VerisError(`${context}: empty response body`, { phase, responseBody: text });
|
|
97
|
+
}
|
|
98
|
+
return parsed;
|
|
99
|
+
}
|
|
100
|
+
async createTwin(environmentId, opts = {}) {
|
|
101
|
+
const res = await this.request("POST", `/v1/environments/${environmentId}/sandboxes`, {
|
|
102
|
+
ttl_minutes: opts.ttlMinutes,
|
|
103
|
+
metadata: opts.metadata
|
|
104
|
+
});
|
|
105
|
+
if (res.status === 404) {
|
|
106
|
+
throw new VerisError(
|
|
107
|
+
`environment '${environmentId}' does not exist on ${this.apiBase}. Environments belong to one control plane: check that VERIS_ENVIRONMENT_ID and VERIS_API_BASE name the same one (an id from the other environment gives exactly this 404).`,
|
|
108
|
+
{ phase: "twin-provision", responseBody: await res.text() }
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
return this.json(res, `create sandbox in environment ${environmentId}`, "twin-provision");
|
|
112
|
+
}
|
|
113
|
+
async getTwin(sandboxId) {
|
|
114
|
+
const res = await this.request("GET", `/v1/sandboxes/${sandboxId}`);
|
|
115
|
+
if (res.status === 404) return null;
|
|
116
|
+
return this.json(res, `get sandbox ${sandboxId}`);
|
|
117
|
+
}
|
|
118
|
+
/** Poll until the twin reports ready. "failed" is terminal per the API docs. */
|
|
119
|
+
async waitReady(sandboxId, timeoutMs) {
|
|
120
|
+
const deadline = Date.now() + timeoutMs;
|
|
121
|
+
for (; ; ) {
|
|
122
|
+
const twin = await this.getTwin(sandboxId);
|
|
123
|
+
if (!twin) throw new TwinExpiredError(`Veris sandbox ${sandboxId} disappeared while provisioning`, { verisSandboxId: sandboxId });
|
|
124
|
+
if (twin.status === "ready") return twin;
|
|
125
|
+
if (twin.status === "failed") {
|
|
126
|
+
throw new VerisError(
|
|
127
|
+
`Veris sandbox ${sandboxId} failed to provision: ${twin.failure_reason ?? "no failure_reason"}`,
|
|
128
|
+
{ phase: "twin-provision", verisSandboxId: sandboxId }
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
if (Date.now() > deadline) {
|
|
132
|
+
throw new VerisError(
|
|
133
|
+
`Veris sandbox ${sandboxId} not ready after ${timeoutMs}ms (status: ${twin.status})`,
|
|
134
|
+
{ phase: "twin-provision", verisSandboxId: sandboxId }
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
await sleep(1500);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
async services(sandboxId) {
|
|
141
|
+
const res = await this.request("GET", `/v1/sandboxes/${sandboxId}/services`);
|
|
142
|
+
if (res.status === 404) {
|
|
143
|
+
throw new TwinExpiredError(`Veris sandbox ${sandboxId} not found \u2014 expired or deleted`, { verisSandboxId: sandboxId });
|
|
144
|
+
}
|
|
145
|
+
return this.json(res, `services of sandbox ${sandboxId}`, "receipt");
|
|
146
|
+
}
|
|
147
|
+
async deleteTwin(environmentId, sandboxId) {
|
|
148
|
+
const res = await this.request("DELETE", `/v1/environments/${environmentId}/sandboxes/${sandboxId}`);
|
|
149
|
+
if (res.status === 404) return false;
|
|
150
|
+
if (!res.ok) await this.json(res, `delete sandbox ${sandboxId}`);
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Mint (or re-mint) the gateway egress credential for a twin. Returns null
|
|
155
|
+
* when the control plane does not offer gateway mode at all (404 — route
|
|
156
|
+
* absent), so `mode: 'auto'` can fall back; throws VerisGatewayNotOfferedError
|
|
157
|
+
* on an explicit version refusal (409 sdk_too_old).
|
|
158
|
+
*/
|
|
159
|
+
async mintEgressCredential(environmentId, sandboxId) {
|
|
160
|
+
const res = await this.request("POST", `/v1/environments/${environmentId}/sandboxes/${sandboxId}/egress-credential`);
|
|
161
|
+
if (res.status === 404) return null;
|
|
162
|
+
if (res.status === 409) {
|
|
163
|
+
const body = await res.json().catch(() => ({}));
|
|
164
|
+
throw new VerisGatewayNotOfferedError(
|
|
165
|
+
`this SDK version is below the control plane's minimum for gateway mode${body.min_sdk ? ` (min_sdk ${body.min_sdk})` : ""} \u2014 upgrade @veris-ai/daytona`,
|
|
166
|
+
{ phase: "credential-mint", verisSandboxId: sandboxId, minSdk: body.min_sdk, responseBody: body }
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
return this.json(res, `mint egress credential for ${sandboxId}`, "credential-mint");
|
|
170
|
+
}
|
|
171
|
+
/** PATCH the twin resource. Omitted fields are untouched by the server. */
|
|
172
|
+
async updateSandbox(environmentId, sandboxId, patch) {
|
|
173
|
+
const res = await this.request("PATCH", `/v1/environments/${environmentId}/sandboxes/${sandboxId}`, patch);
|
|
174
|
+
if (res.status === 404) {
|
|
175
|
+
throw new TwinExpiredError(`Veris sandbox ${sandboxId} not found`, { verisSandboxId: sandboxId });
|
|
176
|
+
}
|
|
177
|
+
if (!res.ok) await this.json(res, `update sandbox ${sandboxId}`);
|
|
178
|
+
}
|
|
179
|
+
// There was an extendTtl() here, and it did nothing.
|
|
180
|
+
//
|
|
181
|
+
// It PATCHed { ttl_minutes } at the sandbox resource, and the server's
|
|
182
|
+
// UpdateSandboxRequest declares only client_base_url. Pydantic ignores
|
|
183
|
+
// unknown fields, so the route answered 200 having changed no TTL at all —
|
|
184
|
+
// a call that reports success while doing nothing, which is worse than no
|
|
185
|
+
// call, because the caller then believes the twin will outlive the run.
|
|
186
|
+
// Nothing here called it; it was API surface promising a thing that could
|
|
187
|
+
// not happen.
|
|
188
|
+
//
|
|
189
|
+
// Bring it back when a route exists to carry it. Until then a twin's TTL is
|
|
190
|
+
// decided once, by ttl_minutes at create — where this SDK already keeps it
|
|
191
|
+
// in step with the Daytona sandbox's own ttlMinutes.
|
|
192
|
+
/** Create-time preflight: is the gateway infrastructure up, per the control plane? */
|
|
193
|
+
async gatewayHealth() {
|
|
194
|
+
const res = await this.request("GET", "/v1/gateway/health");
|
|
195
|
+
if (res.status === 404) return;
|
|
196
|
+
if (!res.ok) {
|
|
197
|
+
throw new VerisGatewayUnreachableError(
|
|
198
|
+
`Veris gateway reported unhealthy (${res.status})`,
|
|
199
|
+
{ phase: "gateway-preflight" }
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
// src/run-receipt.ts
|
|
206
|
+
import { randomUUID } from "crypto";
|
|
207
|
+
|
|
208
|
+
// src/receipt.ts
|
|
209
|
+
var PAGE_LIMIT = 1e3;
|
|
210
|
+
var MAX_PAGES = 20;
|
|
211
|
+
function rowsOf(body) {
|
|
212
|
+
if (!body || !Array.isArray(body.requests)) {
|
|
213
|
+
throw new VerisError("invalid request log: expected requests array", { phase: "receipt" });
|
|
214
|
+
}
|
|
215
|
+
const rows = body.requests;
|
|
216
|
+
for (const r of rows) {
|
|
217
|
+
if (!r || !Number.isSafeInteger(r.id) || r.id <= 0 || typeof r.method !== "string" || typeof r.path !== "string" || typeof r.tier !== "string" || !(r.status === null || typeof r.status === "number")) {
|
|
218
|
+
throw new VerisError("invalid request log row: stable id, method, path, tier and status required", { phase: "receipt" });
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return rows;
|
|
222
|
+
}
|
|
223
|
+
function parseRequestsBody(body) {
|
|
224
|
+
const rows = rowsOf(body);
|
|
225
|
+
const entries = rows.filter(
|
|
226
|
+
(r) => ["handler", "fault", "fallback", "fallback-llm", "fallback-replay"].includes(r.tier) && !/^\/veris(?:\/|$)/.test(r.path)
|
|
227
|
+
).map(({ id, method, path, status, tier }) => ({ id, method, path, status, tier }));
|
|
228
|
+
return { count: entries.length, entries, total: rows.length };
|
|
229
|
+
}
|
|
230
|
+
async function readPage(svc, query) {
|
|
231
|
+
const res = await fetch(`${svc.control_url.replace(/\/$/, "")}/veris/requests?${query}`, {
|
|
232
|
+
signal: AbortSignal.timeout(3e4),
|
|
233
|
+
redirect: "error"
|
|
234
|
+
});
|
|
235
|
+
const text = await res.text();
|
|
236
|
+
if (!res.ok) throw new VerisError(`could not read receipt for service '${svc.name}' (${res.status})`, { phase: "receipt" });
|
|
237
|
+
let body;
|
|
238
|
+
try {
|
|
239
|
+
body = JSON.parse(text);
|
|
240
|
+
} catch {
|
|
241
|
+
throw new VerisError(`service '${svc.name}' returned a non-JSON receipt body`, { phase: "receipt" });
|
|
242
|
+
}
|
|
243
|
+
return rowsOf(body);
|
|
244
|
+
}
|
|
245
|
+
async function fetchWatermark(svc) {
|
|
246
|
+
const rows = await readPage(svc, new URLSearchParams({ limit: "1", order: "desc" }));
|
|
247
|
+
if (rows.length > 1) throw new VerisError("request log ignored watermark limit", { phase: "receipt" });
|
|
248
|
+
return rows[0]?.id ?? 0;
|
|
249
|
+
}
|
|
250
|
+
async function fetchReceiptEntry(svc, sinceId = 0) {
|
|
251
|
+
if (!Number.isSafeInteger(sinceId) || sinceId < 0) throw new VerisError("invalid receipt watermark", { phase: "receipt" });
|
|
252
|
+
const end = await fetchWatermark(svc);
|
|
253
|
+
if (end < sinceId) throw new VerisError("receipt baseline invalid: log moved backwards; take a new baseline", { phase: "receipt" });
|
|
254
|
+
const entries = [];
|
|
255
|
+
const raw = [];
|
|
256
|
+
let mark = sinceId;
|
|
257
|
+
let incompleteReason;
|
|
258
|
+
for (let page = 0; mark < end; page++) {
|
|
259
|
+
if (page >= MAX_PAGES) {
|
|
260
|
+
incompleteReason = "page-limit";
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
let rows;
|
|
264
|
+
try {
|
|
265
|
+
rows = await readPage(svc, new URLSearchParams({ limit: String(PAGE_LIMIT), order: "asc", since_id: String(mark) }));
|
|
266
|
+
} catch (error) {
|
|
267
|
+
if (!raw.length) throw error;
|
|
268
|
+
incompleteReason = "read-failed";
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
271
|
+
let previous = mark;
|
|
272
|
+
if (!rows.length || rows.some((r) => {
|
|
273
|
+
const bad = r.id <= previous;
|
|
274
|
+
previous = r.id;
|
|
275
|
+
return bad;
|
|
276
|
+
})) {
|
|
277
|
+
incompleteReason = "pagination-not-progressing";
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
const window = rows.filter((r) => r.id <= end);
|
|
281
|
+
raw.push(...window);
|
|
282
|
+
entries.push(...parseRequestsBody({ requests: window }).entries);
|
|
283
|
+
mark = rows[rows.length - 1].id;
|
|
284
|
+
}
|
|
285
|
+
entries.sort((a, b) => b.id - a.id);
|
|
286
|
+
return {
|
|
287
|
+
requests: entries.length,
|
|
288
|
+
controlUrl: svc.control_url,
|
|
289
|
+
entries,
|
|
290
|
+
sinceId,
|
|
291
|
+
untilId: end,
|
|
292
|
+
capped: Boolean(incompleteReason),
|
|
293
|
+
...incompleteReason ? { incompleteReason } : {},
|
|
294
|
+
raw: { requests: raw }
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// src/run-receipt.ts
|
|
299
|
+
var HEADER = "x-veris-receipt-baseline";
|
|
300
|
+
function markerOf(row) {
|
|
301
|
+
try {
|
|
302
|
+
const headers = typeof row.request_headers === "string" ? JSON.parse(row.request_headers) : row.request_headers;
|
|
303
|
+
return headers?.[HEADER];
|
|
304
|
+
} catch {
|
|
305
|
+
return void 0;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
async function captureBaseline(twinId, sandboxId, services) {
|
|
309
|
+
const marks = await Promise.all(services.map(async (svc) => {
|
|
310
|
+
const marker = randomUUID();
|
|
311
|
+
const res = await fetch(`${svc.control_url.replace(/\/$/, "")}/veris/schema`, {
|
|
312
|
+
headers: { [HEADER]: marker },
|
|
313
|
+
signal: AbortSignal.timeout(3e4),
|
|
314
|
+
redirect: "error"
|
|
315
|
+
});
|
|
316
|
+
await res.text();
|
|
317
|
+
if (!res.ok) throw new VerisError(`could not establish baseline for '${svc.name}' (${res.status})`, { phase: "receipt" });
|
|
318
|
+
for (let attempt = 0; attempt < 5; attempt++) {
|
|
319
|
+
const rows = await readPage(svc, new URLSearchParams({ limit: "1000", order: "desc" }));
|
|
320
|
+
const anchor = rows.find((r) => markerOf(r) === marker && r.tier === "control" && r.path === "/veris/schema");
|
|
321
|
+
if (anchor) return [svc.name, { controlUrl: svc.control_url, id: anchor.id, marker }];
|
|
322
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
323
|
+
}
|
|
324
|
+
throw new VerisError(`baseline unavailable for '${svc.name}': trace must retain control request headers`, { phase: "receipt" });
|
|
325
|
+
}));
|
|
326
|
+
return { version: 1, twinId, sandboxId, services: Object.fromEntries(marks) };
|
|
327
|
+
}
|
|
328
|
+
async function validateBaseline(baseline, twinId, sandboxId, services) {
|
|
329
|
+
const invalid = () => new VerisError("receipt baseline invalid: session, services or history changed; take a new baseline before execution", { phase: "receipt" });
|
|
330
|
+
if (baseline.version !== 1 || baseline.twinId !== twinId || baseline.sandboxId !== sandboxId || Object.keys(baseline.services).sort().join("\0") !== services.map((s) => s.name).sort().join("\0")) throw invalid();
|
|
331
|
+
await Promise.all(services.map(async (svc) => {
|
|
332
|
+
const mark = baseline.services[svc.name];
|
|
333
|
+
if (!mark || mark.controlUrl !== svc.control_url || !Number.isSafeInteger(mark.id) || mark.id <= 0) throw invalid();
|
|
334
|
+
const rows = await readPage(svc, new URLSearchParams({ limit: "1", order: "asc", since_id: String(mark.id - 1) }));
|
|
335
|
+
if (rows.length !== 1 || rows[0].id !== mark.id || markerOf(rows[0]) !== mark.marker) throw invalid();
|
|
336
|
+
}));
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// src/service-control.ts
|
|
340
|
+
async function serviceControl(svc, resource, options = {}) {
|
|
341
|
+
const method = options.method ?? "GET";
|
|
342
|
+
if (!["manual", "schema", "operations", "data", "requests"].includes(resource) || !["GET", "POST", "PATCH"].includes(method) || method !== "GET" && resource !== "data" || method === "GET" && options.body !== void 0 || !/^https?:\/\//.test(svc.control_url)) {
|
|
343
|
+
throw new VerisError("unsupported service control operation");
|
|
344
|
+
}
|
|
345
|
+
const url = new URL(`${svc.control_url.replace(/\/$/, "")}/veris/${resource}`);
|
|
346
|
+
for (const [key, value] of Object.entries(options.query ?? {})) url.searchParams.set(key, value);
|
|
347
|
+
const res = await fetch(url, {
|
|
348
|
+
method,
|
|
349
|
+
redirect: "error",
|
|
350
|
+
signal: AbortSignal.timeout(3e4),
|
|
351
|
+
headers: { "Content-Type": "application/json" },
|
|
352
|
+
body: options.body === void 0 ? void 0 : JSON.stringify(options.body)
|
|
353
|
+
});
|
|
354
|
+
const text = await res.text();
|
|
355
|
+
if (!res.ok) throw new VerisError(`service '${svc.name}' ${method} /veris/${resource} failed (${res.status})`);
|
|
356
|
+
try {
|
|
357
|
+
return JSON.parse(text);
|
|
358
|
+
} catch {
|
|
359
|
+
throw new VerisError(`service '${svc.name}' returned invalid control JSON`);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// src/network.ts
|
|
364
|
+
var isHttpUrl = (u) => /^https?:/.test(u);
|
|
365
|
+
function dataPlaneEnv(services) {
|
|
366
|
+
const envs = {};
|
|
367
|
+
for (const svc of services) {
|
|
368
|
+
if (!svc.env_hint || !svc.url || isHttpUrl(svc.url)) continue;
|
|
369
|
+
if (!isSafeEnvName(svc.env_hint)) continue;
|
|
370
|
+
envs[svc.env_hint] = svc.url;
|
|
371
|
+
}
|
|
372
|
+
return envs;
|
|
373
|
+
}
|
|
374
|
+
var PROCESS_CONTROLLING = /* @__PURE__ */ new Set([
|
|
375
|
+
"PATH",
|
|
376
|
+
"LD_PRELOAD",
|
|
377
|
+
"LD_LIBRARY_PATH",
|
|
378
|
+
"NODE_OPTIONS",
|
|
379
|
+
"BASH_ENV",
|
|
380
|
+
"ENV",
|
|
381
|
+
"PYTHONPATH",
|
|
382
|
+
"PYTHONSTARTUP",
|
|
383
|
+
"SHELL",
|
|
384
|
+
"IFS",
|
|
385
|
+
"HOME",
|
|
386
|
+
"PROMPT_COMMAND"
|
|
387
|
+
]);
|
|
388
|
+
function isSafeEnvName(name) {
|
|
389
|
+
return /^[A-Z][A-Z0-9_]{0,63}$/.test(name) && !PROCESS_CONTROLLING.has(name);
|
|
390
|
+
}
|
|
391
|
+
var IPV4_RE = /^(25[0-5]|2[0-4]\d|1?\d?\d)(\.(25[0-5]|2[0-4]\d|1?\d?\d)){3}$/;
|
|
392
|
+
function buildNetwork(args) {
|
|
393
|
+
const { mode, gatewayIps: gatewayIps2 } = args;
|
|
394
|
+
if (mode === "open") return { params: {} };
|
|
395
|
+
const ips = [...new Set(gatewayIps2)];
|
|
396
|
+
if (!ips.length) {
|
|
397
|
+
throw new VerisError(
|
|
398
|
+
"the control plane did not name the gateway's IP addresses (egress credential `gateway_ips`) and the gateway hostname could not be resolved, so the sandbox cannot be pinned to it. Upgrade the control plane, or run with veris.egress: 'open' (Daytona still blocks anything that bypasses the proxy).",
|
|
399
|
+
{ phase: "credential-mint" }
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
const bad = ips.filter((ip) => !IPV4_RE.test(ip));
|
|
403
|
+
if (bad.length) {
|
|
404
|
+
throw new VerisError(
|
|
405
|
+
`the control plane returned gateway addresses that are not IPv4: ${JSON.stringify(bad)} (Daytona's networkAllowList takes IPv4 CIDRs only)`,
|
|
406
|
+
{ phase: "credential-mint" }
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
return { params: { networkAllowList: ips.map((ip) => `${ip}/32`).join(",") } };
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// src/gateway.ts
|
|
413
|
+
import { promises as dns } from "dns";
|
|
414
|
+
|
|
415
|
+
// src/trust.ts
|
|
416
|
+
var CA_CERT_PATH = "/usr/local/share/ca-certificates/veris-ca.crt";
|
|
417
|
+
var SYSTEM_BUNDLE = "/etc/ssl/certs/ca-certificates.crt";
|
|
418
|
+
var VERIS_CA_FILE = "/tmp/veris-ca.crt";
|
|
419
|
+
var VERIS_BUNDLE = "/tmp/veris-ca-bundle.crt";
|
|
420
|
+
function vendoredTrustEnv() {
|
|
421
|
+
return {
|
|
422
|
+
SSL_CERT_FILE: VERIS_BUNDLE,
|
|
423
|
+
REQUESTS_CA_BUNDLE: VERIS_BUNDLE,
|
|
424
|
+
CURL_CA_BUNDLE: VERIS_BUNDLE,
|
|
425
|
+
GIT_SSL_CAINFO: VERIS_BUNDLE,
|
|
426
|
+
AWS_CA_BUNDLE: VERIS_BUNDLE,
|
|
427
|
+
CARGO_HTTP_CAINFO: VERIS_BUNDLE,
|
|
428
|
+
DENO_CERT: VERIS_BUNDLE,
|
|
429
|
+
PIP_CERT: VERIS_BUNDLE,
|
|
430
|
+
npm_config_cafile: VERIS_BUNDLE,
|
|
431
|
+
GRPC_DEFAULT_SSL_ROOTS_FILE_PATH: VERIS_BUNDLE,
|
|
432
|
+
BUNDLE_SSL_CA_CERT: VERIS_BUNDLE,
|
|
433
|
+
COMPOSER_CAFILE: VERIS_BUNDLE,
|
|
434
|
+
HEX_CACERTS_PATH: VERIS_BUNDLE,
|
|
435
|
+
JULIA_SSL_CA_ROOTS_PATH: VERIS_BUNDLE,
|
|
436
|
+
NIX_SSL_CERT_FILE: VERIS_BUNDLE,
|
|
437
|
+
PERL_LWP_SSL_CA_FILE: VERIS_BUNDLE,
|
|
438
|
+
CLOUDSDK_CORE_CUSTOM_CA_CERTS_FILE: VERIS_BUNDLE,
|
|
439
|
+
NODE_EXTRA_CA_CERTS: VERIS_BUNDLE
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
function trustPrelude(env = vendoredTrustEnv()) {
|
|
443
|
+
return Object.entries(env).map(([k, v]) => `export ${k}='${v.replace(/'/g, `'\\''`)}';`).join(" ");
|
|
444
|
+
}
|
|
445
|
+
var BUNDLED_CA_FILES = [
|
|
446
|
+
{ sdk: "pip (vendored certifi)", suffix: "pip/_vendor/certifi/cacert.pem" },
|
|
447
|
+
{ sdk: "certifi", suffix: "certifi/cacert.pem" },
|
|
448
|
+
{ sdk: "botocore", suffix: "botocore/cacert.pem" },
|
|
449
|
+
// Matches both stripe-python's site-packages layout and the stripe-ruby gem.
|
|
450
|
+
{ sdk: "stripe", suffix: "stripe/data/ca-certificates.crt" },
|
|
451
|
+
{ sdk: "httplib2", suffix: "httplib2/cacerts.txt" }
|
|
452
|
+
];
|
|
453
|
+
var BUNDLED_CA_PATCH_SCRIPT = "/tmp/veris-patch-bundled-cas.sh";
|
|
454
|
+
var BUNDLED_CA_PATCHED_MARKER = "the Veris CA was appended to ";
|
|
455
|
+
function bundledCaPatchScript() {
|
|
456
|
+
const find = BUNDLED_CA_FILES.map((f) => `find / -xdev \\( -path /proc -o -path /sys -o -path /dev \\) -prune -o -path '*/${f.suffix}' -type f -print 2>/dev/null`).join("; ");
|
|
457
|
+
return [
|
|
458
|
+
`marker=$(sed -n 2p ${VERIS_CA_FILE} 2>/dev/null)`,
|
|
459
|
+
// No CA on disk means nothing to append, and appending nothing to every
|
|
460
|
+
// bundle in the image would be the worst available no-op.
|
|
461
|
+
`[ -n "$marker" ] || { echo "no ${VERIS_CA_FILE} to append" >&2; exit 1; }`,
|
|
462
|
+
// The whole loop is one brace group at the end of the pipe so `n` survives
|
|
463
|
+
// to the summary: a pipeline's last stage is a subshell, and a counter
|
|
464
|
+
// incremented inside a bare `while` would be gone by the time we report it.
|
|
465
|
+
`{ ${find}; } | sort -u | { n=0`,
|
|
466
|
+
`while IFS= read -r f; do`,
|
|
467
|
+
` [ -w "$f" ] || continue`,
|
|
468
|
+
` grep -qF "$marker" "$f" && continue`,
|
|
469
|
+
` { printf '\\n'; cat ${VERIS_CA_FILE}; } >> "$f" || continue`,
|
|
470
|
+
` n=$((n+1))`,
|
|
471
|
+
` echo "${BUNDLED_CA_PATCHED_MARKER}$f"`,
|
|
472
|
+
`done`,
|
|
473
|
+
// Silence is the common case and it reads as "did that even run?", so say
|
|
474
|
+
// so. patchBundledCas() keeps only the lines above, so this costs it
|
|
475
|
+
// nothing.
|
|
476
|
+
`if [ "$n" -gt 0 ]; then echo "$n bundled CA file(s) patched"`,
|
|
477
|
+
`else echo "no bundled CA file needed patching \u2014 no SDK that ships its own is installed here, or they already trust the Veris CA"; fi`,
|
|
478
|
+
`}`
|
|
479
|
+
].join("\n");
|
|
480
|
+
}
|
|
481
|
+
var NODE_TRUST_FLAG = "--use-openssl-ca";
|
|
482
|
+
function nodeOptionsWithTrust(existing) {
|
|
483
|
+
const base = (existing ?? "").trim();
|
|
484
|
+
if (base.split(/\s+/).includes(NODE_TRUST_FLAG)) return base;
|
|
485
|
+
return base ? `${base} ${NODE_TRUST_FLAG}` : NODE_TRUST_FLAG;
|
|
486
|
+
}
|
|
487
|
+
var CA_INSTALL_CMD = [
|
|
488
|
+
"update-ca-certificates",
|
|
489
|
+
`(keytool -importcert -noprompt -cacerts -storepass changeit -alias veris -file ${CA_CERT_PATH} 2>/dev/null || true)`,
|
|
490
|
+
`(command -v certutil >/dev/null 2>&1 && for db in $(find /home /root -maxdepth 4 -name "cert9.db" 2>/dev/null | xargs -r -n1 dirname); do certutil -A -n veris -t "C,," -i ${CA_CERT_PATH} -d "sql:$db" 2>/dev/null || true; done || true)`
|
|
491
|
+
].join(" && ");
|
|
492
|
+
function sanitizeTrustEnv(served) {
|
|
493
|
+
const vendored = vendoredTrustEnv();
|
|
494
|
+
const out2 = { ...vendored };
|
|
495
|
+
for (const [k, val] of Object.entries(served ?? {})) {
|
|
496
|
+
if (!(k in vendored)) continue;
|
|
497
|
+
if (typeof val !== "string") continue;
|
|
498
|
+
if (!/^\/[\w./+~-]+$/.test(val)) continue;
|
|
499
|
+
out2[k] = val;
|
|
500
|
+
}
|
|
501
|
+
return out2;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// src/gateway.ts
|
|
505
|
+
var HOSTNAME_RE = /^[A-Za-z0-9.-]+$/;
|
|
506
|
+
var HOSTPORT_RE = /^[A-Za-z0-9.-]+:\d{1,5}$/;
|
|
507
|
+
function shellQuote(s) {
|
|
508
|
+
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
509
|
+
}
|
|
510
|
+
var sh = (sandbox, cmd, timeoutSec = 60) => sandbox.process.executeCommand(`sh -lc ${shellQuote(cmd)}`, void 0, void 0, timeoutSec);
|
|
511
|
+
function gatewayProxyUrl(credential) {
|
|
512
|
+
if (credential.http_proxy_url) return assertProxyUrl(credential.http_proxy_url);
|
|
513
|
+
if (!credential.connect_address || !HOSTPORT_RE.test(credential.connect_address)) {
|
|
514
|
+
throw new VerisError(
|
|
515
|
+
`the control plane returned a malformed gateway address: ${JSON.stringify(credential.connect_address)} (expected host:port)`,
|
|
516
|
+
{ phase: "credential-mint" }
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
return `http://${encodeURIComponent(credential.username)}:x@${credential.connect_address}`;
|
|
520
|
+
}
|
|
521
|
+
async function gatewayIps(credential, proxyUrl) {
|
|
522
|
+
if (credential.gateway_ips?.length) return credential.gateway_ips;
|
|
523
|
+
const host = new URL(proxyUrl).hostname;
|
|
524
|
+
if (/^\d+\.\d+\.\d+\.\d+$/.test(host)) return [host];
|
|
525
|
+
try {
|
|
526
|
+
return await dns.resolve4(host);
|
|
527
|
+
} catch {
|
|
528
|
+
return [];
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
function assertProxyUrl(raw) {
|
|
532
|
+
let u;
|
|
533
|
+
try {
|
|
534
|
+
u = new URL(raw);
|
|
535
|
+
} catch {
|
|
536
|
+
throw new VerisError(
|
|
537
|
+
`the control plane returned an unparseable gateway proxy URL: ${JSON.stringify(raw)}`,
|
|
538
|
+
{ phase: "credential-mint" }
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
if (u.protocol !== "http:" && u.protocol !== "https:") {
|
|
542
|
+
throw new VerisError(
|
|
543
|
+
`the gateway proxy URL uses scheme "${u.protocol.replace(":", "")}", and Daytona accepts only http or https outbound proxies`,
|
|
544
|
+
{ phase: "credential-mint" }
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
if (!u.hostname || !u.port) {
|
|
548
|
+
throw new VerisError(
|
|
549
|
+
`the gateway proxy URL is missing a host or port: ${JSON.stringify(raw)}`,
|
|
550
|
+
{ phase: "credential-mint" }
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
return raw;
|
|
554
|
+
}
|
|
555
|
+
async function installCa(sandbox, caPem) {
|
|
556
|
+
await sandbox.fs.uploadFile(Buffer.from(caPem, "utf8"), VERIS_CA_FILE);
|
|
557
|
+
await sandbox.fs.uploadFile(Buffer.from(bundledCaPatchScript(), "utf8"), BUNDLED_CA_PATCH_SCRIPT);
|
|
558
|
+
const script = [
|
|
559
|
+
`chmod 0644 ${VERIS_CA_FILE}`,
|
|
560
|
+
// Public roots first so they keep working; ours appended. `cat` of a
|
|
561
|
+
// missing file is tolerated — an image with no roots at all still gets a
|
|
562
|
+
// bundle containing the one CA that matters here.
|
|
563
|
+
`{ cat ${SYSTEM_BUNDLE} 2>/dev/null; cat ${VERIS_CA_FILE}; } > ${VERIS_BUNDLE}`,
|
|
564
|
+
`chmod 0644 ${VERIS_BUNDLE}`,
|
|
565
|
+
// Best-effort, for the stacks that read a store rather than a variable:
|
|
566
|
+
// the system bundle, the JVM truststore, and NSS databases. All of it needs
|
|
567
|
+
// root and tooling that may not be there, so none of it is load-bearing —
|
|
568
|
+
// but a Java client honours no CA env var at all, so where we CAN do it,
|
|
569
|
+
// we should.
|
|
570
|
+
`SUDO=; [ "$(id -u)" = 0 ] || SUDO="sudo -n"`,
|
|
571
|
+
`($SUDO install -m 0644 -D ${VERIS_CA_FILE} ${CA_CERT_PATH} 2>/dev/null && $SUDO sh -c ${shellQuote(CA_INSTALL_CMD)} 2>/dev/null) || true`,
|
|
572
|
+
// The bundle is the load-bearing one: fail loudly if it is not there.
|
|
573
|
+
`[ -s ${VERIS_BUNDLE} ] && echo __VERIS_CA_OK__`
|
|
574
|
+
].join("; ");
|
|
575
|
+
const r = await sh(sandbox, script, 120).catch((e) => ({ exitCode: 1, result: String(e) }));
|
|
576
|
+
if (!(r.result ?? "").includes("__VERIS_CA_OK__")) {
|
|
577
|
+
throw new SnapshotUnsupportedError(
|
|
578
|
+
`could not assemble a CA bundle at ${VERIS_BUNDLE}, so the gateway's certificates cannot be trusted (${(r.result ?? "").trim().slice(0, 200)})`,
|
|
579
|
+
{ phase: "ca-install" }
|
|
580
|
+
);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
async function patchBundledCas(sandbox) {
|
|
584
|
+
const r = await sh(sandbox, `sh ${BUNDLED_CA_PATCH_SCRIPT}`, 180).catch((e) => ({ exitCode: 1, result: String(e) }));
|
|
585
|
+
return (r.result ?? "").split("\n").filter((line) => line.startsWith(BUNDLED_CA_PATCHED_MARKER)).map((line) => line.slice(BUNDLED_CA_PATCHED_MARKER.length).trim()).filter(Boolean);
|
|
586
|
+
}
|
|
587
|
+
async function probeCanary(sandbox, canaryHost, expectedTwinId) {
|
|
588
|
+
if (!HOSTNAME_RE.test(canaryHost)) {
|
|
589
|
+
throw new ReceiptIntegrityError(
|
|
590
|
+
`refusing to probe a malformed canary host from the control plane: ${JSON.stringify(canaryHost)}`,
|
|
591
|
+
{ phase: "canary", verisSandboxId: expectedTwinId }
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
const r = await sh(
|
|
595
|
+
sandbox,
|
|
596
|
+
`curl -sS --cacert ${VERIS_BUNDLE} --max-time 20 https://${canaryHost}/ || echo __VERIS_CANARY_FAIL__`,
|
|
597
|
+
45
|
|
598
|
+
).catch((e) => ({ exitCode: 1, result: String(e) }));
|
|
599
|
+
let body = {};
|
|
600
|
+
try {
|
|
601
|
+
body = JSON.parse(r.result ?? "");
|
|
602
|
+
} catch {
|
|
603
|
+
}
|
|
604
|
+
if (body.veris_sandbox_id !== expectedTwinId) {
|
|
605
|
+
throw new ReceiptIntegrityError(
|
|
606
|
+
`canary probe failed: egress from this Daytona sandbox is not tunnelled through the Veris gateway (expected twin ${expectedTwinId}, canary answered: ${(r.result || "nothing").trim().slice(0, 200)})`,
|
|
607
|
+
{ phase: "canary", verisSandboxId: expectedTwinId }
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// src/state.ts
|
|
613
|
+
function assertHttpControlPlane(svc) {
|
|
614
|
+
if (!svc.control_url || !isHttpUrl(svc.control_url)) {
|
|
615
|
+
throw new VerisError(
|
|
616
|
+
`service '${svc.name}' has no HTTP control plane, so there is nothing to read`,
|
|
617
|
+
{ phase: "twin-state" }
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
async function fetchManual(svc) {
|
|
622
|
+
assertHttpControlPlane(svc);
|
|
623
|
+
const res = await fetch(`${svc.control_url}/veris/manual`);
|
|
624
|
+
const text = await res.text();
|
|
625
|
+
if (!res.ok) {
|
|
626
|
+
throw new VerisError(`could not read the manual for service '${svc.name}' (${res.status})`, {
|
|
627
|
+
phase: "twin-state",
|
|
628
|
+
responseBody: text.slice(0, 500)
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
return text;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// src/veris-api.ts
|
|
635
|
+
var VerisApiImpl = class {
|
|
636
|
+
constructor(ctx) {
|
|
637
|
+
this.ctx = ctx;
|
|
638
|
+
}
|
|
639
|
+
ctx;
|
|
640
|
+
get ownsTwin() {
|
|
641
|
+
return this.ctx.ownsTwin;
|
|
642
|
+
}
|
|
643
|
+
get sandboxId() {
|
|
644
|
+
return this.ctx.twinId;
|
|
645
|
+
}
|
|
646
|
+
get environmentId() {
|
|
647
|
+
return this.ctx.environmentId;
|
|
648
|
+
}
|
|
649
|
+
get mode() {
|
|
650
|
+
return "gateway";
|
|
651
|
+
}
|
|
652
|
+
services() {
|
|
653
|
+
return this.ctx.controlPlane.services(this.ctx.twinId);
|
|
654
|
+
}
|
|
655
|
+
/** Resolve a service by name. A typo and a service that exists but saw
|
|
656
|
+
* nothing are different failures, so this throws rather than returning
|
|
657
|
+
* undefined and letting the caller report an empty result. */
|
|
658
|
+
async resolveService(service) {
|
|
659
|
+
const services = await this.services();
|
|
660
|
+
const svc = services.find((s) => s.name === service);
|
|
661
|
+
if (!svc) {
|
|
662
|
+
throw new VerisError(
|
|
663
|
+
`unknown service '${service}' \u2014 the twin has no service by that name (available: ${services.map((s) => s.name).join(", ") || "none"})`,
|
|
664
|
+
{ verisSandboxId: this.ctx.twinId }
|
|
665
|
+
);
|
|
666
|
+
}
|
|
667
|
+
return svc;
|
|
668
|
+
}
|
|
669
|
+
async manual(service) {
|
|
670
|
+
return fetchManual(await this.resolveService(service));
|
|
671
|
+
}
|
|
672
|
+
async receipt(service) {
|
|
673
|
+
await probeCanary(this.ctx.sandbox, this.ctx.canaryHost, this.ctx.twinId);
|
|
674
|
+
if (service !== void 0) {
|
|
675
|
+
const svc = await this.resolveService(service);
|
|
676
|
+
return fetchReceiptEntry(svc, this.watermark(svc.name));
|
|
677
|
+
}
|
|
678
|
+
const services = await this.services();
|
|
679
|
+
const entries = await Promise.all(
|
|
680
|
+
services.filter((s) => isHttpUrl(s.control_url)).map(async (svc) => [svc.name, await fetchReceiptEntry(svc, this.watermark(svc.name))])
|
|
681
|
+
);
|
|
682
|
+
return {
|
|
683
|
+
services: Object.fromEntries(entries),
|
|
684
|
+
mode: "gateway",
|
|
685
|
+
integrity: "verified",
|
|
686
|
+
leaks: this.leaks()
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
/**
|
|
690
|
+
* What this receipt cannot see. The gateway relays TCP, so QUIC/HTTP3 and ECH
|
|
691
|
+
* ride around it — named rather than rounded off.
|
|
692
|
+
*/
|
|
693
|
+
leaks() {
|
|
694
|
+
return ["udp-quic-possible", "ech-possible"];
|
|
695
|
+
}
|
|
696
|
+
/** Where this service's log was when the run began. 0 reads all of it. */
|
|
697
|
+
watermark(service) {
|
|
698
|
+
return this.ctx.watermarks?.[service] ?? 0;
|
|
699
|
+
}
|
|
700
|
+
async verifyIntegrity() {
|
|
701
|
+
await probeCanary(this.ctx.sandbox, this.ctx.canaryHost, this.ctx.twinId);
|
|
702
|
+
}
|
|
703
|
+
async receiptBaseline() {
|
|
704
|
+
await this.verifyIntegrity();
|
|
705
|
+
return captureBaseline(
|
|
706
|
+
this.ctx.twinId,
|
|
707
|
+
this.ctx.sandbox.id,
|
|
708
|
+
(await this.services()).filter((s) => isHttpUrl(s.control_url))
|
|
709
|
+
);
|
|
710
|
+
}
|
|
711
|
+
async receiptSince(baseline, service) {
|
|
712
|
+
await this.verifyIntegrity();
|
|
713
|
+
const services = (await this.services()).filter((s) => isHttpUrl(s.control_url));
|
|
714
|
+
if (service !== void 0 && !services.some((s) => s.name === service)) {
|
|
715
|
+
throw new VerisError(`unknown HTTP service '${service}'`, { phase: "receipt" });
|
|
716
|
+
}
|
|
717
|
+
await validateBaseline(baseline, this.ctx.twinId, this.ctx.sandbox.id, services);
|
|
718
|
+
const selected = service === void 0 ? services : services.filter((s) => s.name === service);
|
|
719
|
+
const entries = await Promise.all(selected.map(async (svc) => [svc.name, await fetchReceiptEntry(svc, baseline.services[svc.name].id)]));
|
|
720
|
+
await validateBaseline(baseline, this.ctx.twinId, this.ctx.sandbox.id, await this.services().then((s) => s.filter((v) => isHttpUrl(v.control_url))));
|
|
721
|
+
return { services: Object.fromEntries(entries), mode: "gateway", integrity: "verified", leaks: this.leaks() };
|
|
722
|
+
}
|
|
723
|
+
async control(service, resource, options) {
|
|
724
|
+
const svc = (await this.services()).find((s) => s.name === service);
|
|
725
|
+
if (!svc) throw new VerisError(`unknown service '${service}'`);
|
|
726
|
+
return serviceControl(svc, resource, options);
|
|
727
|
+
}
|
|
728
|
+
async assertTouched(service, match) {
|
|
729
|
+
const entry = await this.receipt(service);
|
|
730
|
+
const need = match?.minRequests ?? 1;
|
|
731
|
+
const matched = match ? entry.entries.filter((r) => (match.method === void 0 || r.method.toUpperCase() === match.method.toUpperCase()) && (match.path === void 0 || r.path.includes(match.path))) : entry.entries;
|
|
732
|
+
if (matched.length < need && entry.capped) {
|
|
733
|
+
throw new VerisError(`receipt for '${service}' is incomplete; insufficient evidence`, { phase: "receipt" });
|
|
734
|
+
}
|
|
735
|
+
if (matched.length < need) {
|
|
736
|
+
const what = match ? `matching ${match.method ?? "ANY"} ${match.path ?? "*"} (${matched.length}/${need})` : "any intercepted requests";
|
|
737
|
+
throw new VerisUntouchedError(
|
|
738
|
+
`service '${service}' saw no ${what} \u2014 the code under test never reached it (a green run that skipped its dependency looks identical to a working one)`,
|
|
739
|
+
service,
|
|
740
|
+
{ verisSandboxId: this.ctx.twinId }
|
|
741
|
+
);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
async getDataPlaneEnv() {
|
|
745
|
+
return dataPlaneEnv(await this.services());
|
|
746
|
+
}
|
|
747
|
+
/** The CA trust vars injected at create, for callers building their own env. */
|
|
748
|
+
getTrustEnv() {
|
|
749
|
+
return vendoredTrustEnv();
|
|
750
|
+
}
|
|
751
|
+
/**
|
|
752
|
+
* The same variables as a shell prelude, for a caller that has no env map to
|
|
753
|
+
* fill — anything running a command through a session or a shell it did not
|
|
754
|
+
* build the environment for. Daytona resets SSL_CERT_FILE,
|
|
755
|
+
* REQUESTS_CA_BUNDLE, CURL_CA_BUNDLE and NODE_EXTRA_CA_CERTS to its own CA
|
|
756
|
+
* file inside the sandbox, and that file cannot verify the gateway's leaf, so
|
|
757
|
+
* a command that inherits them fails on certificate validation:
|
|
758
|
+
* `uv sync` dies with "invalid peer certificate: UnknownIssuer".
|
|
759
|
+
*
|
|
760
|
+
* sbx.process.executeCommand(`${sbx.veris.trustPrelude()} uv sync`)
|
|
761
|
+
*/
|
|
762
|
+
trustPrelude() {
|
|
763
|
+
return trustPrelude(this.getTrustEnv());
|
|
764
|
+
}
|
|
765
|
+
/**
|
|
766
|
+
* Append the Veris CA to the CA bundles the code under test's own SDKs ship.
|
|
767
|
+
*
|
|
768
|
+
* The variables above reach every client that reads one. stripe-python does
|
|
769
|
+
* not: it passes `verify=stripe.ca_bundle_path`, so the first Stripe call
|
|
770
|
+
* fails with "Could not verify Stripe's SSL certificate" in a sandbox where
|
|
771
|
+
* curl, Node and `requests` all succeed. This patches the file itself, which
|
|
772
|
+
* is the Daytona-shaped version of the veris CLI's --patch-bundled-cas.
|
|
773
|
+
*
|
|
774
|
+
* Call it AFTER installing dependencies — the bundles arrive with them, so
|
|
775
|
+
* there is nothing to patch at create time. Safe to call again; it returns
|
|
776
|
+
* only the files it actually changed.
|
|
777
|
+
*/
|
|
778
|
+
patchBundledCas() {
|
|
779
|
+
return patchBundledCas(this.ctx.sandbox);
|
|
780
|
+
}
|
|
781
|
+
async deliverTo(target, opts = {}) {
|
|
782
|
+
const url = typeof target === "number" ? (await this.ctx.sandbox.getPreviewLink(target)).url : target;
|
|
783
|
+
await this.ctx.controlPlane.updateSandbox(
|
|
784
|
+
this.ctx.environmentId,
|
|
785
|
+
this.ctx.twinId,
|
|
786
|
+
{ client_base_url: url }
|
|
787
|
+
);
|
|
788
|
+
if (url !== null && opts.probe !== false) await this.probeDelivery(url);
|
|
789
|
+
return url;
|
|
790
|
+
}
|
|
791
|
+
/** Ask each service to re-probe the registered destination; throw if none can reach it. */
|
|
792
|
+
async probeDelivery(url) {
|
|
793
|
+
const services = (await this.services()).filter((s) => isHttpUrl(s.control_url));
|
|
794
|
+
if (!services.length) return;
|
|
795
|
+
const probes = await Promise.all(services.map(async (svc) => {
|
|
796
|
+
try {
|
|
797
|
+
const res = await fetch(`${svc.control_url}/veris/client/probe`, { method: "POST" });
|
|
798
|
+
return res.ok ? await res.json() : null;
|
|
799
|
+
} catch {
|
|
800
|
+
return null;
|
|
801
|
+
}
|
|
802
|
+
}));
|
|
803
|
+
if (!probes.some((p) => p?.answered)) {
|
|
804
|
+
throw new VerisError(
|
|
805
|
+
`no service could reach ${url} \u2014 is your app listening on that port inside the Daytona sandbox?`,
|
|
806
|
+
{ phase: "receipt", verisSandboxId: this.ctx.twinId, responseBody: probes }
|
|
807
|
+
);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
};
|
|
811
|
+
|
|
812
|
+
// src/profile.ts
|
|
813
|
+
import { readFileSync } from "fs";
|
|
814
|
+
import { homedir } from "os";
|
|
815
|
+
import { join } from "path";
|
|
816
|
+
import { parse } from "yaml";
|
|
817
|
+
var DEFAULT_API_BASE = "https://svc.api.veris.ai";
|
|
818
|
+
function profilePath(home = homedir()) {
|
|
819
|
+
return join(home, ".veris", "twin.yaml");
|
|
820
|
+
}
|
|
821
|
+
function readOrUndefined(path) {
|
|
822
|
+
try {
|
|
823
|
+
return readFileSync(path, "utf8");
|
|
824
|
+
} catch (e) {
|
|
825
|
+
if (e.code === "ENOENT") return void 0;
|
|
826
|
+
throw e;
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
function parseProfileFile(text, path) {
|
|
830
|
+
if (text === void 0) return void 0;
|
|
831
|
+
let parsed;
|
|
832
|
+
try {
|
|
833
|
+
parsed = parse(text);
|
|
834
|
+
} catch (cause) {
|
|
835
|
+
throw new VerisError(`${path} is not valid YAML`, { phase: "credentials", cause });
|
|
836
|
+
}
|
|
837
|
+
if (parsed === null || parsed === void 0) return {};
|
|
838
|
+
if (typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
839
|
+
throw new VerisError(`${path} is not a profile file (expected a mapping)`, { phase: "credentials" });
|
|
840
|
+
}
|
|
841
|
+
return parsed;
|
|
842
|
+
}
|
|
843
|
+
function selectProfileName(env, file) {
|
|
844
|
+
return env.VERIS_PROFILE || file?.active_profile || "default";
|
|
845
|
+
}
|
|
846
|
+
function resolveVerisCredentials(opts = {}) {
|
|
847
|
+
const env = opts.env ?? process.env;
|
|
848
|
+
const path = opts.path ?? profilePath();
|
|
849
|
+
const file = parseProfileFile((opts.readFile ?? readOrUndefined)(path), path);
|
|
850
|
+
const name = selectProfileName(env, file);
|
|
851
|
+
const profile = file?.profiles?.[name] ?? void 0;
|
|
852
|
+
const found = profile !== void 0 && profile !== null;
|
|
853
|
+
const apiKey = opts.apiKey || env.VERIS_API_KEY || (found ? profile.api_key : void 0) || void 0;
|
|
854
|
+
const keySource = opts.apiKey ? "option" : env.VERIS_API_KEY ? "env" : apiKey ? "profile" : "none";
|
|
855
|
+
const profileBase = found ? profile.api_base : void 0;
|
|
856
|
+
const apiBase = (opts.apiBase || env.VERIS_API_BASE || profileBase || DEFAULT_API_BASE).replace(/\/$/, "");
|
|
857
|
+
const baseSource = opts.apiBase ? "option" : env.VERIS_API_BASE ? "env" : profileBase ? "profile" : "default";
|
|
858
|
+
return { apiKey, apiBase, keySource, baseSource, profile: { name, path, found } };
|
|
859
|
+
}
|
|
860
|
+
function missingKeyWhere(c) {
|
|
861
|
+
const where = c.profile.found ? `profile '${c.profile.name}' in ${c.profile.path} has no api_key` : `${c.profile.path} has no profile '${c.profile.name}'`;
|
|
862
|
+
return `VERIS_API_KEY is not set, and ${where}`;
|
|
863
|
+
}
|
|
864
|
+
var KEY_SOURCES_HINT = "Run `veris login` (the veris CLI saves the key there; `--profile <name>` for another plane, and VERIS_PROFILE picks it here), or set VERIS_API_KEY in the environment. Get a key at https://studio.veris.ai";
|
|
865
|
+
function missingKeyMessage(c) {
|
|
866
|
+
return `no Veris API key: ${missingKeyWhere(c)}. ${KEY_SOURCES_HINT}`;
|
|
867
|
+
}
|
|
868
|
+
function requireVerisCredentials(opts = {}) {
|
|
869
|
+
const c = resolveVerisCredentials(opts);
|
|
870
|
+
if (!c.apiKey) throw new MissingCredentialsError(missingKeyMessage(c), { phase: "credentials" });
|
|
871
|
+
return c;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
// src/version.ts
|
|
875
|
+
var SDK_VERSION = true ? "0.3.0" : "0.0.0-dev";
|
|
876
|
+
|
|
877
|
+
// src/daytona.ts
|
|
878
|
+
var LABEL = {
|
|
879
|
+
twinId: "veris_twin_id",
|
|
880
|
+
envId: "veris_env_id",
|
|
881
|
+
apiBase: "veris_api_base",
|
|
882
|
+
mode: "veris_mode",
|
|
883
|
+
egress: "veris_egress",
|
|
884
|
+
ownsTwin: "veris_owns_twin",
|
|
885
|
+
canaryHost: "veris_canary_host",
|
|
886
|
+
/** Unique to ONE create() call, and the only way back to a sandbox whose
|
|
887
|
+
* create threw before it could hand the object over — see reapFailedCreate.
|
|
888
|
+
* The twin id will not do: an attached twin is shared by every sandbox on
|
|
889
|
+
* it, and deleting the wrong one of those would be worse than the leak. */
|
|
890
|
+
createId: "veris_create_id"
|
|
891
|
+
};
|
|
892
|
+
var VERIS_LABEL_KEYS = Object.values(LABEL);
|
|
893
|
+
function isVerisSandbox(sbx) {
|
|
894
|
+
return typeof sbx.verisSandboxId === "string";
|
|
895
|
+
}
|
|
896
|
+
function verisTwinId(sandbox) {
|
|
897
|
+
return sandbox.labels?.[LABEL.twinId];
|
|
898
|
+
}
|
|
899
|
+
function verisOwnsTwin(sandbox) {
|
|
900
|
+
const labels = sandbox.labels ?? {};
|
|
901
|
+
return labels[LABEL.twinId] !== void 0 && labels[LABEL.ownsTwin] !== "false";
|
|
902
|
+
}
|
|
903
|
+
var Daytona = class extends BaseDaytona {
|
|
904
|
+
verisDefaults;
|
|
905
|
+
constructor(config) {
|
|
906
|
+
super(config);
|
|
907
|
+
this.verisDefaults = config?.veris ?? {};
|
|
908
|
+
}
|
|
909
|
+
async create(params, options) {
|
|
910
|
+
const v = { ...this.verisDefaults, ...params?.veris ?? {} };
|
|
911
|
+
const rest = stripVeris(params);
|
|
912
|
+
if (v.disabled) return this.baseCreate(rest, options);
|
|
913
|
+
const coords = resolveCoordinates(v);
|
|
914
|
+
const controlPlane = new ControlPlane({
|
|
915
|
+
apiKey: coords.apiKey,
|
|
916
|
+
apiBase: coords.apiBase,
|
|
917
|
+
sdkVersion: SDK_VERSION
|
|
918
|
+
});
|
|
919
|
+
const egress = v.egress ?? "strict";
|
|
920
|
+
const ttlMinutes = v.ttlMinutes ?? 60;
|
|
921
|
+
const ownsTwin = !v.attachSandboxId;
|
|
922
|
+
const createId = crypto.randomUUID();
|
|
923
|
+
const twin = await this.provisionTwin(controlPlane, v, coords, ttlMinutes);
|
|
924
|
+
const cleanupTwin = async () => {
|
|
925
|
+
if (ownsTwin) await controlPlane.deleteTwin(twin.environment_id, twin.id).catch(() => {
|
|
926
|
+
});
|
|
927
|
+
};
|
|
928
|
+
let sandbox;
|
|
929
|
+
let credential;
|
|
930
|
+
let watermarks = {};
|
|
931
|
+
let daytonaAsked = false;
|
|
932
|
+
try {
|
|
933
|
+
credential = await controlPlane.mintEgressCredential(twin.environment_id, twin.id);
|
|
934
|
+
if (!credential) {
|
|
935
|
+
throw new VerisGatewayNotOfferedError(
|
|
936
|
+
"this Veris control plane does not offer egress credentials, so there is no gateway for the sandbox to route through",
|
|
937
|
+
{ phase: "credential-mint", verisSandboxId: twin.id }
|
|
938
|
+
);
|
|
939
|
+
}
|
|
940
|
+
if (!credential.connect_address && !credential.http_proxy_url) {
|
|
941
|
+
throw new VerisGatewayNotOfferedError(
|
|
942
|
+
'the Veris gateway offers SOCKS5 but no HTTP CONNECT endpoint, and Daytona accepts only http/https outbound proxies ("Unsupported outbound proxy scheme"). Upgrade the control plane to one that returns connect_address.',
|
|
943
|
+
{ phase: "credential-mint", verisSandboxId: twin.id }
|
|
944
|
+
);
|
|
945
|
+
}
|
|
946
|
+
const services = twin.services?.length ? twin.services : await controlPlane.services(twin.id);
|
|
947
|
+
const proxyUrl = gatewayProxyUrl(credential);
|
|
948
|
+
const network = buildNetwork({ mode: egress, gatewayIps: await gatewayIps(credential, proxyUrl) });
|
|
949
|
+
watermarks = await readWatermarks(services);
|
|
950
|
+
const verisManaged = {
|
|
951
|
+
...v.installCa !== false ? sanitizeTrustEnv(void 0) : {},
|
|
952
|
+
// Node reads none of the variables above once Daytona has overwritten
|
|
953
|
+
// them; the flag makes it read OpenSSL's store, which the CA install
|
|
954
|
+
// fills. Appended, not replaced: a caller's own NODE_OPTIONS keep working.
|
|
955
|
+
...v.installCa !== false ? { NODE_OPTIONS: nodeOptionsWithTrust(rest.envVars?.NODE_OPTIONS) } : {},
|
|
956
|
+
// Node ignores HTTPS_PROXY unless told to, and Daytona blocks anything
|
|
957
|
+
// that dials out directly — so without this every plain `https.get`
|
|
958
|
+
// and `fetch` in Node 24+ dies with ECONNRESET instead of reaching
|
|
959
|
+
// the gateway. Older Node needs a proxy agent; the variable is inert there.
|
|
960
|
+
NODE_USE_ENV_PROXY: "1",
|
|
961
|
+
...v.dataPlaneEnv !== false ? dataPlaneEnv(services) : {},
|
|
962
|
+
VERIS_SANDBOX_ID: twin.id
|
|
963
|
+
};
|
|
964
|
+
const createParams = {
|
|
965
|
+
...rest,
|
|
966
|
+
envVars: { ...rest.envVars ?? {}, ...verisManaged },
|
|
967
|
+
labels: {
|
|
968
|
+
...reserveLabels(rest.labels),
|
|
969
|
+
[LABEL.twinId]: twin.id,
|
|
970
|
+
[LABEL.envId]: twin.environment_id,
|
|
971
|
+
[LABEL.apiBase]: coords.apiBase,
|
|
972
|
+
[LABEL.egress]: egress,
|
|
973
|
+
[LABEL.ownsTwin]: String(ownsTwin),
|
|
974
|
+
[LABEL.mode]: "gateway",
|
|
975
|
+
[LABEL.canaryHost]: credential.canary_host,
|
|
976
|
+
[LABEL.createId]: createId
|
|
977
|
+
},
|
|
978
|
+
...network.params,
|
|
979
|
+
// 3. Where Daytona forwards everything. Chained, not advisory: an
|
|
980
|
+
// unreachable gateway makes traffic 502 rather than quietly going
|
|
981
|
+
// direct.
|
|
982
|
+
outboundProxyUrl: proxyUrl,
|
|
983
|
+
ttlMinutes: rest.ttlMinutes ?? ttlMinutes
|
|
984
|
+
};
|
|
985
|
+
daytonaAsked = true;
|
|
986
|
+
sandbox = await this.baseCreate(createParams, options);
|
|
987
|
+
} catch (cause) {
|
|
988
|
+
const failed = daytonaAsked ? await reapFailedCreate((labels) => super.list({ labels }), createId) : void 0;
|
|
989
|
+
await cleanupTwin();
|
|
990
|
+
if (cause instanceof VerisError) throw cause;
|
|
991
|
+
throw new VerisError(sandboxCreateMessage(cause, failed, rest), {
|
|
992
|
+
phase: "sandbox-create",
|
|
993
|
+
verisSandboxId: twin.id,
|
|
994
|
+
cause
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
try {
|
|
998
|
+
await installCa(sandbox, credential.ca_pem);
|
|
999
|
+
await probeCanary(sandbox, credential.canary_host, twin.id);
|
|
1000
|
+
} catch (err) {
|
|
1001
|
+
await sandbox.delete().catch(() => {
|
|
1002
|
+
});
|
|
1003
|
+
await cleanupTwin();
|
|
1004
|
+
throw err;
|
|
1005
|
+
}
|
|
1006
|
+
return this.attach(sandbox, {
|
|
1007
|
+
controlPlane,
|
|
1008
|
+
environmentId: twin.environment_id,
|
|
1009
|
+
twinId: twin.id,
|
|
1010
|
+
egress,
|
|
1011
|
+
ownsTwin,
|
|
1012
|
+
canaryHost: credential.canary_host,
|
|
1013
|
+
watermarks
|
|
1014
|
+
});
|
|
1015
|
+
}
|
|
1016
|
+
/**
|
|
1017
|
+
* Rehydrate the Veris surface on an existing sandbox.
|
|
1018
|
+
*
|
|
1019
|
+
* Not an optimisation — a necessity. The OpenCode plugin reconnects to a
|
|
1020
|
+
* sandbox with get() on every resumed session and deletes through get() too,
|
|
1021
|
+
* so a get() that returned a bare Sandbox would mean no receipts after any
|
|
1022
|
+
* restart and a leaked twin on every delete.
|
|
1023
|
+
*/
|
|
1024
|
+
async get(sandboxIdOrName) {
|
|
1025
|
+
const sandbox = await super.get(sandboxIdOrName);
|
|
1026
|
+
return this.rehydrate(sandbox);
|
|
1027
|
+
}
|
|
1028
|
+
/** Same rehydration for the sandboxes a list() streams. */
|
|
1029
|
+
list(query) {
|
|
1030
|
+
const inner = super.list(query);
|
|
1031
|
+
const rehydrate = (s) => this.rehydrate(s);
|
|
1032
|
+
return (async function* () {
|
|
1033
|
+
for await (const sandbox of inner) yield rehydrate(sandbox);
|
|
1034
|
+
})();
|
|
1035
|
+
}
|
|
1036
|
+
/**
|
|
1037
|
+
* Attach the Veris surface to a sandbox whose labels say it has a twin.
|
|
1038
|
+
* A sandbox without our labels is passed through untouched — callers can use
|
|
1039
|
+
* this client for ordinary Daytona work.
|
|
1040
|
+
*/
|
|
1041
|
+
rehydrate(sandbox) {
|
|
1042
|
+
const labels = sandbox.labels ?? {};
|
|
1043
|
+
const twinId = labels[LABEL.twinId];
|
|
1044
|
+
if (!twinId) return sandbox;
|
|
1045
|
+
const creds = resolveVerisCredentials({ apiKey: this.verisDefaults.apiKey, apiBase: this.verisDefaults.apiBase });
|
|
1046
|
+
const apiKey = creds.apiKey;
|
|
1047
|
+
if (!apiKey) return sandbox;
|
|
1048
|
+
const trustedBase = creds.baseSource === "default" ? void 0 : creds.apiBase;
|
|
1049
|
+
const labelBase = labels[LABEL.apiBase];
|
|
1050
|
+
if (trustedBase && labelBase && labelBase !== trustedBase) {
|
|
1051
|
+
throw new VerisError(
|
|
1052
|
+
`sandbox ${sandbox.id} labels name a different Veris control plane (${labelBase}) than your configuration (${trustedBase}) \u2014 refusing to send the API key to an unverified host`,
|
|
1053
|
+
{ phase: "attach" }
|
|
1054
|
+
);
|
|
1055
|
+
}
|
|
1056
|
+
const apiBase = trustedBase ?? labelBase ?? "https://svc.api.veris.ai";
|
|
1057
|
+
return this.attach(sandbox, {
|
|
1058
|
+
controlPlane: new ControlPlane({ apiKey, apiBase, sdkVersion: SDK_VERSION }),
|
|
1059
|
+
environmentId: labels[LABEL.envId] ?? "",
|
|
1060
|
+
twinId,
|
|
1061
|
+
// Re-minted below when a receipt is actually asked for; the label only
|
|
1062
|
+
// has to survive the reconnect.
|
|
1063
|
+
canaryHost: labels[LABEL.canaryHost] ?? "",
|
|
1064
|
+
egress: labels[LABEL.egress] ?? "strict",
|
|
1065
|
+
ownsTwin: labels[LABEL.ownsTwin] !== "false"
|
|
1066
|
+
});
|
|
1067
|
+
}
|
|
1068
|
+
async provisionTwin(controlPlane, v, coords, ttlMinutes) {
|
|
1069
|
+
if (v.attachSandboxId) {
|
|
1070
|
+
const existing = await controlPlane.getTwin(v.attachSandboxId);
|
|
1071
|
+
if (!existing) {
|
|
1072
|
+
throw new VerisError(`attach target ${v.attachSandboxId} not found`, {
|
|
1073
|
+
phase: "twin-provision",
|
|
1074
|
+
verisSandboxId: v.attachSandboxId
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
1077
|
+
return existing.status === "ready" ? existing : controlPlane.waitReady(v.attachSandboxId, 24e4);
|
|
1078
|
+
}
|
|
1079
|
+
if (!coords.environmentId) {
|
|
1080
|
+
throw new MissingCredentialsError(
|
|
1081
|
+
"no Veris environment: set VERIS_ENVIRONMENT_ID, or pass veris.environmentId",
|
|
1082
|
+
{ phase: "credentials" }
|
|
1083
|
+
);
|
|
1084
|
+
}
|
|
1085
|
+
const created = await controlPlane.createTwin(coords.environmentId, { ttlMinutes });
|
|
1086
|
+
try {
|
|
1087
|
+
return await controlPlane.waitReady(created.id, 24e4);
|
|
1088
|
+
} catch (e) {
|
|
1089
|
+
await controlPlane.deleteTwin(coords.environmentId, created.id).catch(() => {
|
|
1090
|
+
});
|
|
1091
|
+
throw e;
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
/**
|
|
1095
|
+
* Hang the Veris surface off the instance, and wrap delete() so teardown is
|
|
1096
|
+
* automatic.
|
|
1097
|
+
*
|
|
1098
|
+
* Wrapping rather than asking callers to remember is the point: the OpenCode
|
|
1099
|
+
* plugin's existing `sandbox.delete()` then removes the twin too, with no
|
|
1100
|
+
* change to the plugin at all. A twin outlives its sandbox otherwise, until
|
|
1101
|
+
* its TTL reaps it — invisible until the bill arrives.
|
|
1102
|
+
*/
|
|
1103
|
+
attach(sandbox, ctx) {
|
|
1104
|
+
if (isVerisSandbox(sandbox)) return sandbox;
|
|
1105
|
+
const veris = new VerisApiImpl({ ...ctx, sandbox });
|
|
1106
|
+
const originalDelete = sandbox.delete.bind(sandbox);
|
|
1107
|
+
Object.defineProperties(sandbox, {
|
|
1108
|
+
veris: { value: veris, enumerable: true, configurable: true },
|
|
1109
|
+
verisSandboxId: { value: ctx.twinId, enumerable: true, configurable: true },
|
|
1110
|
+
delete: {
|
|
1111
|
+
configurable: true,
|
|
1112
|
+
value: async (timeout, wait) => {
|
|
1113
|
+
if (ctx.ownsTwin) {
|
|
1114
|
+
await ctx.controlPlane.deleteTwin(ctx.environmentId, ctx.twinId).catch(() => {
|
|
1115
|
+
});
|
|
1116
|
+
}
|
|
1117
|
+
return originalDelete(timeout, wait);
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
});
|
|
1121
|
+
return sandbox;
|
|
1122
|
+
}
|
|
1123
|
+
/** super.create through the overload the params actually match. */
|
|
1124
|
+
baseCreate(params, options) {
|
|
1125
|
+
return super.create.call(this, params, options);
|
|
1126
|
+
}
|
|
1127
|
+
};
|
|
1128
|
+
function resolveCoordinates(v) {
|
|
1129
|
+
const creds = requireVerisCredentials({ apiKey: v.apiKey, apiBase: v.apiBase });
|
|
1130
|
+
return {
|
|
1131
|
+
apiKey: creds.apiKey,
|
|
1132
|
+
environmentId: v.environmentId ?? process.env.VERIS_ENVIRONMENT_ID,
|
|
1133
|
+
apiBase: creds.apiBase
|
|
1134
|
+
};
|
|
1135
|
+
}
|
|
1136
|
+
async function reapFailedCreate(list, createId) {
|
|
1137
|
+
try {
|
|
1138
|
+
for await (const sandbox of list({ [LABEL.createId]: createId })) {
|
|
1139
|
+
if (sandbox.labels?.[LABEL.createId] !== createId) continue;
|
|
1140
|
+
if (!sandbox.errorReason) await sandbox.refreshData().catch(() => {
|
|
1141
|
+
});
|
|
1142
|
+
const found = { id: sandbox.id, state: sandbox.state, errorReason: sandbox.errorReason };
|
|
1143
|
+
return { ...found, deleted: await sandbox.delete().then(() => true, () => false) };
|
|
1144
|
+
}
|
|
1145
|
+
} catch {
|
|
1146
|
+
}
|
|
1147
|
+
return void 0;
|
|
1148
|
+
}
|
|
1149
|
+
function sandboxCreateMessage(cause, failed, params = {}) {
|
|
1150
|
+
const base = "Daytona sandbox create failed";
|
|
1151
|
+
if (!failed) return base;
|
|
1152
|
+
const reason = failed.errorReason?.trim().replace(/[.\s]+$/, "");
|
|
1153
|
+
const parts = [reason ? `${base}: ${reason}.` : `${base}.`];
|
|
1154
|
+
const image = typeof params.image === "string" ? params.image : void 0;
|
|
1155
|
+
if (image && failed.state === "build_failed") {
|
|
1156
|
+
parts.push(
|
|
1157
|
+
`Daytona builds "${image}" on its own servers and pulls it from a registry, so an image that exists only in your local Docker daemon is not one it can see.`
|
|
1158
|
+
);
|
|
1159
|
+
}
|
|
1160
|
+
parts.push(failed.deleted ? `The half-built sandbox ${failed.id} was deleted.` : `The half-built sandbox ${failed.id} could not be deleted \u2014 remove it with \`veris-daytona teardown ${failed.id}\`.`);
|
|
1161
|
+
const raw = cause instanceof Error ? cause.message : "";
|
|
1162
|
+
if (raw) parts.push(`(Daytona SDK: ${raw})`);
|
|
1163
|
+
return parts.join(" ");
|
|
1164
|
+
}
|
|
1165
|
+
async function readWatermarks(services) {
|
|
1166
|
+
const marks = await Promise.all(
|
|
1167
|
+
services.filter((s) => isHttpUrl(s.control_url)).map(async (svc) => [svc.name, await fetchWatermark(svc).catch(() => 0)])
|
|
1168
|
+
);
|
|
1169
|
+
return Object.fromEntries(marks);
|
|
1170
|
+
}
|
|
1171
|
+
function stripVeris(params) {
|
|
1172
|
+
const { veris: _veris, ...rest } = params ?? {};
|
|
1173
|
+
return rest;
|
|
1174
|
+
}
|
|
1175
|
+
function reserveLabels(labels) {
|
|
1176
|
+
const out2 = {};
|
|
1177
|
+
for (const [k, val] of Object.entries(labels ?? {})) {
|
|
1178
|
+
if (!VERIS_LABEL_KEYS.includes(k)) out2[k] = val;
|
|
1179
|
+
}
|
|
1180
|
+
return out2;
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
// src/run.ts
|
|
1184
|
+
var DEFAULT_TIMEOUT_SECONDS = 1800;
|
|
1185
|
+
var USAGE = `usage: veris-daytona run [options] -- <command>
|
|
1186
|
+
|
|
1187
|
+
Runs <command> in a Daytona sandbox whose vendor API calls are answered by a
|
|
1188
|
+
Veris twin, then prints what the twin received.
|
|
1189
|
+
|
|
1190
|
+
--repo <url> git URL to clone into the sandbox (default: upload the current directory)
|
|
1191
|
+
--ref <branch> branch to clone
|
|
1192
|
+
--environment <id> Veris environment (default: $VERIS_ENVIRONMENT_ID)
|
|
1193
|
+
--sandbox <twin-id> attach to an existing twin instead of creating one
|
|
1194
|
+
--image <name> Daytona image to run in (default: Daytona's default snapshot)
|
|
1195
|
+
--snapshot <name> Daytona snapshot to run in
|
|
1196
|
+
--setup <cmd> shell command run first, e.g. 'npm ci' or 'pip install -e .'
|
|
1197
|
+
--require-service <name> the receipt must show this service (repeatable; default: any)
|
|
1198
|
+
--env KEY=VALUE exported to the setup and main commands (repeatable)
|
|
1199
|
+
--timeout <seconds> how long <command> may run (default: ${DEFAULT_TIMEOUT_SECONDS})
|
|
1200
|
+
--keep leave the sandbox and twin running afterwards
|
|
1201
|
+
|
|
1202
|
+
needs: DAYTONA_API_KEY (with delete:sandboxes, or the sandbox outlives the run),
|
|
1203
|
+
a Veris key \u2014 VERIS_API_KEY, or the profile \`veris login\` saved in
|
|
1204
|
+
~/.veris/twin.yaml (VERIS_PROFILE picks one) \u2014 and VERIS_ENVIRONMENT_ID (or
|
|
1205
|
+
--environment). GITHUB_TOKEN or GH_TOKEN is used for a private --repo.
|
|
1206
|
+
|
|
1207
|
+
exit code: the command's; 1 if the twin received nothing (a pass without a receipt is not a pass).`;
|
|
1208
|
+
var UsageError = class extends Error {
|
|
1209
|
+
};
|
|
1210
|
+
function parseRunArgs(argv) {
|
|
1211
|
+
const opts = {
|
|
1212
|
+
requireService: [],
|
|
1213
|
+
env: {},
|
|
1214
|
+
keep: false,
|
|
1215
|
+
timeoutSeconds: DEFAULT_TIMEOUT_SECONDS,
|
|
1216
|
+
command: ""
|
|
1217
|
+
};
|
|
1218
|
+
const sep = argv.indexOf("--");
|
|
1219
|
+
const flags = sep === -1 ? [...argv] : argv.slice(0, sep);
|
|
1220
|
+
const command = sep === -1 ? [] : argv.slice(sep + 1);
|
|
1221
|
+
const takeValue = (i, flag) => {
|
|
1222
|
+
const v = flags[i + 1];
|
|
1223
|
+
if (v === void 0 || v.startsWith("--")) throw new UsageError(`${flag} needs a value`);
|
|
1224
|
+
return v;
|
|
1225
|
+
};
|
|
1226
|
+
for (let i = 0; i < flags.length; i++) {
|
|
1227
|
+
const flag = flags[i];
|
|
1228
|
+
switch (flag) {
|
|
1229
|
+
case "--repo":
|
|
1230
|
+
opts.repo = takeValue(i++, flag);
|
|
1231
|
+
break;
|
|
1232
|
+
case "--ref":
|
|
1233
|
+
opts.ref = takeValue(i++, flag);
|
|
1234
|
+
break;
|
|
1235
|
+
case "--environment":
|
|
1236
|
+
opts.environment = takeValue(i++, flag);
|
|
1237
|
+
break;
|
|
1238
|
+
case "--sandbox":
|
|
1239
|
+
opts.sandbox = takeValue(i++, flag);
|
|
1240
|
+
break;
|
|
1241
|
+
case "--image":
|
|
1242
|
+
opts.image = takeValue(i++, flag);
|
|
1243
|
+
break;
|
|
1244
|
+
case "--snapshot":
|
|
1245
|
+
opts.snapshot = takeValue(i++, flag);
|
|
1246
|
+
break;
|
|
1247
|
+
case "--setup":
|
|
1248
|
+
opts.setup = takeValue(i++, flag);
|
|
1249
|
+
break;
|
|
1250
|
+
case "--require-service":
|
|
1251
|
+
opts.requireService.push(takeValue(i++, flag));
|
|
1252
|
+
break;
|
|
1253
|
+
case "--env": {
|
|
1254
|
+
const pair = takeValue(i++, flag);
|
|
1255
|
+
const eq = pair.indexOf("=");
|
|
1256
|
+
if (eq <= 0) throw new UsageError(`--env wants KEY=VALUE, got '${pair}'`);
|
|
1257
|
+
opts.env[pair.slice(0, eq)] = pair.slice(eq + 1);
|
|
1258
|
+
break;
|
|
1259
|
+
}
|
|
1260
|
+
case "--timeout": {
|
|
1261
|
+
const n = Number(takeValue(i++, flag));
|
|
1262
|
+
if (!Number.isFinite(n) || n <= 0) throw new UsageError("--timeout wants a positive number of seconds");
|
|
1263
|
+
opts.timeoutSeconds = n;
|
|
1264
|
+
break;
|
|
1265
|
+
}
|
|
1266
|
+
case "--keep":
|
|
1267
|
+
opts.keep = true;
|
|
1268
|
+
break;
|
|
1269
|
+
case "-h":
|
|
1270
|
+
case "--help":
|
|
1271
|
+
throw new UsageError(USAGE);
|
|
1272
|
+
default:
|
|
1273
|
+
throw new UsageError(`unknown option '${flag}'
|
|
1274
|
+
|
|
1275
|
+
${USAGE}`);
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
if (opts.image && opts.snapshot) throw new UsageError("--image and --snapshot are exclusive");
|
|
1279
|
+
if (command.length === 0) throw new UsageError(`no command given: put it after '--'
|
|
1280
|
+
|
|
1281
|
+
${USAGE}`);
|
|
1282
|
+
opts.command = shellJoin(command);
|
|
1283
|
+
return opts;
|
|
1284
|
+
}
|
|
1285
|
+
function shellJoin(words) {
|
|
1286
|
+
return words.map((w) => /^[A-Za-z0-9_./:=@%+,-]+$/.test(w) ? w : `'${w.replace(/'/g, `'\\''`)}'`).join(" ");
|
|
1287
|
+
}
|
|
1288
|
+
function verdict(commandExitCode, receipt, requireService) {
|
|
1289
|
+
const problems = [];
|
|
1290
|
+
const seen = (name) => (receipt.services[name]?.requests ?? 0) > 0;
|
|
1291
|
+
const total = Object.values(receipt.services).reduce((n, e) => n + (e?.requests ?? 0), 0);
|
|
1292
|
+
if (requireService.length > 0) {
|
|
1293
|
+
for (const name of requireService) {
|
|
1294
|
+
if (!(name in receipt.services)) problems.push(`the twin has no service named '${name}'`);
|
|
1295
|
+
else if (!seen(name)) problems.push(`'${name}' received ZERO requests \u2014 the code under test never called it`);
|
|
1296
|
+
}
|
|
1297
|
+
} else if (total === 0) {
|
|
1298
|
+
problems.push("the twin received ZERO requests \u2014 the code under test never called any vendor");
|
|
1299
|
+
}
|
|
1300
|
+
if (commandExitCode !== 0) return { exitCode: commandExitCode, problems };
|
|
1301
|
+
return { exitCode: problems.length ? 1 : 0, problems };
|
|
1302
|
+
}
|
|
1303
|
+
function formatReceipt(receipt, twinId) {
|
|
1304
|
+
const lines = [`Veris receipt \u2014 twin ${twinId}`, ` interception: ${receipt.mode} integrity: ${receipt.integrity}`];
|
|
1305
|
+
if (receipt.leaks.length) lines.push(` blind spots: ${receipt.leaks.join(", ")}`);
|
|
1306
|
+
const entries = Object.entries(receipt.services);
|
|
1307
|
+
const total = entries.reduce((n, [, e]) => n + (e?.requests ?? 0), 0);
|
|
1308
|
+
const floor = (capped) => capped ? "\u2265" : "";
|
|
1309
|
+
lines.push("", `${floor(entries.some(([, e]) => e?.capped))}${total} request(s) reached the twin:`);
|
|
1310
|
+
for (const [name, entry] of entries) {
|
|
1311
|
+
lines.push(` ${name}: ${floor(entry?.capped)}${entry?.requests ?? 0} request(s)`);
|
|
1312
|
+
for (const r of (entry?.entries ?? []).slice(0, 20)) {
|
|
1313
|
+
lines.push(` ${r.method} ${r.path} -> ${r.status ?? "no response"}`);
|
|
1314
|
+
}
|
|
1315
|
+
if ((entry?.entries.length ?? 0) > 20) lines.push(` \u2026 ${entry.entries.length - 20} more`);
|
|
1316
|
+
}
|
|
1317
|
+
return lines.join("\n");
|
|
1318
|
+
}
|
|
1319
|
+
var WORK_SUBDIR = "veris-run";
|
|
1320
|
+
var UPLOAD_EXCLUDES = [
|
|
1321
|
+
".git",
|
|
1322
|
+
"node_modules",
|
|
1323
|
+
".venv",
|
|
1324
|
+
"venv",
|
|
1325
|
+
"__pycache__",
|
|
1326
|
+
".pytest_cache",
|
|
1327
|
+
".mypy_cache",
|
|
1328
|
+
".ruff_cache",
|
|
1329
|
+
"dist",
|
|
1330
|
+
"build",
|
|
1331
|
+
"target",
|
|
1332
|
+
".next",
|
|
1333
|
+
".turbo",
|
|
1334
|
+
"coverage",
|
|
1335
|
+
".DS_Store"
|
|
1336
|
+
];
|
|
1337
|
+
var TIMED_OUT_EXIT = 124;
|
|
1338
|
+
function commandLine(cwd, env, command, timeoutSeconds) {
|
|
1339
|
+
const exports = Object.entries(env).map(([k, v]) => `export ${k}=${shellQuote2(v)};`).join(" ");
|
|
1340
|
+
const q = shellQuote2(command);
|
|
1341
|
+
return `cd ${shellQuote2(cwd)} && ${exports} if command -v timeout >/dev/null 2>&1; then timeout -k 10 ${Math.ceil(timeoutSeconds)} sh -c ${q}; else sh -c ${q}; fi`;
|
|
1342
|
+
}
|
|
1343
|
+
function shellQuote2(s) {
|
|
1344
|
+
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
1345
|
+
}
|
|
1346
|
+
function commandEnv(trust, user) {
|
|
1347
|
+
return { ...trust, ...user };
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
// src/push.ts
|
|
1351
|
+
function wrapped(words, width = 74) {
|
|
1352
|
+
const lines = [];
|
|
1353
|
+
for (const w of words) {
|
|
1354
|
+
const last = lines.at(-1);
|
|
1355
|
+
if (last !== void 0 && `${last}, ${w}`.length <= width) lines[lines.length - 1] = `${last}, ${w}`;
|
|
1356
|
+
else lines.push(w);
|
|
1357
|
+
}
|
|
1358
|
+
return lines.map((l) => ` ${l}`).join("\n");
|
|
1359
|
+
}
|
|
1360
|
+
var PUSH_USAGE = `usage: veris-daytona push <daytona-sandbox-id> [options]
|
|
1361
|
+
|
|
1362
|
+
Puts code into a sandbox that already exists \u2014 the id \`veris-daytona provision\`
|
|
1363
|
+
printed as daytonaSandboxId, not the Veris twin's. By default it tars the
|
|
1364
|
+
current directory and unpacks it in the sandbox; --repo clones instead.
|
|
1365
|
+
|
|
1366
|
+
--repo <url> git URL to clone into the sandbox (default: upload the current directory)
|
|
1367
|
+
--ref <branch> branch to clone
|
|
1368
|
+
|
|
1369
|
+
It lands in <sandbox home>/${WORK_SUBDIR} \u2014 the same path \`provision\` printed as
|
|
1370
|
+
workDir, so the two chain without carrying it between them. The upload leaves
|
|
1371
|
+
out what is rebuilt inside or is not source:
|
|
1372
|
+
|
|
1373
|
+
${wrapped(UPLOAD_EXCLUDES)}
|
|
1374
|
+
|
|
1375
|
+
Files of the same name are overwritten; anything else already in the directory
|
|
1376
|
+
is left alone.
|
|
1377
|
+
|
|
1378
|
+
This verb exists because the Daytona CLI has no upload, copy or sync command,
|
|
1379
|
+
its \`ssh\` takes no remote command, and \`--context\` is a build context on
|
|
1380
|
+
\`create\` only.
|
|
1381
|
+
|
|
1382
|
+
needs: DAYTONA_API_KEY. GITHUB_TOKEN or GH_TOKEN is used for a private --repo.
|
|
1383
|
+
A clone runs inside the sandbox and reaches the git host through the Veris
|
|
1384
|
+
gateway, which passes public hosts through.
|
|
1385
|
+
|
|
1386
|
+
exit code: 0 when the code is in the sandbox; 1 when there is no such sandbox.`;
|
|
1387
|
+
function parsePushArgs(argv) {
|
|
1388
|
+
const opts = { sandboxId: "" };
|
|
1389
|
+
const ids = [];
|
|
1390
|
+
const takeValue = (i, flag) => {
|
|
1391
|
+
const v = argv[i + 1];
|
|
1392
|
+
if (v === void 0 || v.startsWith("--")) throw new UsageError(`${flag} needs a value`);
|
|
1393
|
+
return v;
|
|
1394
|
+
};
|
|
1395
|
+
for (let i = 0; i < argv.length; i++) {
|
|
1396
|
+
const arg = argv[i];
|
|
1397
|
+
switch (arg) {
|
|
1398
|
+
case "--repo":
|
|
1399
|
+
opts.repo = takeValue(i++, arg);
|
|
1400
|
+
break;
|
|
1401
|
+
case "--ref":
|
|
1402
|
+
opts.ref = takeValue(i++, arg);
|
|
1403
|
+
break;
|
|
1404
|
+
case "-h":
|
|
1405
|
+
case "--help":
|
|
1406
|
+
throw new UsageError(PUSH_USAGE);
|
|
1407
|
+
default:
|
|
1408
|
+
if (arg.startsWith("-")) throw new UsageError(`unknown option '${arg}'
|
|
1409
|
+
|
|
1410
|
+
${PUSH_USAGE}`);
|
|
1411
|
+
ids.push(arg);
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
if (ids.length === 0) throw new UsageError(`no sandbox id given
|
|
1415
|
+
|
|
1416
|
+
${PUSH_USAGE}`);
|
|
1417
|
+
if (ids.length > 1) throw new UsageError(`push takes one sandbox id, got ${ids.length}: ${ids.join(" ")}`);
|
|
1418
|
+
if (opts.ref && !opts.repo) throw new UsageError("--ref only means something with --repo; an upload has no branch");
|
|
1419
|
+
opts.sandboxId = ids[0];
|
|
1420
|
+
return opts;
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
// src/exec.ts
|
|
1424
|
+
var EXEC_USAGE = `usage: veris-daytona exec <daytona-sandbox-id> [options] -- <command>
|
|
1425
|
+
|
|
1426
|
+
Runs <command> in a sandbox that already exists \u2014 the id \`veris-daytona
|
|
1427
|
+
provision\` printed as daytonaSandboxId \u2014 with the Veris trust environment
|
|
1428
|
+
applied, and streams the output as it happens.
|
|
1429
|
+
|
|
1430
|
+
--cwd <dir> directory to run in (default: <sandbox home>/${WORK_SUBDIR}, provision's workDir)
|
|
1431
|
+
--env KEY=VALUE exported on top of the trust environment (repeatable)
|
|
1432
|
+
--timeout <seconds> how long <command> may run (default: ${DEFAULT_TIMEOUT_SECONDS})
|
|
1433
|
+
|
|
1434
|
+
The trust environment is applied for you, and that is the point of the verb:
|
|
1435
|
+
Daytona overwrites SSL_CERT_FILE, REQUESTS_CA_BUNDLE, CURL_CA_BUNDLE and
|
|
1436
|
+
NODE_EXTRA_CA_CERTS inside the sandbox with its own CA file, which cannot verify
|
|
1437
|
+
the twin gateway's certificates, and \`daytona exec\` has no way to set a variable
|
|
1438
|
+
at all. Every command here is run with the Veris values exported in front of it.
|
|
1439
|
+
--env is applied after them and wins.
|
|
1440
|
+
|
|
1441
|
+
Output streams rather than arriving at the end, so a long install or suite shows
|
|
1442
|
+
its progress. No receipt is read and no verdict is passed \u2014 take a watermark
|
|
1443
|
+
before and read \`veris sandbox trace --since\` after.
|
|
1444
|
+
|
|
1445
|
+
needs: DAYTONA_API_KEY.
|
|
1446
|
+
|
|
1447
|
+
exit code: the command's own. 124 when it ran out of --timeout; 1 when there is
|
|
1448
|
+
no such sandbox; 2 on a usage error.`;
|
|
1449
|
+
function parseExecArgs(argv) {
|
|
1450
|
+
const opts = { sandboxId: "", env: {}, timeoutSeconds: DEFAULT_TIMEOUT_SECONDS, command: "" };
|
|
1451
|
+
const sep = argv.indexOf("--");
|
|
1452
|
+
const flags = sep === -1 ? [...argv] : argv.slice(0, sep);
|
|
1453
|
+
const command = sep === -1 ? [] : argv.slice(sep + 1);
|
|
1454
|
+
const ids = [];
|
|
1455
|
+
const takeValue = (i, flag) => {
|
|
1456
|
+
const v = flags[i + 1];
|
|
1457
|
+
if (v === void 0 || v.startsWith("--")) throw new UsageError(`${flag} needs a value`);
|
|
1458
|
+
return v;
|
|
1459
|
+
};
|
|
1460
|
+
for (let i = 0; i < flags.length; i++) {
|
|
1461
|
+
const arg = flags[i];
|
|
1462
|
+
switch (arg) {
|
|
1463
|
+
case "--cwd":
|
|
1464
|
+
opts.cwd = takeValue(i++, arg);
|
|
1465
|
+
break;
|
|
1466
|
+
case "--env": {
|
|
1467
|
+
const pair = takeValue(i++, arg);
|
|
1468
|
+
const eq = pair.indexOf("=");
|
|
1469
|
+
if (eq <= 0) throw new UsageError(`--env wants KEY=VALUE, got '${pair}'`);
|
|
1470
|
+
opts.env[pair.slice(0, eq)] = pair.slice(eq + 1);
|
|
1471
|
+
break;
|
|
1472
|
+
}
|
|
1473
|
+
case "--timeout": {
|
|
1474
|
+
const n = Number(takeValue(i++, arg));
|
|
1475
|
+
if (!Number.isFinite(n) || n <= 0) throw new UsageError("--timeout wants a positive number of seconds");
|
|
1476
|
+
opts.timeoutSeconds = n;
|
|
1477
|
+
break;
|
|
1478
|
+
}
|
|
1479
|
+
case "-h":
|
|
1480
|
+
case "--help":
|
|
1481
|
+
throw new UsageError(EXEC_USAGE);
|
|
1482
|
+
default:
|
|
1483
|
+
if (arg.startsWith("-")) throw new UsageError(`unknown option '${arg}'
|
|
1484
|
+
|
|
1485
|
+
${EXEC_USAGE}`);
|
|
1486
|
+
ids.push(arg);
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
if (ids.length === 0) throw new UsageError(`no sandbox id given
|
|
1490
|
+
|
|
1491
|
+
${EXEC_USAGE}`);
|
|
1492
|
+
if (ids.length > 1) throw new UsageError(`exec takes one sandbox id, got ${ids.length}: ${ids.join(" ")}`);
|
|
1493
|
+
if (command.length === 0) throw new UsageError(`no command given: put it after '--'
|
|
1494
|
+
|
|
1495
|
+
${EXEC_USAGE}`);
|
|
1496
|
+
opts.sandboxId = ids[0];
|
|
1497
|
+
opts.command = shellJoin(command);
|
|
1498
|
+
return opts;
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
// src/provision.ts
|
|
1502
|
+
var AUTO_STOP_MINUTES = 30;
|
|
1503
|
+
var AUTO_DELETE_MINUTES = 60;
|
|
1504
|
+
var PROVISION_TTL_MINUTES = 240;
|
|
1505
|
+
var PROVISION_USAGE = `usage: veris-daytona provision --sandbox <twin-id> [options]
|
|
1506
|
+
|
|
1507
|
+
Creates a Daytona sandbox whose vendor API calls are answered by an existing
|
|
1508
|
+
Veris twin, and stops there: nothing is uploaded, nothing is run, nothing is
|
|
1509
|
+
deleted. One JSON object is printed on stdout; progress goes to stderr.
|
|
1510
|
+
|
|
1511
|
+
--sandbox <twin-id> the Veris twin to attach to (required)
|
|
1512
|
+
--image <name> Daytona image to run in (default: Daytona's default snapshot)
|
|
1513
|
+
--snapshot <name> Daytona snapshot to run in
|
|
1514
|
+
--env KEY=VALUE set as a sandbox environment variable (repeatable)
|
|
1515
|
+
|
|
1516
|
+
needs: DAYTONA_API_KEY, and a Veris key: VERIS_API_KEY, or the profile
|
|
1517
|
+
\`veris login\` saved in ~/.veris/twin.yaml (VERIS_PROFILE picks one; the
|
|
1518
|
+
environment variable wins when both exist). No VERIS_ENVIRONMENT_ID \u2014 the
|
|
1519
|
+
environment comes from the twin. A Daytona key without delete:sandboxes still
|
|
1520
|
+
provisions, with a warning that teardown will be refused.
|
|
1521
|
+
|
|
1522
|
+
the JSON on stdout:
|
|
1523
|
+
daytonaSandboxId the Daytona sandbox id; \`veris-daytona teardown\` takes it
|
|
1524
|
+
verisSandboxId the Veris twin's id \u2014 NOT the Daytona one
|
|
1525
|
+
verisEnvironmentId the environment the twin belongs to
|
|
1526
|
+
ownsTwin false: the twin is yours, and teardown leaves it alone
|
|
1527
|
+
workDir an empty directory to put code in and run commands from
|
|
1528
|
+
caBundlePath the CA bundle inside the sandbox (public roots + the Veris CA)
|
|
1529
|
+
trustEnv the CA variables, as a map \u2014 export them on EVERY command
|
|
1530
|
+
trustPrelude the same variables as one line of shell \`export\`s
|
|
1531
|
+
patchBundledCasCommand run it inside the sandbox after installing dependencies
|
|
1532
|
+
pushCommand how to get code in \u2014 the Daytona CLI cannot upload
|
|
1533
|
+
execCommand how to run one command in there, trustEnv already applied
|
|
1534
|
+
services the twin's service names
|
|
1535
|
+
expiresAt when Daytona destroys the sandbox whatever state it is in
|
|
1536
|
+
autoStopMinutes idle minutes before Daytona stops it
|
|
1537
|
+
autoDeleteMinutes minutes after stopping before Daytona deletes it
|
|
1538
|
+
|
|
1539
|
+
Getting code in: the Daytona CLI has no upload, copy or sync command, its \`ssh\`
|
|
1540
|
+
takes no remote command, and \`--context\` is a build context on \`create\` only \u2014
|
|
1541
|
+
so \`veris-daytona push <daytonaSandboxId>\` is the route. It tars the current
|
|
1542
|
+
directory into workDir. \`veris-daytona exec <daytonaSandboxId> -- <command>\`
|
|
1543
|
+
then runs a command in there with trustEnv already exported and its output
|
|
1544
|
+
streaming, neither of which \`daytona exec\` can do.
|
|
1545
|
+
|
|
1546
|
+
Data-plane variables (DATABASE_URL and the like) are already set inside the
|
|
1547
|
+
sandbox, so a command run in there inherits them.
|
|
1548
|
+
|
|
1549
|
+
The sandbox stops after ${AUTO_STOP_MINUTES} idle minutes, is deleted ${AUTO_DELETE_MINUTES} minutes after
|
|
1550
|
+
it stops, and is destroyed at ${PROVISION_TTL_MINUTES} minutes old whatever state it is in \u2014 nothing
|
|
1551
|
+
here deletes it for you. The twin's own TTL is whatever created it.
|
|
1552
|
+
|
|
1553
|
+
exit code: 0 when the sandbox is up and the canary answered; 2 otherwise.`;
|
|
1554
|
+
var RUN_ONLY = {
|
|
1555
|
+
"--repo": "provision puts no code in the sandbox \u2014 clone or upload it yourself",
|
|
1556
|
+
"--ref": "provision puts no code in the sandbox \u2014 clone or upload it yourself",
|
|
1557
|
+
"--setup": "provision runs no commands \u2014 install dependencies yourself in the sandbox it hands back",
|
|
1558
|
+
"--require-service": "provision reads no receipt, so it judges nothing \u2014 that belongs to the `veris` CLI",
|
|
1559
|
+
"--timeout": "provision runs no command, so there is nothing to time out",
|
|
1560
|
+
"--keep": "provision always keeps the sandbox; delete it with `veris-daytona teardown`",
|
|
1561
|
+
"--environment": "the environment comes from the twin named by --sandbox"
|
|
1562
|
+
};
|
|
1563
|
+
function parseProvisionArgs(argv) {
|
|
1564
|
+
const opts = { sandbox: "", env: {} };
|
|
1565
|
+
const takeValue = (i, flag) => {
|
|
1566
|
+
const v = argv[i + 1];
|
|
1567
|
+
if (v === void 0 || v.startsWith("--")) throw new UsageError(`${flag} needs a value`);
|
|
1568
|
+
return v;
|
|
1569
|
+
};
|
|
1570
|
+
for (let i = 0; i < argv.length; i++) {
|
|
1571
|
+
const flag = argv[i];
|
|
1572
|
+
if (flag === "--") {
|
|
1573
|
+
throw new UsageError(
|
|
1574
|
+
`provision runs no command, so there is nothing to put after '--'. Use \`veris-daytona run\` for that, or run your command in the sandbox provision hands back.`
|
|
1575
|
+
);
|
|
1576
|
+
}
|
|
1577
|
+
if (flag in RUN_ONLY) throw new UsageError(`${flag} is a \`run\` flag: ${RUN_ONLY[flag]}`);
|
|
1578
|
+
switch (flag) {
|
|
1579
|
+
case "--sandbox":
|
|
1580
|
+
opts.sandbox = takeValue(i++, flag);
|
|
1581
|
+
break;
|
|
1582
|
+
case "--image":
|
|
1583
|
+
opts.image = takeValue(i++, flag);
|
|
1584
|
+
break;
|
|
1585
|
+
case "--snapshot":
|
|
1586
|
+
opts.snapshot = takeValue(i++, flag);
|
|
1587
|
+
break;
|
|
1588
|
+
case "--env": {
|
|
1589
|
+
const pair = takeValue(i++, flag);
|
|
1590
|
+
const eq = pair.indexOf("=");
|
|
1591
|
+
if (eq <= 0) throw new UsageError(`--env wants KEY=VALUE, got '${pair}'`);
|
|
1592
|
+
opts.env[pair.slice(0, eq)] = pair.slice(eq + 1);
|
|
1593
|
+
break;
|
|
1594
|
+
}
|
|
1595
|
+
case "-h":
|
|
1596
|
+
case "--help":
|
|
1597
|
+
throw new UsageError(PROVISION_USAGE);
|
|
1598
|
+
default:
|
|
1599
|
+
throw new UsageError(`unknown option '${flag}'
|
|
1600
|
+
|
|
1601
|
+
${PROVISION_USAGE}`);
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
if (opts.image && opts.snapshot) throw new UsageError("--image and --snapshot are exclusive");
|
|
1605
|
+
if (!opts.sandbox) {
|
|
1606
|
+
throw new UsageError(
|
|
1607
|
+
`--sandbox is required: provision attaches to a twin that already exists (\`veris up\` prints its id)
|
|
1608
|
+
|
|
1609
|
+
${PROVISION_USAGE}`
|
|
1610
|
+
);
|
|
1611
|
+
}
|
|
1612
|
+
return opts;
|
|
1613
|
+
}
|
|
1614
|
+
function provisionResult(facts) {
|
|
1615
|
+
return {
|
|
1616
|
+
daytonaSandboxId: facts.daytonaSandboxId,
|
|
1617
|
+
verisSandboxId: facts.verisSandboxId,
|
|
1618
|
+
verisEnvironmentId: facts.verisEnvironmentId,
|
|
1619
|
+
// provision requires --sandbox, so the twin is always the caller's.
|
|
1620
|
+
ownsTwin: false,
|
|
1621
|
+
workDir: facts.workDir,
|
|
1622
|
+
caBundlePath: VERIS_BUNDLE,
|
|
1623
|
+
trustEnv: facts.trustEnv,
|
|
1624
|
+
trustPrelude: trustPrelude(facts.trustEnv),
|
|
1625
|
+
// The script is already in the sandbox — installCa uploads it at create,
|
|
1626
|
+
// precisely so that whoever installs the dependencies can patch the bundles
|
|
1627
|
+
// afterwards without holding this SDK. Naming the command here is the whole
|
|
1628
|
+
// of "how a caller patches bundled CAs": one line of shell, no third verb.
|
|
1629
|
+
patchBundledCasCommand: `sh ${BUNDLED_CA_PATCH_SCRIPT}`,
|
|
1630
|
+
// Named with the id already in them, because the gap this closes is a
|
|
1631
|
+
// caller who reads the JSON, finds workDir, and has nothing that can put a
|
|
1632
|
+
// file there — the trial wrote a Node script against @daytona/sdk to do it.
|
|
1633
|
+
pushCommand: `veris-daytona push ${facts.daytonaSandboxId}`,
|
|
1634
|
+
execCommand: `veris-daytona exec ${facts.daytonaSandboxId} -- <command>`,
|
|
1635
|
+
services: facts.services,
|
|
1636
|
+
expiresAt: facts.expiresAt,
|
|
1637
|
+
autoStopMinutes: AUTO_STOP_MINUTES,
|
|
1638
|
+
autoDeleteMinutes: AUTO_DELETE_MINUTES
|
|
1639
|
+
};
|
|
1640
|
+
}
|
|
1641
|
+
function provisionJson(result) {
|
|
1642
|
+
return JSON.stringify(result, null, 2);
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
// src/daytona-key.ts
|
|
1646
|
+
var DAYTONA_API_URL = "https://app.daytona.io/api";
|
|
1647
|
+
var DELETE_SANDBOXES = "delete:sandboxes";
|
|
1648
|
+
function daytonaApiUrl(env = process.env) {
|
|
1649
|
+
return (env.DAYTONA_API_URL || env.DAYTONA_SERVER_URL || DAYTONA_API_URL).replace(/\/$/, "");
|
|
1650
|
+
}
|
|
1651
|
+
async function fetchDaytonaKey(apiKey, apiUrl = daytonaApiUrl()) {
|
|
1652
|
+
try {
|
|
1653
|
+
const res = await fetch(`${apiUrl}/api-keys/current`, {
|
|
1654
|
+
headers: { Authorization: `Bearer ${apiKey}` }
|
|
1655
|
+
});
|
|
1656
|
+
if (!res.ok) return void 0;
|
|
1657
|
+
const body = await res.json();
|
|
1658
|
+
if (!Array.isArray(body.permissions)) return void 0;
|
|
1659
|
+
return {
|
|
1660
|
+
name: typeof body.name === "string" ? body.name : "",
|
|
1661
|
+
permissions: body.permissions.filter((p) => typeof p === "string")
|
|
1662
|
+
};
|
|
1663
|
+
} catch {
|
|
1664
|
+
return void 0;
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
function canDeleteSandboxes(key) {
|
|
1668
|
+
return key === void 0 ? void 0 : key.permissions.includes(DELETE_SANDBOXES);
|
|
1669
|
+
}
|
|
1670
|
+
function describeKey(key) {
|
|
1671
|
+
const name = key.name ? `"${key.name}" ` : "";
|
|
1672
|
+
return `${name}(permissions: ${key.permissions.join(", ") || "none"})`;
|
|
1673
|
+
}
|
|
1674
|
+
var DELETE_PERMISSION_FIX = 'create a key with the "delete sandboxes" permission at https://app.daytona.io/dashboard/keys';
|
|
1675
|
+
function cannotTeardownWarning(key, autoStopMinutes, autoDeleteMinutes) {
|
|
1676
|
+
return `DAYTONA_API_KEY ${describeKey(key)} lacks \`${DELETE_SANDBOXES}\`, so \`veris-daytona teardown\` will be refused for the sandbox about to be created, and \`run\` cannot delete its own. The box still stops after ${autoStopMinutes} idle minutes and is deleted ${autoDeleteMinutes} minutes after it stops. For a teardown that works, ${DELETE_PERMISSION_FIX} and use it instead.`;
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
// src/teardown.ts
|
|
1680
|
+
var TEARDOWN_USAGE = `usage: veris-daytona teardown <daytona-sandbox-id>
|
|
1681
|
+
|
|
1682
|
+
Deletes the Daytona sandbox. The id is the one \`veris-daytona provision\`
|
|
1683
|
+
printed as daytonaSandboxId \u2014 not the Veris twin's id.
|
|
1684
|
+
|
|
1685
|
+
The twin follows the rule it was created under: one this package created is
|
|
1686
|
+
deleted with the sandbox, one it attached to (\`provision\`, or \`run --sandbox\`)
|
|
1687
|
+
is yours and is left running. Which of the two happened is printed.
|
|
1688
|
+
|
|
1689
|
+
needs: DAYTONA_API_KEY with the ${DELETE_SANDBOXES} permission \u2014 a key made with
|
|
1690
|
+
"write sandboxes" alone is refused, and the refusal says when Daytona will stop
|
|
1691
|
+
and delete the box on its own. To reach a twin that must be deleted, a Veris
|
|
1692
|
+
key too: VERIS_API_KEY, or the profile \`veris login\` saved.
|
|
1693
|
+
|
|
1694
|
+
exit code: 0 when the sandbox is deleted; 1 when there is no such sandbox, or
|
|
1695
|
+
the key may not delete one.`;
|
|
1696
|
+
function isPermissionDenied(e) {
|
|
1697
|
+
return typeof e === "object" && e !== null && e.statusCode === 403;
|
|
1698
|
+
}
|
|
1699
|
+
function sandboxBrakes(r) {
|
|
1700
|
+
const stop = r.autoStopMinutes && r.autoStopMinutes > 0 ? `stops after ${r.autoStopMinutes} idle minutes` : "never stops on its own";
|
|
1701
|
+
const del = r.autoDeleteMinutes === void 0 || r.autoDeleteMinutes < 0 ? "is never auto-deleted" : r.autoDeleteMinutes === 0 ? "is deleted as soon as it stops" : `is deleted ${r.autoDeleteMinutes} minutes after it stops`;
|
|
1702
|
+
const destroy = r.expiresAt ? `, and is destroyed at ${r.expiresAt} whatever state it is in` : "";
|
|
1703
|
+
return `${stop} and ${del}${destroy}`;
|
|
1704
|
+
}
|
|
1705
|
+
function teardownRefusedMessage(r) {
|
|
1706
|
+
const key = r.key ? `this DAYTONA_API_KEY ${describeKey(r.key)} lacks the \`${DELETE_SANDBOXES}\` permission` : `Daytona answered 403 (Access denied), which is what a DAYTONA_API_KEY without the \`${DELETE_SANDBOXES}\` permission gets \u2014 the key's own record could not be read to confirm it`;
|
|
1707
|
+
const twin = r.twinId === void 0 ? "" : r.ownsTwin ? ` Its twin ${r.twinId} was deleted before Daytona refused, so nothing is left on the Veris side.` : ` Twin ${r.twinId} is yours and was not touched.`;
|
|
1708
|
+
return `Daytona refused to delete sandbox ${r.sandboxId}: ${key}. The sandbox is still there: it ${sandboxBrakes(r)}.${twin} To delete it sooner, ${DELETE_PERMISSION_FIX}, then run \`veris-daytona teardown ${r.sandboxId}\` with it \u2014 or delete the sandbox in the Daytona dashboard.`;
|
|
1709
|
+
}
|
|
1710
|
+
function parseTeardownArgs(argv) {
|
|
1711
|
+
const ids = [];
|
|
1712
|
+
for (const arg of argv) {
|
|
1713
|
+
if (arg === "-h" || arg === "--help") throw new UsageError(TEARDOWN_USAGE);
|
|
1714
|
+
if (arg.startsWith("-")) throw new UsageError(`unknown option '${arg}'
|
|
1715
|
+
|
|
1716
|
+
${TEARDOWN_USAGE}`);
|
|
1717
|
+
ids.push(arg);
|
|
1718
|
+
}
|
|
1719
|
+
if (ids.length === 0) throw new UsageError(`no sandbox id given
|
|
1720
|
+
|
|
1721
|
+
${TEARDOWN_USAGE}`);
|
|
1722
|
+
if (ids.length > 1) {
|
|
1723
|
+
throw new UsageError(`teardown takes one sandbox id, got ${ids.length}: ${ids.join(" ")}`);
|
|
1724
|
+
}
|
|
1725
|
+
return { sandboxId: ids[0] };
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
// src/cli.ts
|
|
1729
|
+
var out = (msg) => process.stdout.write(msg + "\n");
|
|
1730
|
+
var say = (msg) => process.stderr.write(`\x1B[1m\xBB ${msg}\x1B[0m
|
|
1731
|
+
`);
|
|
1732
|
+
var note = (msg) => process.stderr.write(` \x1B[2m${msg}\x1B[0m
|
|
1733
|
+
`);
|
|
1734
|
+
var ROOT_USAGE = `usage: veris-daytona <command> [options]
|
|
1735
|
+
|
|
1736
|
+
run run a command in a wired sandbox, print the receipt, delete everything
|
|
1737
|
+
provision create a wired sandbox on an existing twin, print its JSON, and stop
|
|
1738
|
+
push put code into a sandbox that already exists
|
|
1739
|
+
exec run one command in a sandbox, with the Veris trust environment applied
|
|
1740
|
+
teardown delete a sandbox that provision (or run --keep) left behind
|
|
1741
|
+
|
|
1742
|
+
veris-daytona <command> --help describes each one.`;
|
|
1743
|
+
var HELP_TEXTS = [ROOT_USAGE, USAGE, PROVISION_USAGE, PUSH_USAGE, EXEC_USAGE, TEARDOWN_USAGE];
|
|
1744
|
+
async function main(argv) {
|
|
1745
|
+
const [verb, ...rest] = argv;
|
|
1746
|
+
if (verb === "--version" || verb === "-v") {
|
|
1747
|
+
out(SDK_VERSION);
|
|
1748
|
+
return 0;
|
|
1749
|
+
}
|
|
1750
|
+
switch (verb) {
|
|
1751
|
+
case "run": {
|
|
1752
|
+
const opts = parseOrExit(() => parseRunArgs(rest));
|
|
1753
|
+
return typeof opts === "number" ? opts : run(opts);
|
|
1754
|
+
}
|
|
1755
|
+
case "provision": {
|
|
1756
|
+
const opts = parseOrExit(() => parseProvisionArgs(rest));
|
|
1757
|
+
return typeof opts === "number" ? opts : provision(opts);
|
|
1758
|
+
}
|
|
1759
|
+
case "push": {
|
|
1760
|
+
const opts = parseOrExit(() => parsePushArgs(rest));
|
|
1761
|
+
return typeof opts === "number" ? opts : push(opts);
|
|
1762
|
+
}
|
|
1763
|
+
case "exec": {
|
|
1764
|
+
const opts = parseOrExit(() => parseExecArgs(rest));
|
|
1765
|
+
return typeof opts === "number" ? opts : exec(opts);
|
|
1766
|
+
}
|
|
1767
|
+
case "teardown": {
|
|
1768
|
+
const opts = parseOrExit(() => parseTeardownArgs(rest));
|
|
1769
|
+
return typeof opts === "number" ? opts : teardown(opts);
|
|
1770
|
+
}
|
|
1771
|
+
default:
|
|
1772
|
+
process.stderr.write(ROOT_USAGE + "\n");
|
|
1773
|
+
return verb === void 0 || verb === "--help" || verb === "-h" ? 0 : 2;
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
function parseOrExit(parse2) {
|
|
1777
|
+
try {
|
|
1778
|
+
return parse2();
|
|
1779
|
+
} catch (e) {
|
|
1780
|
+
if (!(e instanceof UsageError)) throw e;
|
|
1781
|
+
process.stderr.write(e.message + "\n");
|
|
1782
|
+
return HELP_TEXTS.includes(e.message) ? 0 : 2;
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
async function run(opts) {
|
|
1786
|
+
const daytonaKey = requireDaytonaKey();
|
|
1787
|
+
if (!daytonaKey) return 2;
|
|
1788
|
+
await warnIfCannotDelete(daytonaKey);
|
|
1789
|
+
const ttlMinutes = Math.ceil(opts.timeoutSeconds / 60) + 30;
|
|
1790
|
+
say(opts.sandbox ? `Creating the Daytona sandbox on twin ${opts.sandbox}` : "Creating the Daytona sandbox and its Veris twin");
|
|
1791
|
+
const daytona = new Daytona({ apiKey: daytonaKey });
|
|
1792
|
+
const created = await daytona.create({
|
|
1793
|
+
...opts.image ? { image: opts.image } : {},
|
|
1794
|
+
...opts.snapshot ? { snapshot: opts.snapshot } : {},
|
|
1795
|
+
// A run deletes its own sandbox, so these two only ever matter when it
|
|
1796
|
+
// could not: --keep, a crash, a machine that went away mid-run. Daytona's
|
|
1797
|
+
// own auto-stop default is 15 minutes and auto-delete is off entirely, so
|
|
1798
|
+
// saying them out loud is the difference between a stranded box costing an
|
|
1799
|
+
// hour and costing until someone notices.
|
|
1800
|
+
autoStopInterval: AUTO_STOP_MINUTES,
|
|
1801
|
+
autoDeleteInterval: AUTO_DELETE_MINUTES,
|
|
1802
|
+
veris: {
|
|
1803
|
+
environmentId: opts.environment,
|
|
1804
|
+
attachSandboxId: opts.sandbox,
|
|
1805
|
+
ttlMinutes
|
|
1806
|
+
}
|
|
1807
|
+
}, { timeout: 300 });
|
|
1808
|
+
if (!isVerisSandbox(created)) throw new VerisError("create() returned a sandbox with no Veris surface");
|
|
1809
|
+
const sandbox = created;
|
|
1810
|
+
note(`Daytona sandbox ${sandbox.id}`);
|
|
1811
|
+
note(`Veris twin ${sandbox.verisSandboxId}`);
|
|
1812
|
+
let code = 2;
|
|
1813
|
+
try {
|
|
1814
|
+
const root = await sandboxRoot(sandbox);
|
|
1815
|
+
const work = `${root}/${WORK_SUBDIR}`;
|
|
1816
|
+
await putCode(sandbox, root, work, opts.repo, opts.ref);
|
|
1817
|
+
const env = commandEnv(sandbox.veris.getTrustEnv(), opts.env);
|
|
1818
|
+
if (opts.setup) {
|
|
1819
|
+
say(`Setup: ${opts.setup}`);
|
|
1820
|
+
const setup = await stream(sandbox, opts.setup, work, env, opts.timeoutSeconds);
|
|
1821
|
+
if (setup !== 0) {
|
|
1822
|
+
process.stderr.write(`
|
|
1823
|
+
setup exited ${setup}; not running the command
|
|
1824
|
+
`);
|
|
1825
|
+
return setup;
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
const patched = await sandbox.veris.patchBundledCas();
|
|
1829
|
+
for (const file of patched) note(`${BUNDLED_CA_PATCHED_MARKER}${file}`);
|
|
1830
|
+
if (patched.length) note(`${patched.length} bundled CA file(s) patched`);
|
|
1831
|
+
say(`Running: ${opts.command}`);
|
|
1832
|
+
const commandExit = await stream(sandbox, opts.command, work, env, opts.timeoutSeconds);
|
|
1833
|
+
note(`exit ${commandExit}`);
|
|
1834
|
+
say("Reading the receipt");
|
|
1835
|
+
const receipt = await sandbox.veris.receipt();
|
|
1836
|
+
out("");
|
|
1837
|
+
out(formatReceipt(receipt, sandbox.verisSandboxId));
|
|
1838
|
+
const v = verdict(commandExit, receipt, opts.requireService);
|
|
1839
|
+
for (const p of v.problems) process.stderr.write(`
|
|
1840
|
+
\x1B[31m\u2717\x1B[0m ${p}`);
|
|
1841
|
+
if (v.problems.length) process.stderr.write("\n Do not report this run as working.\n");
|
|
1842
|
+
code = v.exitCode;
|
|
1843
|
+
return code;
|
|
1844
|
+
} finally {
|
|
1845
|
+
const owns = verisOwnsTwin(sandbox);
|
|
1846
|
+
if (opts.keep) {
|
|
1847
|
+
say(owns ? "Keeping the sandbox and twin (--keep)" : "Keeping the sandbox (--keep); the twin was already yours");
|
|
1848
|
+
note(`VERIS_SANDBOX_ID=${sandbox.verisSandboxId}`);
|
|
1849
|
+
note(`daytona sandbox: ${sandbox.id}`);
|
|
1850
|
+
note(owns ? `delete both with: veris-daytona teardown ${sandbox.id}` : `delete the sandbox with: veris-daytona teardown ${sandbox.id} # your twin is left alone`);
|
|
1851
|
+
} else {
|
|
1852
|
+
say(owns ? "Deleting the sandbox and twin" : `Deleting the sandbox; twin ${sandbox.verisSandboxId} is yours and is left running`);
|
|
1853
|
+
try {
|
|
1854
|
+
await sandbox.delete();
|
|
1855
|
+
} catch (e) {
|
|
1856
|
+
const why = isPermissionDenied(e) ? await refusedMessage(daytonaKey, sandbox, owns) : String(e);
|
|
1857
|
+
process.stderr.write(` cleanup failed: ${why}
|
|
1858
|
+
`);
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
async function provision(opts) {
|
|
1864
|
+
const daytonaKey = requireDaytonaKey();
|
|
1865
|
+
if (!daytonaKey) return 2;
|
|
1866
|
+
await warnIfCannotDelete(daytonaKey);
|
|
1867
|
+
const createdAt = Date.now();
|
|
1868
|
+
say(`Creating the Daytona sandbox on twin ${opts.sandbox}`);
|
|
1869
|
+
const created = await new Daytona({ apiKey: daytonaKey }).create({
|
|
1870
|
+
...opts.image ? { image: opts.image } : {},
|
|
1871
|
+
...opts.snapshot ? { snapshot: opts.snapshot } : {},
|
|
1872
|
+
// The only place a caller's --env can land: there is no command here to
|
|
1873
|
+
// export it in front of, so it is set on the sandbox and every command run
|
|
1874
|
+
// in there inherits it. Veris-managed variables still win — see create().
|
|
1875
|
+
...Object.keys(opts.env).length ? { envVars: opts.env } : {},
|
|
1876
|
+
// Load-bearing here in a way they are not in `run`: nothing deletes this
|
|
1877
|
+
// box afterwards, so these are what stop an abandoned one from billing.
|
|
1878
|
+
autoStopInterval: AUTO_STOP_MINUTES,
|
|
1879
|
+
autoDeleteInterval: AUTO_DELETE_MINUTES,
|
|
1880
|
+
veris: {
|
|
1881
|
+
attachSandboxId: opts.sandbox,
|
|
1882
|
+
// The Daytona box's wall-clock backstop. It does not move the twin's own
|
|
1883
|
+
// TTL: an attached twin is never created here, so its life was already
|
|
1884
|
+
// decided by whoever made it.
|
|
1885
|
+
ttlMinutes: PROVISION_TTL_MINUTES
|
|
1886
|
+
}
|
|
1887
|
+
}, { timeout: 300 });
|
|
1888
|
+
if (!isVerisSandbox(created)) throw new VerisError("create() returned a sandbox with no Veris surface");
|
|
1889
|
+
const sandbox = created;
|
|
1890
|
+
note(`Daytona sandbox ${sandbox.id}`);
|
|
1891
|
+
note(`Veris twin ${sandbox.verisSandboxId}`);
|
|
1892
|
+
const work = `${await sandboxRoot(sandbox)}/${WORK_SUBDIR}`;
|
|
1893
|
+
const mkdir = await sandbox.process.executeCommand(`mkdir -p ${shellQuote2(work)}`, void 0, void 0, 60);
|
|
1894
|
+
if (mkdir.exitCode !== 0) throw new VerisError(`could not create ${work} in the sandbox: ${mkdir.result}`);
|
|
1895
|
+
const services = await sandbox.veris.services();
|
|
1896
|
+
say("Ready. The sandbox is up, trusted, and running nothing");
|
|
1897
|
+
note(`put code in: veris-daytona push ${sandbox.id}`);
|
|
1898
|
+
note(`run something: veris-daytona exec ${sandbox.id} -- <command>`);
|
|
1899
|
+
note(`delete it: veris-daytona teardown ${sandbox.id}`);
|
|
1900
|
+
out(provisionJson(provisionResult({
|
|
1901
|
+
daytonaSandboxId: sandbox.id,
|
|
1902
|
+
verisSandboxId: sandbox.verisSandboxId,
|
|
1903
|
+
verisEnvironmentId: sandbox.veris.environmentId,
|
|
1904
|
+
workDir: work,
|
|
1905
|
+
trustEnv: sandbox.veris.getTrustEnv(),
|
|
1906
|
+
services: services.map((s) => s.name),
|
|
1907
|
+
// Daytona's own answer where it gives one: it is the authority on when the
|
|
1908
|
+
// box dies, and the local sum can only estimate the moment it began.
|
|
1909
|
+
expiresAt: sandbox.autoDestroyAt ?? new Date(createdAt + PROVISION_TTL_MINUTES * 6e4).toISOString()
|
|
1910
|
+
})));
|
|
1911
|
+
return 0;
|
|
1912
|
+
}
|
|
1913
|
+
async function push(opts) {
|
|
1914
|
+
const daytonaKey = requireDaytonaKey();
|
|
1915
|
+
if (!daytonaKey) return 2;
|
|
1916
|
+
const sandbox = await getSandbox(daytonaKey, opts.sandboxId);
|
|
1917
|
+
if (typeof sandbox === "number") return sandbox;
|
|
1918
|
+
const root = await sandboxRoot(sandbox);
|
|
1919
|
+
await putCode(sandbox, root, `${root}/${WORK_SUBDIR}`, opts.repo, opts.ref);
|
|
1920
|
+
return 0;
|
|
1921
|
+
}
|
|
1922
|
+
async function exec(opts) {
|
|
1923
|
+
const daytonaKey = requireDaytonaKey();
|
|
1924
|
+
if (!daytonaKey) return 2;
|
|
1925
|
+
const sandbox = await getSandbox(daytonaKey, opts.sandboxId);
|
|
1926
|
+
if (typeof sandbox === "number") return sandbox;
|
|
1927
|
+
const wired = verisTwinId(sandbox) !== void 0;
|
|
1928
|
+
if (!wired) note(`${sandbox.id} has no Veris twin, so there is no trust environment to apply`);
|
|
1929
|
+
const trust = !wired ? {} : isVerisSandbox(sandbox) ? sandbox.veris.getTrustEnv() : vendoredTrustEnv();
|
|
1930
|
+
const cwd = opts.cwd ?? `${await sandboxRoot(sandbox)}/${WORK_SUBDIR}`;
|
|
1931
|
+
say(`Running in ${cwd}: ${opts.command}`);
|
|
1932
|
+
const code = await stream(sandbox, opts.command, cwd, commandEnv(trust, opts.env), opts.timeoutSeconds);
|
|
1933
|
+
note(`exit ${code}`);
|
|
1934
|
+
return code;
|
|
1935
|
+
}
|
|
1936
|
+
async function teardown(opts) {
|
|
1937
|
+
const daytonaKey = requireDaytonaKey();
|
|
1938
|
+
if (!daytonaKey) return 2;
|
|
1939
|
+
const sandbox = await getSandbox(daytonaKey, opts.sandboxId, " No Veris twin was touched.");
|
|
1940
|
+
if (typeof sandbox === "number") return sandbox;
|
|
1941
|
+
const twinId = verisTwinId(sandbox);
|
|
1942
|
+
const owns = verisOwnsTwin(sandbox);
|
|
1943
|
+
if (owns && !isVerisSandbox(sandbox)) {
|
|
1944
|
+
process.stderr.write(
|
|
1945
|
+
`sandbox ${sandbox.id} owns Veris twin ${twinId}, and no Veris API key was found (${missingKeyWhere(resolveVerisCredentials())}), so the twin cannot be deleted. Run \`veris login\` or set VERIS_API_KEY and run this again, or the twin outlives the sandbox until its TTL. Nothing was deleted.
|
|
1946
|
+
`
|
|
1947
|
+
);
|
|
1948
|
+
return 2;
|
|
1949
|
+
}
|
|
1950
|
+
say(owns ? `Deleting the sandbox and twin ${twinId}` : twinId ? `Deleting the sandbox; twin ${twinId} is yours and is left running` : "Deleting the sandbox (it has no Veris twin)");
|
|
1951
|
+
try {
|
|
1952
|
+
await sandbox.delete();
|
|
1953
|
+
} catch (e) {
|
|
1954
|
+
if (!isPermissionDenied(e)) throw e;
|
|
1955
|
+
process.stderr.write(`${await refusedMessage(daytonaKey, sandbox, owns)}
|
|
1956
|
+
`);
|
|
1957
|
+
return 1;
|
|
1958
|
+
}
|
|
1959
|
+
note(`deleted ${sandbox.id}`);
|
|
1960
|
+
return 0;
|
|
1961
|
+
}
|
|
1962
|
+
function requireDaytonaKey() {
|
|
1963
|
+
const key = process.env.DAYTONA_API_KEY;
|
|
1964
|
+
if (!key) process.stderr.write("DAYTONA_API_KEY is not set. Get one at https://app.daytona.io/dashboard/keys\n");
|
|
1965
|
+
return key;
|
|
1966
|
+
}
|
|
1967
|
+
async function warnIfCannotDelete(daytonaKey) {
|
|
1968
|
+
const key = await fetchDaytonaKey(daytonaKey);
|
|
1969
|
+
if (key && canDeleteSandboxes(key) === false) {
|
|
1970
|
+
process.stderr.write(`\x1B[33m!\x1B[0m ${cannotTeardownWarning(key, AUTO_STOP_MINUTES, AUTO_DELETE_MINUTES)}
|
|
1971
|
+
`);
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
async function refusedMessage(daytonaKey, sandbox, ownsTwin) {
|
|
1975
|
+
return teardownRefusedMessage({
|
|
1976
|
+
sandboxId: sandbox.id,
|
|
1977
|
+
twinId: verisTwinId(sandbox),
|
|
1978
|
+
ownsTwin,
|
|
1979
|
+
autoStopMinutes: sandbox.autoStopInterval,
|
|
1980
|
+
autoDeleteMinutes: sandbox.autoDeleteInterval,
|
|
1981
|
+
expiresAt: sandbox.autoDestroyAt,
|
|
1982
|
+
key: await fetchDaytonaKey(daytonaKey)
|
|
1983
|
+
});
|
|
1984
|
+
}
|
|
1985
|
+
async function getSandbox(apiKey, id, tail = "") {
|
|
1986
|
+
try {
|
|
1987
|
+
return await new Daytona({ apiKey }).get(id);
|
|
1988
|
+
} catch (e) {
|
|
1989
|
+
if (!(e instanceof DaytonaNotFoundError)) throw e;
|
|
1990
|
+
process.stderr.write(
|
|
1991
|
+
`no Daytona sandbox '${id}' \u2014 already deleted, or it belongs to another organization than this DAYTONA_API_KEY.${tail}
|
|
1992
|
+
`
|
|
1993
|
+
);
|
|
1994
|
+
return 1;
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
async function putCode(sandbox, root, work, repo, ref) {
|
|
1998
|
+
if (repo) {
|
|
1999
|
+
say(`Cloning ${repo}${ref ? ` (${ref})` : ""}`);
|
|
2000
|
+
const token = process.env.GITHUB_TOKEN ?? process.env.GH_TOKEN;
|
|
2001
|
+
const auth = token && /github\.com/.test(repo) ? ["x-access-token", token] : [void 0, void 0];
|
|
2002
|
+
await sandbox.git.clone(repo, work, ref, void 0, auth[0], auth[1], void 0, 1);
|
|
2003
|
+
note(`cloned into ${work}`);
|
|
2004
|
+
return;
|
|
2005
|
+
}
|
|
2006
|
+
say(`Uploading ${process.cwd()}`);
|
|
2007
|
+
const archive = packCwd();
|
|
2008
|
+
note(`${(archive.length / 1024 / 1024).toFixed(1)} MB after excluding ${UPLOAD_EXCLUDES.join(", ")}`);
|
|
2009
|
+
const tgz = `${root}/${WORK_SUBDIR}.tgz`;
|
|
2010
|
+
await sandbox.fs.uploadFile(archive, tgz, 300);
|
|
2011
|
+
const unpack = await sandbox.process.executeCommand(
|
|
2012
|
+
`mkdir -p ${shellQuote2(work)} && tar xzf ${shellQuote2(tgz)} -C ${shellQuote2(work)} && rm ${shellQuote2(tgz)}`,
|
|
2013
|
+
void 0,
|
|
2014
|
+
void 0,
|
|
2015
|
+
120
|
|
2016
|
+
);
|
|
2017
|
+
if (unpack.exitCode !== 0) throw new VerisError(`unpacking the upload failed: ${unpack.result}`);
|
|
2018
|
+
note(`unpacked into ${work}`);
|
|
2019
|
+
}
|
|
2020
|
+
async function sandboxRoot(sandbox) {
|
|
2021
|
+
const root = await sandbox.getWorkDir() ?? await sandbox.getUserRootDir() ?? "/home/daytona";
|
|
2022
|
+
return root.replace(/\/$/, "");
|
|
2023
|
+
}
|
|
2024
|
+
function packCwd() {
|
|
2025
|
+
const args = ["czf", "-", ...UPLOAD_EXCLUDES.map((x) => `--exclude=${x}`), "-C", process.cwd(), "."];
|
|
2026
|
+
return execFileSync("tar", args, {
|
|
2027
|
+
maxBuffer: 1024 * 1024 * 1024,
|
|
2028
|
+
// macOS bsdtar writes an AppleDouble `._name` beside every file it packs.
|
|
2029
|
+
// Measured in the sandbox: a two-file upload arrived as `smoke.py`,
|
|
2030
|
+
// `trust_check.py`, `._smoke.py`, `._trust_check.py` and a bare `._.` —
|
|
2031
|
+
// resource forks for a Linux box that will never read one. The variable is
|
|
2032
|
+
// ignored everywhere else, so it costs nothing on Linux.
|
|
2033
|
+
env: { ...process.env, COPYFILE_DISABLE: "1" }
|
|
2034
|
+
});
|
|
2035
|
+
}
|
|
2036
|
+
async function stream(sandbox, command, cwd, env, timeoutSeconds) {
|
|
2037
|
+
const session = `veris-run-${randomUUID2().slice(0, 8)}`;
|
|
2038
|
+
await sandbox.process.createSession(session);
|
|
2039
|
+
let timer;
|
|
2040
|
+
try {
|
|
2041
|
+
const started = await sandbox.process.executeSessionCommand(
|
|
2042
|
+
session,
|
|
2043
|
+
{ command: commandLine(cwd, env, command, timeoutSeconds), runAsync: true },
|
|
2044
|
+
60
|
|
2045
|
+
);
|
|
2046
|
+
const cmdId = started.cmdId;
|
|
2047
|
+
if (!cmdId) throw new VerisError("Daytona did not return a command id for the session command");
|
|
2048
|
+
const backstopMs = (timeoutSeconds + 30) * 1e3;
|
|
2049
|
+
const backstop = new Promise((resolve) => {
|
|
2050
|
+
timer = setTimeout(() => resolve("timeout"), backstopMs);
|
|
2051
|
+
});
|
|
2052
|
+
const logs = sandbox.process.getSessionCommandLogs(
|
|
2053
|
+
session,
|
|
2054
|
+
cmdId,
|
|
2055
|
+
(chunk) => process.stdout.write(chunk),
|
|
2056
|
+
(chunk) => process.stderr.write(chunk)
|
|
2057
|
+
).then(() => "done");
|
|
2058
|
+
if (await Promise.race([logs, backstop]) === "timeout") {
|
|
2059
|
+
process.stderr.write(`
|
|
2060
|
+
command still running after ${timeoutSeconds}s; stopping it
|
|
2061
|
+
`);
|
|
2062
|
+
return TIMED_OUT_EXIT;
|
|
2063
|
+
}
|
|
2064
|
+
const deadline = Date.now() + 3e4;
|
|
2065
|
+
for (; ; ) {
|
|
2066
|
+
const cmd = await sandbox.process.getSessionCommand(session, cmdId);
|
|
2067
|
+
if (typeof cmd.exitCode === "number") {
|
|
2068
|
+
if (cmd.exitCode === TIMED_OUT_EXIT) process.stderr.write(`
|
|
2069
|
+
command timed out after ${timeoutSeconds}s
|
|
2070
|
+
`);
|
|
2071
|
+
return cmd.exitCode;
|
|
2072
|
+
}
|
|
2073
|
+
if (Date.now() > deadline) throw new VerisError("command finished but Daytona never reported its exit code");
|
|
2074
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
2075
|
+
}
|
|
2076
|
+
} finally {
|
|
2077
|
+
if (timer) clearTimeout(timer);
|
|
2078
|
+
await sandbox.process.deleteSession(session).catch(() => {
|
|
2079
|
+
});
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
main(process.argv.slice(2)).then(
|
|
2083
|
+
(code) => process.exit(code),
|
|
2084
|
+
(err) => {
|
|
2085
|
+
const e = err;
|
|
2086
|
+
process.stderr.write(`
|
|
2087
|
+
\x1B[31mFAILED\x1B[0m ${e?.message ?? err}
|
|
2088
|
+
`);
|
|
2089
|
+
if (e?.phase) process.stderr.write(` phase: ${e.phase}
|
|
2090
|
+
`);
|
|
2091
|
+
if (e?.responseBody) process.stderr.write(` body: ${JSON.stringify(e.responseBody).slice(0, 400)}
|
|
2092
|
+
`);
|
|
2093
|
+
process.exit(2);
|
|
2094
|
+
}
|
|
2095
|
+
);
|
|
2096
|
+
export {
|
|
2097
|
+
main
|
|
2098
|
+
};
|
|
2099
|
+
//# sourceMappingURL=cli.js.map
|