langsmith 0.10.4 → 0.10.5

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.
Files changed (43) hide show
  1. package/dist/client.cjs +10 -0
  2. package/dist/client.d.ts +1 -0
  3. package/dist/client.js +10 -0
  4. package/dist/evaluation/_runner.cjs +47 -5
  5. package/dist/evaluation/_runner.d.ts +12 -0
  6. package/dist/evaluation/_runner.js +47 -5
  7. package/dist/index.cjs +1 -1
  8. package/dist/index.d.ts +1 -1
  9. package/dist/index.js +1 -1
  10. package/dist/sandbox/access_delegation.cjs +24 -0
  11. package/dist/sandbox/access_delegation.d.ts +15 -0
  12. package/dist/sandbox/access_delegation.js +21 -0
  13. package/dist/sandbox/client.cjs +70 -5
  14. package/dist/sandbox/client.d.ts +18 -0
  15. package/dist/sandbox/client.js +70 -5
  16. package/dist/sandbox/command_handle.cjs +34 -0
  17. package/dist/sandbox/command_handle.d.ts +10 -0
  18. package/dist/sandbox/command_handle.js +34 -0
  19. package/dist/sandbox/helpers.cjs +129 -0
  20. package/dist/sandbox/helpers.d.ts +29 -0
  21. package/dist/sandbox/helpers.js +123 -0
  22. package/dist/sandbox/index.cjs +7 -1
  23. package/dist/sandbox/index.d.ts +5 -1
  24. package/dist/sandbox/index.js +2 -0
  25. package/dist/sandbox/mounts.cjs +21 -3
  26. package/dist/sandbox/mounts.d.ts +7 -1
  27. package/dist/sandbox/mounts.js +21 -3
  28. package/dist/sandbox/proxy_config.cjs +41 -10
  29. package/dist/sandbox/proxy_config.d.ts +33 -6
  30. package/dist/sandbox/proxy_config.js +40 -10
  31. package/dist/sandbox/sandbox.cjs +160 -4
  32. package/dist/sandbox/sandbox.d.ts +71 -1
  33. package/dist/sandbox/sandbox.js +161 -5
  34. package/dist/sandbox/service_url.cjs +125 -0
  35. package/dist/sandbox/service_url.d.ts +50 -0
  36. package/dist/sandbox/service_url.js +120 -0
  37. package/dist/sandbox/types.d.ts +251 -8
  38. package/dist/sandbox/ws_execute.cjs +31 -1
  39. package/dist/sandbox/ws_execute.d.ts +9 -0
  40. package/dist/sandbox/ws_execute.js +31 -1
  41. package/dist/utils/error.cjs +17 -22
  42. package/dist/utils/error.js +17 -22
  43. package/package.json +1 -1
@@ -1,6 +1,8 @@
1
1
  /**
2
2
  * Main SandboxClient class for interacting with the sandbox server API.
3
3
  */
4
+ import { validateAccessDelegation } from "./access_delegation.js";
5
+ import { ServiceLoginUrl, ServiceUrl, } from "./service_url.js";
4
6
  import { getLangSmithEnvironmentVariable } from "../utils/env.js";
5
7
  import { _getFetchImplementation } from "../singletons/fetch.js";
6
8
  import { AsyncCaller } from "../utils/async_caller.js";
@@ -404,6 +406,53 @@ export class SandboxClient {
404
406
  const suffix = segments.length ? `/${segments.join("/")}` : "";
405
407
  return `${this._baseUrl}/boxes/${encodeURIComponent(name)}${suffix}`;
406
408
  }
