langsmith 0.10.3 → 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.
- package/dist/client.cjs +19 -4
- package/dist/client.d.ts +2 -1
- package/dist/client.js +19 -4
- package/dist/evaluation/_runner.cjs +47 -5
- package/dist/evaluation/_runner.d.ts +12 -0
- package/dist/evaluation/_runner.js +47 -5
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/sandbox/access_delegation.cjs +24 -0
- package/dist/sandbox/access_delegation.d.ts +15 -0
- package/dist/sandbox/access_delegation.js +21 -0
- package/dist/sandbox/client.cjs +70 -5
- package/dist/sandbox/client.d.ts +18 -0
- package/dist/sandbox/client.js +70 -5
- package/dist/sandbox/command_handle.cjs +34 -0
- package/dist/sandbox/command_handle.d.ts +10 -0
- package/dist/sandbox/command_handle.js +34 -0
- package/dist/sandbox/helpers.cjs +129 -0
- package/dist/sandbox/helpers.d.ts +29 -0
- package/dist/sandbox/helpers.js +123 -0
- package/dist/sandbox/index.cjs +7 -1
- package/dist/sandbox/index.d.ts +5 -1
- package/dist/sandbox/index.js +2 -0
- package/dist/sandbox/mounts.cjs +21 -3
- package/dist/sandbox/mounts.d.ts +7 -1
- package/dist/sandbox/mounts.js +21 -3
- package/dist/sandbox/proxy_config.cjs +41 -10
- package/dist/sandbox/proxy_config.d.ts +33 -6
- package/dist/sandbox/proxy_config.js +40 -10
- package/dist/sandbox/sandbox.cjs +160 -4
- package/dist/sandbox/sandbox.d.ts +71 -1
- package/dist/sandbox/sandbox.js +161 -5
- package/dist/sandbox/service_url.cjs +125 -0
- package/dist/sandbox/service_url.d.ts +50 -0
- package/dist/sandbox/service_url.js +120 -0
- package/dist/sandbox/types.d.ts +251 -8
- package/dist/sandbox/ws_execute.cjs +31 -1
- package/dist/sandbox/ws_execute.d.ts +9 -0
- package/dist/sandbox/ws_execute.js +31 -1
- package/dist/utils/error.cjs +17 -22
- package/dist/utils/error.js +17 -22
- package/dist/wrappers/openai_agents.cjs +37 -28
- package/dist/wrappers/openai_agents.d.ts +0 -1
- package/dist/wrappers/openai_agents.js +37 -28
- package/package.json +2 -2
package/dist/sandbox/client.cjs
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.SandboxClient = void 0;
|
|
7
|
+
const access_delegation_js_1 = require("./access_delegation.cjs");
|
|
8
|
+
const service_url_js_1 = require("./service_url.cjs");
|
|
7
9
|
const env_js_1 = require("../utils/env.cjs");
|
|
8
10
|
const fetch_js_1 = require("../singletons/fetch.cjs");
|
|
9
11
|
const async_caller_js_1 = require("../utils/async_caller.cjs");
|
|
@@ -407,6 +409,53 @@ class SandboxClient {
|
|
|
407
409
|
const suffix = segments.length ? `/${segments.join("/")}` : "";
|
|
408
410
|
return `${this._baseUrl}/boxes/${encodeURIComponent(name)}${suffix}`;
|
|
409
411
|
}
|
|
412
|
+
/**
|
|
413
|
+
* Generate an authenticated URL for an HTTP service running in a sandbox.
|
|
414
|
+
*
|
|
415
|
+
* Without `access`, mints a short-lived token and returns a {@link ServiceUrl}
|
|
416
|
+
* that refreshes it as it nears expiry. With `access` set to `"restricted"`
|
|
417
|
+
* or `"workspace"` the URL is gated by LangSmith login instead and a
|
|
418
|
+
* {@link ServiceLoginUrl} is returned: no token, no expiry, browser only.
|
|
419
|
+
*
|
|
420
|
+
* A login grant is durable, so token mode is refused with 409 while one is
|
|
421
|
+
* in place, and `expiresInSeconds` does not apply to the login modes.
|
|
422
|
+
*/
|
|
423
|
+
async serviceUrl(name, options) {
|
|
424
|
+
const { port, expiresInSeconds, access, signal } = options;
|
|
425
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
426
|
+
throw new errors_js_1.LangSmithValidationError(`port must be between 1 and 65535 (got ${port})`, "port");
|
|
427
|
+
}
|
|
428
|
+
if (access !== undefined &&
|
|
429
|
+
access !== "restricted" &&
|
|
430
|
+
access !== "workspace") {
|
|
431
|
+
throw new errors_js_1.LangSmithValidationError(`access must be "restricted" or "workspace" (got ${String(access)})`, "access");
|
|
432
|
+
}
|
|
433
|
+
const loginMode = access !== undefined;
|
|
434
|
+
if (expiresInSeconds !== undefined && loginMode) {
|
|
435
|
+
throw new errors_js_1.LangSmithValidationError(`expiresInSeconds does not apply to access "${access}": a LangSmith ` +
|
|
436
|
+
"login URL carries no token and does not expire", "expiresInSeconds");
|
|
437
|
+
}
|
|
438
|
+
const url = this._boxUrl(name, "service-url");
|
|
439
|
+
const payload = { port };
|
|
440
|
+
if (!loginMode && expiresInSeconds !== undefined) {
|
|
441
|
+
payload.expires_in_seconds = expiresInSeconds;
|
|
442
|
+
}
|
|
443
|
+
if (access !== undefined) {
|
|
444
|
+
payload.access = access;
|
|
445
|
+
}
|
|
446
|
+
const response = await this._postJson(url, payload, { signal });
|
|
447
|
+
const data = await response.json();
|
|
448
|
+
if (loginMode) {
|
|
449
|
+
return new service_url_js_1.ServiceLoginUrl(data);
|
|
450
|
+
}
|
|
451
|
+
// Refresh with the token parameters only. Carrying options.signal over
|
|
452
|
+
// would tie every later refresh to the first request's lifetime, so a
|
|
453
|
+
// caller-supplied timeout signal would leave the URL unable to refresh.
|
|
454
|
+
return new service_url_js_1.ServiceUrl(data, () => this.serviceUrl(name, {
|
|
455
|
+
port,
|
|
456
|
+
expiresInSeconds,
|
|
457
|
+
}));
|
|
458
|
+
}
|
|
410
459
|
async _postJson(url, body, options) {
|
|
411
460
|
const response = await this._fetch(url, {
|
|
412
461
|
method: "POST",
|
|
@@ -424,7 +473,7 @@ class SandboxClient {
|
|
|
424
473
|
const resolvedOptions = typeof snapshotIdOrOptions === "object" && snapshotIdOrOptions !== null
|
|
425
474
|
? snapshotIdOrOptions
|
|
426
475
|
: options;
|
|
427
|
-
const { snapshotName, name, timeout = 30, waitForReady = true, idleTtlSeconds, deleteAfterStopSeconds, vCpus, memBytes, fsCapacityBytes, mountConfig, proxyConfig, } = resolvedOptions;
|
|
476
|
+
const { snapshotName, name, timeout = 30, waitForReady = true, idleTtlSeconds, deleteAfterStopSeconds, vCpus, memBytes, fsCapacityBytes, mountConfig, proxyConfig, runConfig, accessDelegation, } = resolvedOptions;
|
|
428
477
|
if (snapshotId && snapshotName) {
|
|
429
478
|
throw new errors_js_1.LangSmithValidationError("At most one of snapshotId or options.snapshotName may be set", "snapshotId");
|
|
430
479
|
}
|
|
@@ -468,6 +517,12 @@ class SandboxClient {
|
|
|
468
517
|
if (proxyConfig !== undefined) {
|
|
469
518
|
payload.proxy_config = proxyConfig;
|
|
470
519
|
}
|
|
520
|
+
if (runConfig !== undefined) {
|
|
521
|
+
payload.run_config = runConfig;
|
|
522
|
+
}
|
|
523
|
+
if (accessDelegation !== undefined) {
|
|
524
|
+
payload.access_delegation = (0, access_delegation_js_1.validateAccessDelegation)(accessDelegation);
|
|
525
|
+
}
|
|
471
526
|
const httpTimeout = waitForReady ? (timeout + 30) * 1000 : 30 * 1000;
|
|
472
527
|
const response = await this._fetch(url, {
|
|
473
528
|
method: "POST",
|
|
@@ -523,13 +578,14 @@ class SandboxClient {
|
|
|
523
578
|
const options = typeof newNameOrOptions === "string"
|
|
524
579
|
? { newName: newNameOrOptions }
|
|
525
580
|
: newNameOrOptions;
|
|
526
|
-
const { newName, idleTtlSeconds, deleteAfterStopSeconds, proxyConfig } = options;
|
|
581
|
+
const { newName, idleTtlSeconds, deleteAfterStopSeconds, proxyConfig, runConfig, } = options;
|
|
527
582
|
(0, helpers_js_1.validateTtl)(idleTtlSeconds, "idleTtlSeconds");
|
|
528
583
|
(0, helpers_js_1.validateTtl)(deleteAfterStopSeconds, "deleteAfterStopSeconds");
|
|
529
584
|
if (newName === undefined &&
|
|
530
585
|
idleTtlSeconds === undefined &&
|
|
531
586
|
deleteAfterStopSeconds === undefined &&
|
|
532
|
-
proxyConfig === undefined
|
|
587
|
+
proxyConfig === undefined &&
|
|
588
|
+
runConfig === undefined) {
|
|
533
589
|
return this.getSandbox(name);
|
|
534
590
|
}
|
|
535
591
|
const url = this._boxUrl(name);
|
|
@@ -546,6 +602,9 @@ class SandboxClient {
|
|
|
546
602
|
if (proxyConfig !== undefined) {
|
|
547
603
|
payload.proxy_config = proxyConfig;
|
|
548
604
|
}
|
|
605
|
+
if (runConfig !== undefined) {
|
|
606
|
+
payload.run_config = runConfig;
|
|
607
|
+
}
|
|
549
608
|
const response = await this._fetch(url, {
|
|
550
609
|
method: "PATCH",
|
|
551
610
|
headers: { "Content-Type": "application/json" },
|
|
@@ -730,7 +789,7 @@ class SandboxClient {
|
|
|
730
789
|
* @returns Snapshot in "ready" status.
|
|
731
790
|
*/
|
|
732
791
|
async createSnapshot(name, dockerImage, fsCapacityBytes, options = {}) {
|
|
733
|
-
const { registryId, timeout = 60, signal } = options;
|
|
792
|
+
const { registryId, runConfig, timeout = 60, signal } = options;
|
|
734
793
|
const url = `${this._baseUrl}/snapshots`;
|
|
735
794
|
const payload = {
|
|
736
795
|
name,
|
|
@@ -740,6 +799,9 @@ class SandboxClient {
|
|
|
740
799
|
if (registryId !== undefined) {
|
|
741
800
|
payload.registry_id = registryId;
|
|
742
801
|
}
|
|
802
|
+
if (runConfig !== undefined) {
|
|
803
|
+
payload.run_config = runConfig;
|
|
804
|
+
}
|
|
743
805
|
const response = await this._postJson(url, payload, { signal });
|
|
744
806
|
const snapshot = (await response.json());
|
|
745
807
|
return this.waitForSnapshot(snapshot.id, { timeout, signal });
|
|
@@ -832,7 +894,7 @@ class SandboxClient {
|
|
|
832
894
|
* @returns Snapshot in "ready" status.
|
|
833
895
|
*/
|
|
834
896
|
async captureSnapshot(sandboxName, name, options = {}) {
|
|
835
|
-
const { dockerImage, fsCapacityBytes, timeout = 60, signal } = options;
|
|
897
|
+
const { dockerImage, fsCapacityBytes, runConfig, timeout = 60, signal, } = options;
|
|
836
898
|
const url = this._boxUrl(sandboxName, "snapshot");
|
|
837
899
|
const payload = { name };
|
|
838
900
|
if (dockerImage !== undefined) {
|
|
@@ -841,6 +903,9 @@ class SandboxClient {
|
|
|
841
903
|
if (fsCapacityBytes !== undefined) {
|
|
842
904
|
payload.fs_capacity_bytes = fsCapacityBytes;
|
|
843
905
|
}
|
|
906
|
+
if (runConfig !== undefined) {
|
|
907
|
+
payload.run_config = runConfig;
|
|
908
|
+
}
|
|
844
909
|
const response = await this._postJson(url, payload, { signal });
|
|
845
910
|
const snapshot = (await response.json());
|
|
846
911
|
return this.waitForSnapshot(snapshot.id, { timeout, signal });
|
package/dist/sandbox/client.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Main SandboxClient class for interacting with the sandbox server API.
|
|
3
3
|
*/
|
|
4
|
+
import { ServiceLoginUrl, ServiceUrl, type ServiceAccess } from "./service_url.js";
|
|
4
5
|
import { Registries } from "../_openapi_client/resources/sandboxes/registries.js";
|
|
5
6
|
import type { CaptureSnapshotOptions, CreateDockerfileSnapshotOptions, CreateSandboxOptions, CreateSnapshotOptions, DownloadURL, GenerateDownloadURLOptions, ListSnapshotsOptions, ResourceStatus, SandboxClientConfig, Snapshot, StartSandboxOptions, UpdateSandboxOptions, WaitForSandboxOptions, WaitForSnapshotOptions } from "./types.js";
|
|
6
7
|
import { Sandbox } from "./sandbox.js";
|
|
@@ -64,6 +65,23 @@ export declare class SandboxClient {
|
|
|
64
65
|
* ```
|
|
65
66
|
*/
|
|
66
67
|
get registries(): Registries;
|
|
68
|
+
/**
|
|
69
|
+
* Generate an authenticated URL for an HTTP service running in a sandbox.
|
|
70
|
+
*
|
|
71
|
+
* Without `access`, mints a short-lived token and returns a {@link ServiceUrl}
|
|
72
|
+
* that refreshes it as it nears expiry. With `access` set to `"restricted"`
|
|
73
|
+
* or `"workspace"` the URL is gated by LangSmith login instead and a
|
|
74
|
+
* {@link ServiceLoginUrl} is returned: no token, no expiry, browser only.
|
|
75
|
+
*
|
|
76
|
+
* A login grant is durable, so token mode is refused with 409 while one is
|
|
77
|
+
* in place, and `expiresInSeconds` does not apply to the login modes.
|
|
78
|
+
*/
|
|
79
|
+
serviceUrl(name: string, options: {
|
|
80
|
+
port: number;
|
|
81
|
+
expiresInSeconds?: number;
|
|
82
|
+
access?: ServiceAccess;
|
|
83
|
+
signal?: AbortSignal;
|
|
84
|
+
}): Promise<ServiceUrl | ServiceLoginUrl>;
|
|
67
85
|
private _postJson;
|
|
68
86
|
/**
|
|
69
87
|
* Create a new Sandbox.
|
package/dist/sandbox/client.js
CHANGED
|
@@ -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
|
}
|
package/dist/sandbox/helpers.cjs
CHANGED
|
@@ -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 {};
|