@prized/sdk 0.1.0 → 0.1.1

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/dist/cjs/files.js CHANGED
@@ -9,7 +9,12 @@ const http_js_1 = require("./http.js");
9
9
  const outcome_js_1 = require("./outcome.js");
10
10
  const shell_js_1 = require("./shell.js");
11
11
  const toolbox_js_1 = require("./toolbox.js");
12
- /** The sandbox's file transfers take at most this long, retries included. */
12
+ /**
13
+ * The sandbox's file transfers take at most this long, retries included:
14
+ * the edge's own limit on one transfer. A write's upload sent again (a
15
+ * staged one is, after a lost answer or a 502, 503 or 504) is sent only
16
+ * while time is left, so a transfer that timed out is not.
17
+ */
13
18
  exports.FILE_TIMEOUT_MS = 10 * 60_000;
14
19
  function byteSize(data) {
15
20
  if (typeof data === 'string')
@@ -45,12 +50,16 @@ exports.EXIT_TARGET_NOT_REGULAR = 48;
45
50
  const DISCARD_MS = 10_000;
46
51
  /** A write's own commands: a rename, a chmod or an rm. */
47
52
  const WRITE_COMMAND_MS = 60_000;
48
- /** For the staged upload: the target is changed only by the command that follows it. */
53
+ /**
54
+ * For the staged upload: the target is changed only by the command that
55
+ * follows it. The upload may have been sent again, so it says only what to
56
+ * do (langchain-prized's staged_upload_effect words it the same).
57
+ */
49
58
  function stagedEffect(path) {
50
59
  return {
51
60
  maybe: `${path} was not changed (its new bytes may have reached the sandbox, but were not put in place)`,
52
61
  did: `${path} was not changed yet`,
53
- check: 'It was not sent again: write it again.',
62
+ check: 'Write it again.',
54
63
  };
55
64
  }
56
65
  /** For the command that puts the staged upload in place: a 2xx proves it ran, not how it ended. */
@@ -170,9 +179,13 @@ class Files {
170
179
  * A file that is replaced keeps its mode, so a script made executable
171
180
  * stays executable; a new file gets the sandbox's default (0600 in
172
181
  * prized-base-v1 and prized-agent-v1). `mode` sets the permission bits either way.
173
- * Sent again only if it provably never arrived: a write whose answer was
174
- * lost throws, since it may have landed and the sandbox written the file
175
- * since (read it back, or write again, as suits the file).
182
+ * The bytes go up under a temporary name beside the file, sent again if
183
+ * need be (a lost answer, a 502, 503 or 504; all copies within the
184
+ * transfer's 10 minutes),
185
+ * and one command that is never sent again puts them in place: a write
186
+ * whose answer to that command was lost throws, since it may have landed
187
+ * and the sandbox written the file since (read it back, or write again,
188
+ * as suits the file).
176
189
  */
177
190
  async write(path, data, options = {}) {
178
191
  const sandboxId = this.toolbox.sandboxId;
@@ -184,14 +197,18 @@ class Files {
184
197
  const target = (0, shell_js_1.sandboxPath)(path);
185
198
  const plan = stagingFor(target);
186
199
  if (plan === null)
187
- return this.send(target, data, size, target, (0, outcome_js_1.writeEffect)(target));
200
+ return this.send(target, data, size, target, (0, outcome_js_1.writeEffect)(target), 'exec');
188
201
  const { clean, staging } = plan;
189
202
  try {
190
- await this.send(staging, data, size, clean, stagedEffect(clean));
203
+ // Every copy to the same fresh name, which only the command below puts in place.
204
+ await this.send(staging, data, size, clean, stagedEffect(clean), 'staged');
191
205
  }
192
206
  catch (error) {
193
207
  // A 2xx whose body could not be read: the staged bytes are all there.
194
208
  if (!landed(error)) {
209
+ // Removed only if a copy may have landed: a refusal, or copies that
210
+ // never arrived, left nothing, and a command would wake a sandbox
211
+ // the edge's refusal left paused.
195
212
  if ((0, outcome_js_1.mayHaveActed)(error))
196
213
  await this.discard(staging);
197
214
  throw error;
@@ -217,7 +234,7 @@ class Files {
217
234
  return { path: clean, size };
218
235
  if (result.exitCode === exports.EXIT_TARGET_IS_LINK) {
219
236
  // A link: the files API follows it or refuses it, as for any upload.
220
- const info = await this.send(clean, data, size, clean, (0, outcome_js_1.writeEffect)(clean));
237
+ const info = await this.send(clean, data, size, clean, (0, outcome_js_1.writeEffect)(clean), 'exec');
221
238
  if (mode !== null)
222
239
  await this.chmod(clean, mode);
223
240
  return info;
@@ -228,16 +245,20 @@ class Files {
228
245
  const denied = /Permission denied|Operation not permitted/.test(result.stderr);
229
246
  throw new errors_js_1.PrizedError(denied ? 'permission_denied' : 'command_failed', `Could not write ${clean}: ${commandDetail(result)}`, { sandboxId });
230
247
  }
231
- /** One upload to `path`, as the files API takes it; `target` is the file the caller asked for, and `effect` what the upload does to it. */
232
- async send(path, data, size, target, effect) {
248
+ /**
249
+ * One upload to `path`, as the files API takes it; `target` is the file the
250
+ * caller asked for, and `effect` what the upload does to it. `exec` for an
251
+ * upload straight to the file: like a command, a resend after the answer
252
+ * was lost could overwrite what the sandbox wrote to the path since.
253
+ * `staged` for one to a fresh name: sent again like a read.
254
+ */
255
+ async send(path, data, size, target, effect, retry) {
233
256
  const result = await (0, http_js_1.request)(this.toolbox.config, {
234
257
  method: 'POST',
235
258
  url: this.url('upload', path),
236
259
  body: data,
237
260
  contentType: 'application/octet-stream',
238
- // Like a command: a resend after the answer was lost could overwrite
239
- // what the sandbox wrote to the path since.
240
- retry: 'exec',
261
+ retry,
241
262
  effect,
242
263
  timeoutMs: exports.FILE_TIMEOUT_MS,
243
264
  deadline: this.toolbox.config.now() + exports.FILE_TIMEOUT_MS,
@@ -6,11 +6,16 @@ export declare const DEFAULT_EDGE_URL = "https://term.prized.dev";
6
6
  export interface ClientOptions {
7
7
  /** Your API key (`dcp_…`). Default: the `PRIZED_API_KEY` environment variable. */
8
8
  apiKey?: string;
9
- /** The API origin. Default: `https://api.prized.dev`. */
9
+ /**
10
+ * The API origin: https, or plain http only on this machine (`localhost`,
11
+ * `127.0.0.1`, `[::1]`). Default: `https://api.prized.dev`.
12
+ */
10
13
  baseUrl?: string;
11
14
  /**
12
- * The origin that serves exec and files. Default: the `toolboxBase` the API
13
- * returns for each sandbox (today `https://term.prized.dev`).
15
+ * The origin that serves exec and files, by the same rule as `baseUrl`.
16
+ * Default: the `toolboxBase` the API returns for each sandbox (today
17
+ * `https://term.prized.dev`), on a host trusted beside the API's
18
+ * (toolboxBaseFor).
14
19
  */
15
20
  edgeUrl?: string;
16
21
  /** A fetch implementation. Default: the global `fetch`. */
@@ -23,10 +28,11 @@ export interface ClientOptions {
23
28
  * the request itself and the API's hold of a create, pause or resume. Default 500. */
24
29
  pollIntervalMs?: number;
25
30
  /**
26
- * How long a streamed or long `exec` keeps polling while its polls fail
27
- * (no answer, a 5xx, a rate limit), in ms, before it gives up and stops
28
- * the command. Default 120 000. `0` gives up once one poll's retries run
29
- * out. `exec` takes it per call too.
31
+ * How long a wait keeps polling while its polls fail (no answer, a 5xx, a
32
+ * rate limit), in ms, before it gives up: a streamed or long `exec` then
33
+ * stops the command, a handle's `wait()` throws. Default 120 000. `0`
34
+ * gives up once one poll's retries run out. `exec` and `wait()` take it
35
+ * per call too.
30
36
  */
31
37
  pollOutageMs?: number;
32
38
  /** Sleep seam (tests inject one that never really waits). */
@@ -51,10 +57,10 @@ export interface Config {
51
57
  }
52
58
  export declare function abortableSleep(ms: number, signal?: AbortSignal): Promise<void>;
53
59
  /**
54
- * How long a streamed or long exec keeps polling while its polls fail
55
- * before it stops the command (pollOutageMs): long enough to ride out a
56
- * dropped connection or an edge that restarts. The Python SDK's
57
- * DEFAULT_POLL_OUTAGE.
60
+ * How long a wait (a streamed or long exec, a handle's wait()) keeps polling
61
+ * while its polls fail before it gives up (pollOutageMs), and a streamed or
62
+ * long exec stops its command: long enough to ride out a dropped connection
63
+ * or an edge that restarts. The Python SDK's DEFAULT_POLL_OUTAGE.
58
64
  */
59
65
  export declare const DEFAULT_POLL_OUTAGE_MS = 120000;
60
66
  /** A pollOutageMs given (a number of ms ≥ 0), or undefined. */
@@ -63,7 +69,10 @@ export declare function resolveConfig(options?: ClientOptions): Config;
63
69
  /**
64
70
  * Where exec and files go for one sandbox. An explicit edgeUrl wins; else the
65
71
  * API's own `toolboxBase` when it has the expected shape (https, or plain
66
- * http on loopback for local development); else the default edge.
72
+ * http on loopback for local development, ending /v1/sandbox/<id>) on a host
73
+ * trusted beside the API's (term.prized.dev beside api.prized.dev; see
74
+ * trustedEdge); else the default edge, for Prized's own API only. The API key
75
+ * is never sent anywhere else: discovery is routing, not authority.
67
76
  */
68
77
  export declare function toolboxBaseFor(config: Config, sandboxId: string, toolboxBase: string | null): string;
69
78
  /**
@@ -93,8 +102,15 @@ export declare function warmEdge(config: Config): Promise<void>;
93
102
  * provably never arrived: a connection that never opened, or an answer
94
103
  * saying so (the edge's rate limit, its lookup failing). A lost answer
95
104
  * is never retried: it may already have run.
105
+ * - `staged`: a write's upload to a fresh name beside its target, which only
106
+ * a command after it puts in place (files.ts). Retried as `safe` (a lost
107
+ * answer, a timeout, 429/502/503/504) and `exec` both, a GET's 500 aside:
108
+ * every copy writes the same bytes under the same name, which nothing else
109
+ * writes, so a second one changes no file of the caller's. A 2xx whose body
110
+ * broke off is not sent again: the bytes are there. Its error says whether
111
+ * a copy may have reached the sandbox.
96
112
  */
97
- export type RetryMode = 'safe' | 'exec';
113
+ export type RetryMode = 'safe' | 'exec' | 'staged';
98
114
  /**
99
115
  * Whether a fetch that threw never sent its request: the connection was
100
116
  * refused, the name did not resolve, or connecting timed out. Anything else
package/dist/cjs/http.js CHANGED
@@ -33,7 +33,15 @@ function envApiKey() {
33
33
  function isLoopback(hostname) {
34
34
  return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '[::1]';
35
35
  }
36
- /** An http(s) origin, trailing slashes and a trailing /api/v1 dropped. */
36
+ /**
37
+ * https anywhere, plain http only to this machine: the API key rides in
38
+ * every request's Authorization header. The same rule for the options and
39
+ * for an edge the API names.
40
+ */
41
+ function secureEndpoint(url) {
42
+ return url.protocol === 'https:' || (url.protocol === 'http:' && isLoopback(url.hostname));
43
+ }
44
+ /** An https (or loopback http) origin, trailing slashes and a trailing /api/v1 dropped. */
37
45
  function normalizeOrigin(name, raw) {
38
46
  let url;
39
47
  try {
@@ -45,6 +53,9 @@ function normalizeOrigin(name, raw) {
45
53
  if (url.protocol !== 'https:' && url.protocol !== 'http:') {
46
54
  throw new errors_js_1.PrizedError('invalid_argument', `${name} must be an http(s) URL`);
47
55
  }
56
+ if (!secureEndpoint(url)) {
57
+ throw new errors_js_1.PrizedError('invalid_argument', `${name} must use https (plain http only for localhost, 127.0.0.1 or [::1])`);
58
+ }
48
59
  if (url.username || url.password || url.search || url.hash) {
49
60
  throw new errors_js_1.PrizedError('invalid_argument', `${name} must not carry credentials, a query or a fragment`);
50
61
  }
@@ -69,10 +80,10 @@ function abortableSleep(ms, signal) {
69
80
  });
70
81
  }
71
82
  /**
72
- * How long a streamed or long exec keeps polling while its polls fail
73
- * before it stops the command (pollOutageMs): long enough to ride out a
74
- * dropped connection or an edge that restarts. The Python SDK's
75
- * DEFAULT_POLL_OUTAGE.
83
+ * How long a wait (a streamed or long exec, a handle's wait()) keeps polling
84
+ * while its polls fail before it gives up (pollOutageMs), and a streamed or
85
+ * long exec stops its command: long enough to ride out a dropped connection
86
+ * or an edge that restarts. The Python SDK's DEFAULT_POLL_OUTAGE.
76
87
  */
77
88
  exports.DEFAULT_POLL_OUTAGE_MS = 120_000;
78
89
  /** A pollOutageMs given (a number of ms ≥ 0), or undefined. */
@@ -116,20 +127,51 @@ function resolveConfig(options = {}) {
116
127
  random: options.random ?? Math.random,
117
128
  };
118
129
  }
130
+ /** Prized's own domain: its API and its edges are hosts under it. */
131
+ const PRIZED_DOMAIN = 'prized.dev';
132
+ function under(host, domain) {
133
+ return host === domain || host.endsWith(`.${domain}`);
134
+ }
135
+ /** A URL's hostname (lowercase, as URL gives it), or '' if it does not parse. */
136
+ function hostOf(raw) {
137
+ try {
138
+ return new URL(raw).hostname;
139
+ }
140
+ catch {
141
+ return '';
142
+ }
143
+ }
144
+ /**
145
+ * Where a toolboxBase may point, given the API's host: the API host itself;
146
+ * prized.dev's own hosts when the API is one of them; loopback when the API
147
+ * is. Never a domain guessed from the API host's name (a public suffix such
148
+ * as co.uk would trust everyone under it). The Python SDK's _trusted_edge.
149
+ */
150
+ function trustedEdge(edgeHost, apiHost) {
151
+ if (edgeHost === '' || apiHost === '')
152
+ return false;
153
+ if (isLoopback(apiHost) || isLoopback(edgeHost))
154
+ return isLoopback(apiHost) && isLoopback(edgeHost);
155
+ return edgeHost === apiHost || (under(apiHost, PRIZED_DOMAIN) && under(edgeHost, PRIZED_DOMAIN));
156
+ }
119
157
  /**
120
158
  * Where exec and files go for one sandbox. An explicit edgeUrl wins; else the
121
159
  * API's own `toolboxBase` when it has the expected shape (https, or plain
122
- * http on loopback for local development); else the default edge.
160
+ * http on loopback for local development, ending /v1/sandbox/<id>) on a host
161
+ * trusted beside the API's (term.prized.dev beside api.prized.dev; see
162
+ * trustedEdge); else the default edge, for Prized's own API only. The API key
163
+ * is never sent anywhere else: discovery is routing, not authority.
123
164
  */
124
165
  function toolboxBaseFor(config, sandboxId, toolboxBase) {
125
166
  const path = `/v1/sandbox/${encodeURIComponent(sandboxId)}`;
126
167
  if (config.edgeUrl !== null)
127
168
  return `${config.edgeUrl}${path}`;
169
+ const apiHost = hostOf(config.baseUrl);
128
170
  if (toolboxBase) {
129
171
  try {
130
172
  const url = new URL(toolboxBase);
131
- const secure = url.protocol === 'https:' || (url.protocol === 'http:' && isLoopback(url.hostname));
132
- if (secure &&
173
+ if (secureEndpoint(url) &&
174
+ trustedEdge(url.hostname, apiHost) &&
133
175
  !url.username &&
134
176
  !url.password &&
135
177
  !url.search &&
@@ -142,7 +184,11 @@ function toolboxBaseFor(config, sandboxId, toolboxBase) {
142
184
  // fall through to the default
143
185
  }
144
186
  }
145
- return `${exports.DEFAULT_EDGE_URL}${path}`;
187
+ // Prized's own edge only for Prized's own API: a key for another
188
+ // deployment is never sent to it. That deployment names its edge.
189
+ if (under(apiHost, PRIZED_DOMAIN))
190
+ return `${exports.DEFAULT_EDGE_URL}${path}`;
191
+ throw new errors_js_1.PrizedError('edge_unknown', `The API at ${config.baseUrl} named no command endpoint this SDK trusts for ${sandboxId}; pass edgeUrl for that deployment.`, { sandboxId });
146
192
  }
147
193
  /**
148
194
  * The edge this config's sandboxes answer on, before the API has named one:
@@ -239,7 +285,7 @@ function retryable(error, mode, method) {
239
285
  // have done a write's work, so a write is left to its caller (a create
240
286
  // that may have made a sandbox finds and deletes it). A GET did nothing:
241
287
  // a poll that meets one is sent again rather than failing its create.
242
- if (error.status === 500 && method === 'GET')
288
+ if (mode === 'safe' && error.status === 500 && method === 'GET')
243
289
  return true;
244
290
  return error.status === 429 || error.status === 502 || error.status === 503 || error.status === 504;
245
291
  }
@@ -427,8 +473,9 @@ function outcomeOf(error) {
427
473
  /**
428
474
  * The error a request throws. With an `effect`, it says whether the sandbox
429
475
  * acted on the request: by this attempt, or by an earlier one that was sent
430
- * again (`acted`: only a keyed request, the background start, is sent again
431
- * after it may have arrived). `cutStatus`: the 2xx whose body broke off.
476
+ * again (`acted`: only a keyed request, the background start, and a staged
477
+ * upload are sent again after they may have arrived). `cutStatus`: the 2xx
478
+ * whose body broke off.
432
479
  */
433
480
  function raised(failure, outcome, acted, effect, cutStatus = null) {
434
481
  if (effect === undefined)
@@ -443,7 +490,8 @@ async function request(config, spec) {
443
490
  const timeoutMs = spec.timeoutMs ?? config.requestTimeoutMs;
444
491
  const sandboxId = spec.sandboxId ?? null;
445
492
  const effect = spec.effect ?? (spec.retry === 'exec' ? (0, outcome_js_1.commandEffect)() : undefined);
446
- const unsent = spec.retry === 'exec' ? effect : undefined;
493
+ // Whether an attempt that was sent again may have reached the sandbox.
494
+ let acted = false;
447
495
  // Names the last attempt's failure, if any, so a call cut short by its
448
496
  // deadline still says what it was waiting out (a 503, a rate limit).
449
497
  const pastDeadline = (last) => {
@@ -451,9 +499,10 @@ async function request(config, spec) {
451
499
  const tail = what === null ? '' : ` The last attempt failed with ${what}.`;
452
500
  let error = new errors_js_1.PrizedError('timeout', `The time allowed for this call ran out.${tail}`, { sandboxId, cause: last });
453
501
  // A command is sent again only when it provably never arrived, so every
454
- // attempt of one cut short this way never reached the sandbox.
455
- if (unsent !== undefined)
456
- error = (0, outcome_js_1.withOutcome)(error, unsent, 'never');
502
+ // attempt of one cut short this way never reached the sandbox. A staged
503
+ // upload's copies may have, unless each provably never arrived.
504
+ if (effect !== undefined && spec.retry !== 'safe')
505
+ error = (0, outcome_js_1.withOutcome)(error, effect, spec.retry === 'staged' && acted ? 'maybe' : 'never');
457
506
  cutShort.add(error);
458
507
  return error;
459
508
  };
@@ -462,8 +511,6 @@ async function request(config, spec) {
462
511
  return error;
463
512
  };
464
513
  let last;
465
- // Whether an attempt that was sent again may have reached the sandbox.
466
- let acted = false;
467
514
  // An abort with an `effect` says whether the sandbox may have acted:
468
515
  // before an attempt, or while waiting to send one again, nothing was sent
469
516
  // that might have, unless an earlier attempt of a keyed request (`acted`)
@@ -500,8 +547,9 @@ async function request(config, spec) {
500
547
  // The status came and the body did not: the sandbox acted on it.
501
548
  const cut = outcome.status !== null && outcome.status >= 200 && outcome.status < 300;
502
549
  const said = cut ? 'did' : unsentAttempt ? 'never' : 'maybe';
503
- // A command or upload is sent again only if it never left.
504
- const resend = spec.retry === 'safe' || (spec.retry === 'exec' && unsentAttempt);
550
+ // A command is sent again only if it never left; a staged upload
551
+ // unless its 2xx came (its bytes are there).
552
+ const resend = spec.retry === 'safe' || (spec.retry === 'exec' && unsentAttempt) || (spec.retry === 'staged' && !cut);
505
553
  if (!resend || attempt >= config.maxRetries)
506
554
  throw raised(failure, said, acted, effect, cut ? outcome.status : null);
507
555
  acted = acted || said !== 'never';
@@ -4,9 +4,9 @@ export { CommandHandle, DEFAULT_EXEC_TIMEOUT_MS, MAX_BLOCKING_EXEC_MS } from './
4
4
  export type { CommandStatus, ExecOptions, WaitOptions } from './commands.js';
5
5
  export { Files } from './files.js';
6
6
  export type { FileData, WriteOptions } from './files.js';
7
- export { CommandTimeoutError, PrizedError } from './errors.js';
8
- export type { PrizedErrorInit } from './errors.js';
7
+ export { CommandTimeoutError, OutputReadError, PrizedError } from './errors.js';
8
+ export type { OutputReadErrorInit, PrizedErrorInit } from './errors.js';
9
9
  export { DEFAULT_BASE_URL, DEFAULT_EDGE_URL } from './http.js';
10
10
  export type { ClientOptions } from './http.js';
11
- export type { ExecResult, FileInfo, Resources, ResourcesInput, SandboxInfo, SandboxState, Template, WriteInfo, } from './types.js';
11
+ export type { ExecResult, FileInfo, Resources, ResourcesInput, SandboxInfo, SandboxState, Template, TruncatedReason, WriteInfo, } from './types.js';
12
12
  export { VERSION } from './version.js';
package/dist/cjs/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.VERSION = exports.DEFAULT_EDGE_URL = exports.DEFAULT_BASE_URL = exports.PrizedError = exports.CommandTimeoutError = exports.Files = exports.MAX_BLOCKING_EXEC_MS = exports.DEFAULT_EXEC_TIMEOUT_MS = exports.CommandHandle = exports.PREVIEW_KEY_HEADER = exports.DEFAULT_TEMPLATE = exports.Sandbox = void 0;
3
+ exports.VERSION = exports.DEFAULT_EDGE_URL = exports.DEFAULT_BASE_URL = exports.PrizedError = exports.OutputReadError = exports.CommandTimeoutError = exports.Files = exports.MAX_BLOCKING_EXEC_MS = exports.DEFAULT_EXEC_TIMEOUT_MS = exports.CommandHandle = exports.PREVIEW_KEY_HEADER = exports.DEFAULT_TEMPLATE = exports.Sandbox = void 0;
4
4
  // Prized Sandboxes (beta) from TypeScript: the public surface.
5
5
  var sandbox_js_1 = require("./sandbox.js");
6
6
  Object.defineProperty(exports, "Sandbox", { enumerable: true, get: function () { return sandbox_js_1.Sandbox; } });
@@ -14,6 +14,7 @@ var files_js_1 = require("./files.js");
14
14
  Object.defineProperty(exports, "Files", { enumerable: true, get: function () { return files_js_1.Files; } });
15
15
  var errors_js_1 = require("./errors.js");
16
16
  Object.defineProperty(exports, "CommandTimeoutError", { enumerable: true, get: function () { return errors_js_1.CommandTimeoutError; } });
17
+ Object.defineProperty(exports, "OutputReadError", { enumerable: true, get: function () { return errors_js_1.OutputReadError; } });
17
18
  Object.defineProperty(exports, "PrizedError", { enumerable: true, get: function () { return errors_js_1.PrizedError; } });
18
19
  var http_js_1 = require("./http.js");
19
20
  Object.defineProperty(exports, "DEFAULT_BASE_URL", { enumerable: true, get: function () { return http_js_1.DEFAULT_BASE_URL; } });
@@ -30,11 +30,12 @@ export declare function helperEffect(): Effect;
30
30
  export type Outcome = 'never' | 'maybe' | 'did' | null;
31
31
  /** Whether withOutcome said the sandbox may have acted on the request. */
32
32
  export declare function mayHaveActed(error: unknown): error is PrizedError;
33
- /** `error` with another message (and, given, another status, code, sandbox or cause), every other field kept. */
33
+ /** `error` with another message (and, given, another status, code, sandbox, exit code or cause), every other field kept. */
34
34
  export declare function restate(error: PrizedError, message: string, init?: {
35
35
  status?: number;
36
36
  code?: string;
37
37
  sandboxId?: string;
38
+ exitCode?: number;
38
39
  cause?: unknown;
39
40
  }): PrizedError;
40
41
  /** `error` (same code, status and body), its message saying what it means for `effect`. */
@@ -14,9 +14,11 @@ exports.startAbandoned = startAbandoned;
14
14
  exports.runAborted = runAborted;
15
15
  exports.pollFailed = pollFailed;
16
16
  // What an error says about a command that may have run. A command, a file
17
- // write, a remove or a signal (`exec` retry mode) is never sent again once it
18
- // may have reached the sandbox, so its error has to say what the caller cannot
19
- // see: whether it happened. The Python SDK words these the same (_core.py).
17
+ // write's rename, a remove or a signal (`exec` retry mode) is never sent again
18
+ // once it may have reached the sandbox, so its error has to say what the
19
+ // caller cannot see: whether it happened. A write's staged upload (`staged`)
20
+ // may have been sent again, and its error says whether a copy may have
21
+ // reached the sandbox. The Python SDK words these the same (_core.py).
20
22
  const errors_js_1 = require("./errors.js");
21
23
  const NOT_SENT_AGAIN = 'It was not sent again';
22
24
  function commandEffect() {
@@ -65,7 +67,7 @@ const acted = new WeakSet();
65
67
  function mayHaveActed(error) {
66
68
  return error instanceof errors_js_1.PrizedError && acted.has(error);
67
69
  }
68
- /** `error` with another message (and, given, another status, code, sandbox or cause), every other field kept. */
70
+ /** `error` with another message (and, given, another status, code, sandbox, exit code or cause), every other field kept. */
69
71
  function restate(error, message, init = {}) {
70
72
  return new errors_js_1.PrizedError(init.code ?? error.code, message, {
71
73
  status: init.status ?? error.status,
@@ -73,6 +75,7 @@ function restate(error, message, init = {}) {
73
75
  sandboxId: error.sandboxId ?? init.sandboxId ?? null,
74
76
  body: error.body,
75
77
  retryAfter: error.retryAfter,
78
+ exitCode: init.exitCode ?? error.exitCode,
76
79
  cause: 'cause' in init ? init.cause : error.cause,
77
80
  });
78
81
  }
@@ -1,5 +1,5 @@
1
1
  import { type Toolbox } from './toolbox.js';
2
- import type { ExecResult } from './types.js';
2
+ import type { ExecResult, TruncatedReason } from './types.js';
3
3
  export declare const OUTPUT_DIR = "/home/prized/.prized/exec";
4
4
  export declare const OUTPUT_ID_RE: RegExp;
5
5
  /** Each stream of a fullOutput command keeps at most this many bytes: one download. */
@@ -22,17 +22,22 @@ export declare const RUN_SCRIPT = "eval \"${PRIZED_EXEC_SCRIPT:?}\"";
22
22
  * Nothing is assigned before it runs but the positional parameters, so every
23
23
  * variable the caller's env sets reaches it unchanged, PATH included; and
24
24
  * every step holds under an errexit the env may switch on (SHELLOPTS). As in
25
- * a plain exec, the readers stop 5 s after the command ends if a process it
25
+ * a plain exec, the relays stop 5 s after the command ends if a process it
26
26
  * left running still holds its output, keeping what came through. A command
27
- * a signal ends exits 128+n, as a background command does. Without tee,
28
- * setsid or a directory to write to, the command runs as a plain exec.
27
+ * a signal ends exits 128+n, as a background command does. A reader that
28
+ * could not write its file (failedLines) sends the output to the files API
29
+ * whatever its size, with the marker saying which. Without /bin/sh, setsid,
30
+ * cat or a directory to write to, the command runs as a plain exec.
29
31
  */
30
32
  export declare function captureScript(id: string): string;
31
33
  /**
32
34
  * What a fullOutput command left when the script could not print it (its
33
35
  * time limit, or a signal, ended the script): the marker line, or `gone`,
34
36
  * with the directory removed, when there is nothing to read (it was never
35
- * made, or its output was already printed back).
37
+ * made, or its output was already printed back). A failed write is read from
38
+ * the reader's flag, before the size, which is final once the flag is there
39
+ * (the script's wait for the reader's exit status went with the script); a
40
+ * reader killed, or one that could not make its flag, is not seen here.
36
41
  */
37
42
  export declare function collectScript(id: string): string;
38
43
  /**
@@ -41,11 +46,17 @@ export declare function collectScript(id: string): string;
41
46
  * answer to remove its own.
42
47
  */
43
48
  export declare function removeOutputScript(id: string): string;
44
- /** The marker's byte counts (a stream cut at FULL_OUTPUT_BYTES reads more), `gone`, or null for no marker. */
45
- export declare function parseMarker(stdout: string, id: string): {
49
+ /** What the marker line says: each stream's byte count (one cut at FULL_OUTPUT_BYTES reads more), and whether its reader failed to write it. */
50
+ export interface Marker {
46
51
  out: number;
47
52
  err: number;
48
- } | 'gone' | null;
53
+ outFailed: boolean;
54
+ errFailed: boolean;
55
+ }
56
+ /** The marker (one without failure flags, as an earlier SDK's collect printed, has none), `gone`, or null for no marker. */
57
+ export declare function parseMarker(stdout: string, id: string): Marker | 'gone' | null;
58
+ /** Why the read-back output is cut, if it is: a failed write first, since it is the one the caller cannot foresee. */
59
+ export declare function markerTruncation(marker: Marker): TruncatedReason | null;
49
60
  /**
50
61
  * A blocking exec that keeps the whole output (up to FULL_OUTPUT_BYTES of
51
62
  * each stream). The result is a plain exec's, timeouts included: the caller