409
+ /**
410
+ * Generate an authenticated URL for an HTTP service running in a sandbox.
411
+ *
412
+ * Without `access`, mints a short-lived token and returns a {@link ServiceUrl}
413
+ * that refreshes it as it nears expiry. With `access` set to `"restricted"`
414
+ * or `"workspace"` the URL is gated by LangSmith login instead and a
415
+ * {@link ServiceLoginUrl} is returned: no token, no expiry, browser only.
416
+ *
417
+ * A login grant is durable, so token mode is refused with 409 while one is
418
+ * in place, and `expiresInSeconds` does not apply to the login modes.
419
+ */
420
+ async serviceUrl(name, options) {
421
+ const { port, expiresInSeconds, access, signal } = options;
422
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
423
+ throw new LangSmithValidationError(`port must be between 1 and 65535 (got ${port})`, "port");
424
+ }
425
+ if (access !== undefined &&
426
+ access !== "restricted" &&
427
+ access !== "workspace") {
428
+ throw new LangSmithValidationError(`access must be "restricted" or "workspace" (got ${String(access)})`, "access");
429
+ }
430
+ const loginMode = access !== undefined;
431
+ if (expiresInSeconds !== undefined && loginMode) {
432
+ throw new LangSmithValidationError(`expiresInSeconds does not apply to access "${access}": a LangSmith ` +
433
+ "login URL carries no token and does not expire", "expiresInSeconds");
434
+ }
435
+ const url = this._boxUrl(name, "service-url");
436
+ const payload = { port };
437
+ if (!loginMode && expiresInSeconds !== undefined) {
438
+ payload.expires_in_seconds = expiresInSeconds;
439
+ }
440
+ if (access !== undefined) {
441
+ payload.access = access;
442
+ }
443
+ const response = await this._postJson(url, payload, { signal });
444
+ const data = await response.json();
445
+ if (loginMode) {
446
+ return new ServiceLoginUrl(data);
447
+ }
448
+ // Refresh with the token parameters only. Carrying options.signal over
449
+ // would tie every later refresh to the first request's lifetime, so a
450
+ // caller-supplied timeout signal would leave the URL unable to refresh.
451
+ return new ServiceUrl(data, () => this.serviceUrl(name, {
452
+ port,
453
+ expiresInSeconds,
454
+ }));
455
+ }
407
456
  async _postJson(url, body, options) {
408
457
  const response = await this._fetch(url, {
409
458
  method: "POST",
@@ -421,7 +470,7 @@ export class SandboxClient {
421
470
  const resolvedOptions = typeof snapshotIdOrOptions === "object" && snapshotIdOrOptions !== null
422
471
  ? snapshotIdOrOptions
423
472
  : options;
424
- const { snapshotName, name, timeout = 30, waitForReady = true, idleTtlSeconds, deleteAfterStopSeconds, vCpus, memBytes, fsCapacityBytes, mountConfig, proxyConfig, } = resolvedOptions;
473
+ const { snapshotName, name, timeout = 30, waitForReady = true, idleTtlSeconds, deleteAfterStopSeconds, vCpus, memBytes, fsCapacityBytes, mountConfig, proxyConfig, runConfig, accessDelegation, } = resolvedOptions;
425
474
  if (snapshotId && snapshotName) {
426
475
  throw new LangSmithValidationError("At most one of snapshotId or options.snapshotName may be set", "snapshotId");
427
476
  }
@@ -465,6 +514,12 @@ export class SandboxClient {
465
514
  if (proxyConfig !== undefined) {
466
515
  payload.proxy_config = proxyConfig;
467
516
  }
517
+ if (runConfig !== undefined) {
518
+ payload.run_config = runConfig;
519
+ }
520
+ if (accessDelegation !== undefined) {
521
+ payload.access_delegation = validateAccessDelegation(accessDelegation);
522
+ }
468
523
  const httpTimeout = waitForReady ? (timeout + 30) * 1000 : 30 * 1000;
469
524
  const response = await this._fetch(url, {
470
525
  method: "POST",
@@ -520,13 +575,14 @@ export class SandboxClient {
520
575
  const options = typeof newNameOrOptions === "string"
521
576
  ? { newName: newNameOrOptions }
522
577
  : newNameOrOptions;
523
- const { newName, idleTtlSeconds, deleteAfterStopSeconds, proxyConfig } = options;
578
+ const { newName, idleTtlSeconds, deleteAfterStopSeconds, proxyConfig, runConfig, } = options;
524
579
  validateTtl(idleTtlSeconds, "idleTtlSeconds");
525
580
  validateTtl(deleteAfterStopSeconds, "deleteAfterStopSeconds");
526
581
  if (newName === undefined &&
527
582
  idleTtlSeconds === undefined &&
528
583
  deleteAfterStopSeconds === undefined &&
529
- proxyConfig === undefined) {
584
+ proxyConfig === undefined &&
585
+ runConfig === undefined) {
530
586
  return this.getSandbox(name);
531
587
  }
532
588
  const url = this._boxUrl(name);
@@ -543,6 +599,9 @@ export class SandboxClient {
543
599
  if (proxyConfig !== undefined) {
544
600
  payload.proxy_config = proxyConfig;
545
601
  }
602
+ if (runConfig !== undefined) {
603
+ payload.run_config = runConfig;
604
+ }
546
605
  const response = await this._fetch(url, {
547
606
  method: "PATCH",
548
607
  headers: { "Content-Type": "application/json" },
@@ -727,7 +786,7 @@ export class SandboxClient {
727
786
  * @returns Snapshot in "ready" status.
728
787
  */
729
788
  async createSnapshot(name, dockerImage, fsCapacityBytes, options = {}) {
730
- const { registryId, timeout = 60, signal } = options;
789
+ const { registryId, runConfig, timeout = 60, signal } = options;
731
790
  const url = `${this._baseUrl}/snapshots`;
732
791
  const payload = {
733
792
  name,
@@ -737,6 +796,9 @@ export class SandboxClient {
737
796
  if (registryId !== undefined) {
738
797
  payload.registry_id = registryId;
739
798
  }
799
+ if (runConfig !== undefined) {
800
+ payload.run_config = runConfig;
801
+ }
740
802
  const response = await this._postJson(url, payload, { signal });
741
803
  const snapshot = (await response.json());
742
804
  return this.waitForSnapshot(snapshot.id, { timeout, signal });
@@ -829,7 +891,7 @@ export class SandboxClient {
829
891
  * @returns Snapshot in "ready" status.
830
892
  */
831
893
  async captureSnapshot(sandboxName, name, options = {}) {
832
- const { dockerImage, fsCapacityBytes, timeout = 60, signal } = options;
894
+ const { dockerImage, fsCapacityBytes, runConfig, timeout = 60, signal, } = options;
833
895
  const url = this._boxUrl(sandboxName, "snapshot");
834
896
  const payload = { name };
835
897
  if (dockerImage !== undefined) {
@@ -838,6 +900,9 @@ export class SandboxClient {
838
900
  if (fsCapacityBytes !== undefined) {
839
901
  payload.fs_capacity_bytes = fsCapacityBytes;
840
902
  }
903
+ if (runConfig !== undefined) {
904
+ payload.run_config = runConfig;
905
+ }
841
906
  const response = await this._postJson(url, payload, { signal });
842
907
  const snapshot = (await response.json());
843
908
  return this.waitForSnapshot(snapshot.id, { timeout, signal });
@@ -141,6 +141,18 @@ class CommandHandle {
141
141
  writable: true,
142
142
  value: void 0
143
143
  });
144
+ Object.defineProperty(this, "_stdinClosed", {
145
+ enumerable: true,
146
+ configurable: true,
147
+ writable: true,
148
+ value: void 0
149
+ });
150
+ Object.defineProperty(this, "_pty", {
151
+ enumerable: true,
152
+ configurable: true,
153
+ writable: true,
154
+ value: void 0
155
+ });
144
156
  this._stream = messageStream;
145
157
  this._control = control;
146
158
  this._sandbox = sandbox;
@@ -148,6 +160,8 @@ class CommandHandle {
148
160
  this._lastStderrOffset = options?.stderrOffset ?? 0;
149
161
  this._onStdout = options?.onStdout;
150
162
  this._onStderr = options?.onStderr;
163
+ this._stdinClosed = options?.stdinClosed ?? false;
164
+ this._pty = options?.pty ?? false;
151
165
  // New executions (no commandId): _ensureStarted reads "started".
152
166
  // Reconnections (commandId set): skip since reconnect streams
153
167
  // don't send a "started" message.
@@ -319,10 +333,28 @@ class CommandHandle {
319
333
  * Write data to the command's stdin.
320
334
  */
321
335
  sendInput(data) {
336
+ if (this._stdinClosed) {
337
+ throw new errors_js_1.LangSmithSandboxOperationError("stdin is closed for this command. Non-PTY commands close stdin by " +
338
+ "default so a command that reads it sees EOF instead of hanging; " +
339
+ "pass closeInput: false to run() to stream input into it.", "sendInput");
340
+ }
322
341
  if (this._control) {
323
342
  this._control.sendInput(data);
324
343
  }
325
344
  }
345
+ /**
346
+ * Half-close stdin so the command reads EOF.
347
+ *
348
+ * Idempotent, and a no-op under a PTY, where input and output share one
349
+ * terminal file descriptor and there is no write end to close -- send an
350
+ * EOT byte (0x04) with {@link sendInput} instead.
351
+ */
352
+ closeInput() {
353
+ if (this._pty || this._stdinClosed)
354
+ return;
355
+ this._stdinClosed = true;
356
+ this._control?.sendCloseStdin();
357
+ }
326
358
  /** Last known stdout byte offset (for manual reconnection). */
327
359
  get lastStdoutOffset() {
328
360
  return this._lastStdoutOffset;
@@ -344,6 +376,8 @@ class CommandHandle {
344
376
  return this._sandbox.reconnect(this._commandId, {
345
377
  stdoutOffset: this._lastStdoutOffset,
346
378
  stderrOffset: this._lastStderrOffset,
379
+ stdinClosed: this._stdinClosed,
380
+ pty: this._pty,
347
381
  });
348
382
  }
349
383
  }
@@ -48,6 +48,8 @@ export declare class CommandHandle {
48
48
  private _reconnectAttempts;
49
49
  private _onStdout?;
50
50
  private _onStderr?;
51
+ private _stdinClosed;
52
+ private _pty;
51
53
  /**
52
54
  * Read the 'started' message to populate commandId and pid.
53
55
  *
@@ -87,6 +89,14 @@ export declare class CommandHandle {
87
89
  * Write data to the command's stdin.
88
90
  */
89
91
  sendInput(data: string): void;
92
+ /**
93
+ * Half-close stdin so the command reads EOF.
94
+ *
95
+ * Idempotent, and a no-op under a PTY, where input and output share one
96
+ * terminal file descriptor and there is no write end to close -- send an
97
+ * EOT byte (0x04) with {@link sendInput} instead.
98
+ */
99
+ closeInput(): void;
90
100
  /** Last known stdout byte offset (for manual reconnection). */
91
101
  get lastStdoutOffset(): number;
92
102
  /** Last known stderr byte offset (for manual reconnection). */
@@ -138,6 +138,18 @@ export class CommandHandle {
138
138
  writable: true,
139
139
  value: void 0
140
140
  });
141
+ Object.defineProperty(this, "_stdinClosed", {
142
+ enumerable: true,
143
+ configurable: true,
144
+ writable: true,
145
+ value: void 0
146
+ });
147
+ Object.defineProperty(this, "_pty", {
148
+ enumerable: true,
149
+ configurable: true,
150
+ writable: true,
151
+ value: void 0
152
+ });
141
153
  this._stream = messageStream;
142
154
  this._control = control;
143
155
  this._sandbox = sandbox;
@@ -145,6 +157,8 @@ export class CommandHandle {
145
157
  this._lastStderrOffset = options?.stderrOffset ?? 0;
146
158
  this._onStdout = options?.onStdout;
147
159
  this._onStderr = options?.onStderr;
160
+ this._stdinClosed = options?.stdinClosed ?? false;
161
+ this._pty = options?.pty ?? false;
148
162
  // New executions (no commandId): _ensureStarted reads "started".
149
163
  // Reconnections (commandId set): skip since reconnect streams
150
164
  // don't send a "started" message.
@@ -316,10 +330,28 @@ export class CommandHandle {
316
330
  * Write data to the command's stdin.
317
331
  */
318
332
  sendInput(data) {
333
+ if (this._stdinClosed) {
334
+ throw new LangSmithSandboxOperationError("stdin is closed for this command. Non-PTY commands close stdin by " +
335
+ "default so a command that reads it sees EOF instead of hanging; " +
336
+ "pass closeInput: false to run() to stream input into it.", "sendInput");
337
+ }
319
338
  if (this._control) {
320
339
  this._control.sendInput(data);
321
340
  }
322
341
  }
342
+ /**
343
+ * Half-close stdin so the command reads EOF.
344
+ *
345
+ * Idempotent, and a no-op under a PTY, where input and output share one
346
+ * terminal file descriptor and there is no write end to close -- send an
347
+ * EOT byte (0x04) with {@link sendInput} instead.
348
+ */
349
+ closeInput() {
350
+ if (this._pty || this._stdinClosed)
351
+ return;
352
+ this._stdinClosed = true;
353
+ this._control?.sendCloseStdin();
354
+ }
323
355
  /** Last known stdout byte offset (for manual reconnection). */
324
356
  get lastStdoutOffset() {
325
357
  return this._lastStdoutOffset;
@@ -341,6 +373,8 @@ export class CommandHandle {
341
373
  return this._sandbox.reconnect(this._commandId, {
342
374
  stdoutOffset: this._lastStdoutOffset,
343
375
  stderrOffset: this._lastStderrOffset,
376
+ stdinClosed: this._stdinClosed,
377
+ pty: this._pty,
344
378
  });
345
379
  }
346
380
  }
@@ -14,6 +14,12 @@ exports.handleSandboxCreationError = handleSandboxCreationError;
14
14
  exports.throwIfNotReady = throwIfNotReady;
15
15
  exports.handleClientHttpError = handleClientHttpError;
16
16
  exports.handleSandboxHttpError = handleSandboxHttpError;
17
+ exports.assertRunConfigNotCombined = assertRunConfigNotCombined;
18
+ exports.resolveCloseInput = resolveCloseInput;
19
+ exports.buildRangeHeader = buildRangeHeader;
20
+ exports.fileStatFromResponse = fileStatFromResponse;
21
+ exports.fileChunkFromResponse = fileChunkFromResponse;
22
+ exports.handleFileHttpError = handleFileHttpError;
17
23
  const errors_js_1 = require("./errors.cjs");
18
24
  // =============================================================================
19
25
  // Input validation
@@ -268,3 +274,126 @@ async function handleSandboxHttpError(response) {
268
274
  }
269
275
  throw new errors_js_1.LangSmithSandboxError(message);
270
276
  }
277
+ // =============================================================================
278
+ // Run configuration
279
+ // =============================================================================
280
+ /**
281
+ * Reject a per-command `runConfig` alongside the deprecated `env`/`cwd`.
282
+ *
283
+ * The server answers 400 for a request carrying both spellings rather than
284
+ * letting one silently win, so refuse it here where the message can name the
285
+ * replacement.
286
+ */
287
+ function assertRunConfigNotCombined(options) {
288
+ if (options.runConfig !== undefined &&
289
+ (options.env !== undefined || options.cwd !== undefined)) {
290
+ throw new errors_js_1.LangSmithValidationError("Cannot combine runConfig with the deprecated env/cwd options. " +
291
+ "Use runConfig.env_vars and runConfig.work_dir instead.");
292
+ }
293
+ }
294
+ /**
295
+ * Whether to half-close stdin at spawn.
296
+ *
297
+ * Defaults on for a non-PTY command: a command that reads stdin otherwise
298
+ * blocks on a pipe nobody writes to until the timeout kills it. A PTY has no
299
+ * separate write end to close, so the server ignores the flag there.
300
+ */
301
+ function resolveCloseInput(closeInput, pty) {
302
+ if (pty)
303
+ return false;
304
+ return closeInput ?? true;
305
+ }
306
+ // =============================================================================
307
+ // Ranged downloads
308
+ // =============================================================================
309
+ /** Render a byte range as an RFC 9110 `Range` header value. */
310
+ function buildRangeHeader(options) {
311
+ const { start, end, suffixBytes } = options;
312
+ if (suffixBytes !== undefined) {
313
+ if (start !== undefined || end !== undefined) {
314
+ throw new errors_js_1.LangSmithValidationError("Cannot combine suffixBytes with start/end.");
315
+ }
316
+ if (suffixBytes <= 0) {
317
+ throw new errors_js_1.LangSmithValidationError("suffixBytes must be positive.");
318
+ }
319
+ return `bytes=-${suffixBytes}`;
320
+ }
321
+ if (start === undefined) {
322
+ throw new errors_js_1.LangSmithValidationError("Provide start (with optional end), or suffixBytes.");
323
+ }
324
+ if (start < 0) {
325
+ throw new errors_js_1.LangSmithValidationError("start must not be negative.");
326
+ }
327
+ if (end === undefined) {
328
+ return `bytes=${start}-`;
329
+ }
330
+ if (end < start) {
331
+ throw new errors_js_1.LangSmithValidationError("end must not precede start.");
332
+ }
333
+ return `bytes=${start}-${end}`;
334
+ }
335
+ /** Read the first byte offset and total size out of `Content-Range`. */
336
+ function parseContentRange(value) {
337
+ if (!value || !value.startsWith("bytes "))
338
+ return { start: 0 };
339
+ const spec = value.slice("bytes ".length).trim();
340
+ const [rangePart, totalPart] = spec.split("/");
341
+ const first = rangePart.split("-")[0].trim();
342
+ const start = /^\d+$/.test(first) ? Number(first) : 0;
343
+ const total = /^\d+$/.test((totalPart ?? "").trim())
344
+ ? Number(totalPart)
345
+ : undefined;
346
+ return { start, total };
347
+ }
348
+ /** Build a FileStat from a HEAD response's headers. */
349
+ function fileStatFromResponse(response) {
350
+ const length = response.headers.get("content-length");
351
+ return {
352
+ size_bytes: length && /^\d+$/.test(length) ? Number(length) : 0,
353
+ etag: response.headers.get("etag") ?? undefined,
354
+ last_modified: response.headers.get("last-modified") ?? undefined,
355
+ content_type: response.headers.get("content-type") ?? undefined,
356
+ };
357
+ }
358
+ /** Build a FileChunk from a ranged download response. */
359
+ async function fileChunkFromResponse(response) {
360
+ const etag = response.headers.get("etag") ?? undefined;
361
+ const lastModified = response.headers.get("last-modified") ?? undefined;
362
+ if (response.status === 304) {
363
+ return {
364
+ content: new Uint8Array(0),
365
+ etag,
366
+ start: 0,
367
+ partial: false,
368
+ unchanged: true,
369
+ last_modified: lastModified,
370
+ };
371
+ }
372
+ const content = new Uint8Array(await response.arrayBuffer());
373
+ const partial = response.status === 206;
374
+ // A stale If-Range answers 200 with the whole file; the caller has to
375
+ // restart rather than append, so report it from byte zero.
376
+ const { start, total } = partial
377
+ ? parseContentRange(response.headers.get("content-range"))
378
+ : { start: 0, total: content.length };
379
+ return {
380
+ content,
381
+ etag,
382
+ total_bytes: total,
383
+ start,
384
+ partial,
385
+ unchanged: false,
386
+ last_modified: lastModified,
387
+ };
388
+ }
389
+ /** Map a file-operation HTTP error, including the non-JSON 416. */
390
+ async function handleFileHttpError(response, path, sandboxName) {
391
+ if (response.status === 404) {
392
+ throw new errors_js_1.LangSmithResourceNotFoundError(`File '${path}' not found in sandbox '${sandboxName}'`, "file");
393
+ }
394
+ if (response.status === 416) {
395
+ throw new errors_js_1.LangSmithSandboxOperationError(`Requested range for '${path}' starts past the end of the file ` +
396
+ `(${response.headers.get("content-range") ?? "unknown size"}).`, "read");
397
+ }
398
+ return handleSandboxHttpError(response);
399
+ }
@@ -4,6 +4,7 @@
4
4
  * These functions are used to parse error responses and raise appropriate
5
5
  * exceptions. They contain no I/O operations.
6
6
  */
7
+ import type { FileChunk, FileStat, ReadRangeOptions, RunConfig } from "./types.js";
7
8
  /**
8
9
  * Validate TTL values for sandbox create/update (minute resolution).
9
10
  *
@@ -76,4 +77,32 @@ export declare function handleClientHttpError(response: Response): Promise<never
76
77
  * - 403 -> LangSmithSandboxOperationError (permission denied)
77
78
  */
78
79
  export declare function handleSandboxHttpError(response: Response): Promise<never>;
80
+ /**
81
+ * Reject a per-command `runConfig` alongside the deprecated `env`/`cwd`.
82
+ *
83
+ * The server answers 400 for a request carrying both spellings rather than
84
+ * letting one silently win, so refuse it here where the message can name the
85
+ * replacement.
86
+ */
87
+ export declare function assertRunConfigNotCombined(options: {
88
+ runConfig?: RunConfig;
89
+ env?: Record<string, string>;
90
+ cwd?: string;
91
+ }): void;
92
+ /**
93
+ * Whether to half-close stdin at spawn.
94
+ *
95
+ * Defaults on for a non-PTY command: a command that reads stdin otherwise
96
+ * blocks on a pipe nobody writes to until the timeout kills it. A PTY has no
97
+ * separate write end to close, so the server ignores the flag there.
98
+ */
99
+ export declare function resolveCloseInput(closeInput: boolean | undefined, pty: boolean): boolean;
100
+ /** Render a byte range as an RFC 9110 `Range` header value. */
101
+ export declare function buildRangeHeader(options: ReadRangeOptions): string;
102
+ /** Build a FileStat from a HEAD response's headers. */
103
+ export declare function fileStatFromResponse(response: Response): FileStat;
104
+ /** Build a FileChunk from a ranged download response. */
105
+ export declare function fileChunkFromResponse(response: Response): Promise<FileChunk>;
106
+ /** Map a file-operation HTTP error, including the non-JSON 416. */
107
+ export declare function handleFileHttpError(response: Response, path: string, sandboxName: string): Promise<never>;
79
108
  export {};
@@ -258,3 +258,126 @@ export async function handleSandboxHttpError(response) {
258
258
  }
259
259
  throw new LangSmithSandboxError(message);
260
260
  }
261
+ // =============================================================================
262
+ // Run configuration
263
+ // =============================================================================
264
+ /**
265
+ * Reject a per-command `runConfig` alongside the deprecated `env`/`cwd`.
266
+ *
267
+ * The server answers 400 for a request carrying both spellings rather than
268
+ * letting one silently win, so refuse it here where the message can name the
269
+ * replacement.
270
+ */
271
+ export function assertRunConfigNotCombined(options) {
272
+ if (options.runConfig !== undefined &&
273
+ (options.env !== undefined || options.cwd !== undefined)) {
274
+ throw new LangSmithValidationError("Cannot combine runConfig with the deprecated env/cwd options. " +
275
+ "Use runConfig.env_vars and runConfig.work_dir instead.");
276
+ }
277
+ }
278
+ /**
279
+ * Whether to half-close stdin at spawn.
280
+ *
281
+ * Defaults on for a non-PTY command: a command that reads stdin otherwise
282
+ * blocks on a pipe nobody writes to until the timeout kills it. A PTY has no
283
+ * separate write end to close, so the server ignores the flag there.
284
+ */
285
+ export function resolveCloseInput(closeInput, pty) {
286
+ if (pty)
287
+ return false;
288
+ return closeInput ?? true;
289
+ }
290
+ // =============================================================================
291
+ // Ranged downloads
292
+ // =============================================================================
293
+ /** Render a byte range as an RFC 9110 `Range` header value. */
294
+ export function buildRangeHeader(options) {
295
+ const { start, end, suffixBytes } = options;
296
+ if (suffixBytes !== undefined) {
297
+ if (start !== undefined || end !== undefined) {
298
+ throw new LangSmithValidationError("Cannot combine suffixBytes with start/end.");
299
+ }
300
+ if (suffixBytes <= 0) {
301
+ throw new LangSmithValidationError("suffixBytes must be positive.");
302
+ }
303
+ return `bytes=-${suffixBytes}`;
304
+ }
305
+ if (start === undefined) {
306
+ throw new LangSmithValidationError("Provide start (with optional end), or suffixBytes.");
307
+ }
308
+ if (start < 0) {
309
+ throw new LangSmithValidationError("start must not be negative.");
310
+ }
311
+ if (end === undefined) {
312
+ return `bytes=${start}-`;
313
+ }
314
+ if (end < start) {
315
+ throw new LangSmithValidationError("end must not precede start.");
316
+ }
317
+ return `bytes=${start}-${end}`;
318
+ }
319
+ /** Read the first byte offset and total size out of `Content-Range`. */
320
+ function parseContentRange(value) {
321
+ if (!value || !value.startsWith("bytes "))
322
+ return { start: 0 };
323
+ const spec = value.slice("bytes ".length).trim();
324
+ const [rangePart, totalPart] = spec.split("/");
325
+ const first = rangePart.split("-")[0].trim();
326
+ const start = /^\d+$/.test(first) ? Number(first) : 0;
327
+ const total = /^\d+$/.test((totalPart ?? "").trim())
328
+ ? Number(totalPart)
329
+ : undefined;
330
+ return { start, total };
331
+ }
332
+ /** Build a FileStat from a HEAD response's headers. */
333
+ export function fileStatFromResponse(response) {
334
+ const length = response.headers.get("content-length");
335
+ return {
336
+ size_bytes: length && /^\d+$/.test(length) ? Number(length) : 0,
337
+ etag: response.headers.get("etag") ?? undefined,
338
+ last_modified: response.headers.get("last-modified") ?? undefined,
339
+ content_type: response.headers.get("content-type") ?? undefined,
340
+ };
341
+ }
342
+ /** Build a FileChunk from a ranged download response. */
343
+ export async function fileChunkFromResponse(response) {
344
+ const etag = response.headers.get("etag") ?? undefined;
345
+ const lastModified = response.headers.get("last-modified") ?? undefined;
346
+ if (response.status === 304) {
347
+ return {
348
+ content: new Uint8Array(0),
349
+ etag,
350
+ start: 0,
351
+ partial: false,
352
+ unchanged: true,
353
+ last_modified: lastModified,
354
+ };
355
+ }
356
+ const content = new Uint8Array(await response.arrayBuffer());
357
+ const partial = response.status === 206;
358
+ // A stale If-Range answers 200 with the whole file; the caller has to
359
+ // restart rather than append, so report it from byte zero.
360
+ const { start, total } = partial
361
+ ? parseContentRange(response.headers.get("content-range"))
362
+ : { start: 0, total: content.length };
363
+ return {
364
+ content,
365
+ etag,
366
+ total_bytes: total,
367
+ start,
368
+ partial,
369
+ unchanged: false,
370
+ last_modified: lastModified,
371
+ };
372
+ }
373
+ /** Map a file-operation HTTP error, including the non-JSON 416. */
374
+ export async function handleFileHttpError(response, path, sandboxName) {
375
+ if (response.status === 404) {
376
+ throw new LangSmithResourceNotFoundError(`File '${path}' not found in sandbox '${sandboxName}'`, "file");
377
+ }
378
+ if (response.status === 416) {
379
+ throw new LangSmithSandboxOperationError(`Requested range for '${path}' starts past the end of the file ` +
380
+ `(${response.headers.get("content-range") ?? "unknown size"}).`, "read");
381
+ }
382
+ return handleSandboxHttpError(response);
383
+ }
@@ -29,7 +29,7 @@
29
29
  * @packageDocumentation
30
30
  */
31
31
  Object.defineProperty(exports, "__esModule", { value: true });
32
- exports.LangSmithDataplaneNotConfiguredError = exports.LangSmithCommandTimeoutError = exports.LangSmithSandboxOperationError = exports.LangSmithSandboxNotReadyError = exports.LangSmithSandboxCreationError = exports.LangSmithResourceCreationError = exports.LangSmithQuotaExceededError = exports.LangSmithValidationError = exports.LangSmithResourceNameConflictError = exports.LangSmithResourceAlreadyExistsError = exports.LangSmithResourceInUseError = exports.LangSmithResourceTimeoutError = exports.LangSmithResourceNotFoundError = exports.LangSmithSandboxServerReloadError = exports.LangSmithSandboxConnectTimeoutError = exports.LangSmithSandboxRetryableConnectionError = exports.LangSmithSandboxConnectionError = exports.LangSmithSandboxAuthenticationError = exports.LangSmithSandboxAPIError = exports.LangSmithSandboxError = exports.s3Mount = exports.mountConfig = exports.gitMount = exports.gcsMount = exports.contextHubMount = exports.workspaceSecret = exports.proxyConfig = exports.opaqueSecret = exports.gcpAuth = exports.awsAuth = exports.CommandHandle = exports.Sandbox = exports.SandboxClient = void 0;
32
+ exports.LangSmithDataplaneNotConfiguredError = exports.LangSmithCommandTimeoutError = exports.LangSmithSandboxOperationError = exports.LangSmithSandboxNotReadyError = exports.LangSmithSandboxCreationError = exports.LangSmithResourceCreationError = exports.LangSmithQuotaExceededError = exports.LangSmithValidationError = exports.LangSmithResourceNameConflictError = exports.LangSmithResourceAlreadyExistsError = exports.LangSmithResourceInUseError = exports.LangSmithResourceTimeoutError = exports.LangSmithResourceNotFoundError = exports.LangSmithSandboxServerReloadError = exports.LangSmithSandboxConnectTimeoutError = exports.LangSmithSandboxRetryableConnectionError = exports.LangSmithSandboxConnectionError = exports.LangSmithSandboxAuthenticationError = exports.LangSmithSandboxAPIError = exports.LangSmithSandboxError = exports.s3Mount = exports.mountConfig = exports.gitMount = exports.gcsMount = exports.contextHubMount = exports.SERVICE_TOKEN_HEADER = exports.ServiceLoginUrl = exports.ServiceUrl = exports.validateAccessDelegation = exports.workspaceSecret = exports.proxyConfig = exports.opaqueSecret = exports.gcpAuth = exports.awsAuth = exports.CommandHandle = exports.Sandbox = exports.SandboxClient = void 0;
33
33
  // Main classes
34
34
  var client_js_1 = require("./client.cjs");
35
35
  Object.defineProperty(exports, "SandboxClient", { enumerable: true, get: function () { return client_js_1.SandboxClient; } });
@@ -43,6 +43,12 @@ Object.defineProperty(exports, "gcpAuth", { enumerable: true, get: function () {
43
43
  Object.defineProperty(exports, "opaqueSecret", { enumerable: true, get: function () { return proxy_config_js_1.opaqueSecret; } });
44
44
  Object.defineProperty(exports, "proxyConfig", { enumerable: true, get: function () { return proxy_config_js_1.proxyConfig; } });
45
45
  Object.defineProperty(exports, "workspaceSecret", { enumerable: true, get: function () { return proxy_config_js_1.workspaceSecret; } });
46
+ var access_delegation_js_1 = require("./access_delegation.cjs");
47
+ Object.defineProperty(exports, "validateAccessDelegation", { enumerable: true, get: function () { return access_delegation_js_1.validateAccessDelegation; } });
48
+ var service_url_js_1 = require("./service_url.cjs");
49
+ Object.defineProperty(exports, "ServiceUrl", { enumerable: true, get: function () { return service_url_js_1.ServiceUrl; } });
50
+ Object.defineProperty(exports, "ServiceLoginUrl", { enumerable: true, get: function () { return service_url_js_1.ServiceLoginUrl; } });
51
+ Object.defineProperty(exports, "SERVICE_TOKEN_HEADER", { enumerable: true, get: function () { return service_url_js_1.SERVICE_TOKEN_HEADER; } });
46
52
  var mounts_js_1 = require("./mounts.cjs");
47
53
  Object.defineProperty(exports, "contextHubMount", { enumerable: true, get: function () { return mounts_js_1.contextHubMount; } });
48
54
  Object.defineProperty(exports, "gcsMount", { enumerable: true, get: function () { return mounts_js_1.gcsMount; } });
@@ -31,6 +31,10 @@ export { SandboxClient } from "./client.js";
31
31
  export { Sandbox } from "./sandbox.js";
32
32
  export { CommandHandle } from "./command_handle.js";
33
33
  export { awsAuth, gcpAuth, opaqueSecret, proxyConfig, workspaceSecret, } from "./proxy_config.js";
34
+ export { validateAccessDelegation } from "./access_delegation.js";
35
+ export { ServiceUrl, ServiceLoginUrl, SERVICE_TOKEN_HEADER, } from "./service_url.js";
34
36
  export { contextHubMount, gcsMount, gitMount, mountConfig, s3Mount, } from "./mounts.js";
35
- export type { ExecutionResult, OutputChunk, WsMessage, WsRunOptions, ResourceStatus, Snapshot, SandboxData, SandboxClientConfig, RunOptions, CreateSandboxOptions, SandboxAccessControl, SandboxAwsAuthRule, SandboxAwsMountAuthConfig, SandboxGcpAuthRule, SandboxGcpMountAuthConfig, SandboxMountAuth, SandboxMountAuthConfig, SandboxMountConfig, SandboxProxyConfig, SandboxProxyRule, SandboxProxySecret, SandboxMount, MountCacheConfig, ContextHubMountConfig, ContextHubMountSpec, GCSMountConfig, GCSMountSpec, GitMountConfig, GitMountRefSpec, GitMountSpec, S3MountConfig, S3MountSpec, CreateSnapshotOptions, CreateDockerfileSnapshotOptions, CaptureSnapshotOptions, DownloadContentDisposition, DownloadURL, GenerateDownloadURLOptions, ListSnapshotsOptions, WaitForSnapshotOptions, StartSandboxOptions, UpdateSandboxOptions, WaitForSandboxOptions, } from "./types.js";
37
+ export type { AccessDelegation, AccessDelegationMode, } from "./access_delegation.js";
38
+ export type { ServiceAccess, ServiceUrlData } from "./service_url.js";
39
+ export type { ExecutionResult, FileChunk, FileInfo, FileStat, GlobOptions, GlobResult, GrepMatch, GrepOptions, GrepResult, ReadRangeOptions, RunConfig, OutputChunk, WsMessage, WsRunOptions, ResourceStatus, Snapshot, SandboxData, SandboxClientConfig, RunOptions, CreateSandboxOptions, SandboxAccessControl, SandboxAwsAuthRule, SandboxAwsMountAuthConfig, SandboxGcpAuthRule, SandboxGcpMountAuthConfig, SandboxMountAuth, SandboxMountAuthConfig, SandboxMountConfig, SandboxProxyConfig, SandboxProxyRule, SandboxProxySecret, SandboxMount, MountCacheConfig, ContextHubMountConfig, ContextHubMountSpec, GCSMountConfig, GCSMountSpec, GitMountConfig, GitMountRefSpec, GitMountSpec, S3MountConfig, S3MountSpec, CreateSnapshotOptions, CreateDockerfileSnapshotOptions, CaptureSnapshotOptions, DownloadContentDisposition, DownloadURL, GenerateDownloadURLOptions, ListSnapshotsOptions, WaitForSnapshotOptions, StartSandboxOptions, UpdateSandboxOptions, WaitForSandboxOptions, } from "./types.js";
36
40
  export { LangSmithSandboxError, LangSmithSandboxAPIError, LangSmithSandboxAuthenticationError, LangSmithSandboxConnectionError, LangSmithSandboxRetryableConnectionError, LangSmithSandboxConnectTimeoutError, LangSmithSandboxServerReloadError, LangSmithResourceNotFoundError, LangSmithResourceTimeoutError, LangSmithResourceInUseError, LangSmithResourceAlreadyExistsError, LangSmithResourceNameConflictError, LangSmithValidationError, LangSmithQuotaExceededError, LangSmithResourceCreationError, LangSmithSandboxCreationError, LangSmithSandboxNotReadyError, LangSmithSandboxOperationError, LangSmithCommandTimeoutError, LangSmithDataplaneNotConfiguredError, } from "./errors.js";
@@ -32,6 +32,8 @@ export { SandboxClient } from "./client.js";
32
32
  export { Sandbox } from "./sandbox.js";
33
33
  export { CommandHandle } from "./command_handle.js";
34
34
  export { awsAuth, gcpAuth, opaqueSecret, proxyConfig, workspaceSecret, } from "./proxy_config.js";
35
+ export { validateAccessDelegation } from "./access_delegation.js";
36
+ export { ServiceUrl, ServiceLoginUrl, SERVICE_TOKEN_HEADER, } from "./service_url.js";
35
37
  export { contextHubMount, gcsMount, gitMount, mountConfig, s3Mount, } from "./mounts.js";
36
38
  // Errors
37
39
  export {