langsmith 0.10.4 → 0.10.6

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 (68) hide show
  1. package/dist/address.cjs +119 -0
  2. package/dist/address.d.ts +13 -0
  3. package/dist/address.js +107 -0
  4. package/dist/client.cjs +68 -22
  5. package/dist/client.d.ts +13 -0
  6. package/dist/client.js +68 -22
  7. package/dist/evaluation/_runner.cjs +47 -5
  8. package/dist/evaluation/_runner.d.ts +12 -0
  9. package/dist/evaluation/_runner.js +47 -5
  10. package/dist/experimental/otel/constants.cjs +5 -1
  11. package/dist/experimental/otel/constants.d.ts +3 -0
  12. package/dist/experimental/otel/constants.js +3 -0
  13. package/dist/experimental/otel/translator.cjs +14 -0
  14. package/dist/experimental/otel/translator.js +14 -0
  15. package/dist/index.cjs +4 -2
  16. package/dist/index.d.ts +2 -1
  17. package/dist/index.js +2 -1
  18. package/dist/run_trees.cjs +154 -24
  19. package/dist/run_trees.d.ts +13 -1
  20. package/dist/run_trees.js +154 -24
  21. package/dist/sandbox/access_delegation.cjs +24 -0
  22. package/dist/sandbox/access_delegation.d.ts +15 -0
  23. package/dist/sandbox/access_delegation.js +21 -0
  24. package/dist/sandbox/client.cjs +70 -5
  25. package/dist/sandbox/client.d.ts +18 -0
  26. package/dist/sandbox/client.js +70 -5
  27. package/dist/sandbox/command_handle.cjs +34 -0
  28. package/dist/sandbox/command_handle.d.ts +10 -0
  29. package/dist/sandbox/command_handle.js +34 -0
  30. package/dist/sandbox/errors.cjs +12 -1
  31. package/dist/sandbox/errors.d.ts +7 -0
  32. package/dist/sandbox/errors.js +10 -0
  33. package/dist/sandbox/helpers.cjs +129 -0
  34. package/dist/sandbox/helpers.d.ts +29 -0
  35. package/dist/sandbox/helpers.js +123 -0
  36. package/dist/sandbox/index.cjs +12 -1
  37. package/dist/sandbox/index.d.ts +8 -2
  38. package/dist/sandbox/index.js +4 -1
  39. package/dist/sandbox/mounts.cjs +21 -3
  40. package/dist/sandbox/mounts.d.ts +7 -1
  41. package/dist/sandbox/mounts.js +21 -3
  42. package/dist/sandbox/proxy_config.cjs +41 -10
  43. package/dist/sandbox/proxy_config.d.ts +33 -6
  44. package/dist/sandbox/proxy_config.js +40 -10
  45. package/dist/sandbox/sandbox.cjs +160 -4
  46. package/dist/sandbox/sandbox.d.ts +71 -1
  47. package/dist/sandbox/sandbox.js +161 -5
  48. package/dist/sandbox/service_url.cjs +125 -0
  49. package/dist/sandbox/service_url.d.ts +50 -0
  50. package/dist/sandbox/service_url.js +120 -0
  51. package/dist/sandbox/types.d.ts +251 -8
  52. package/dist/sandbox/verify.cjs +300 -0
  53. package/dist/sandbox/verify.d.ts +122 -0
  54. package/dist/sandbox/verify.js +296 -0
  55. package/dist/sandbox/ws_execute.cjs +31 -1
  56. package/dist/sandbox/ws_execute.d.ts +9 -0
  57. package/dist/sandbox/ws_execute.js +31 -1
  58. package/dist/schemas.d.ts +5 -0
  59. package/dist/traceable.cjs +7 -1
  60. package/dist/traceable.js +7 -1
  61. package/dist/utils/agent_addressing.cjs +92 -0
  62. package/dist/utils/agent_addressing.d.ts +30 -0
  63. package/dist/utils/agent_addressing.js +83 -0
  64. package/dist/utils/env.cjs +2 -0
  65. package/dist/utils/env.js +2 -0
  66. package/dist/utils/error.cjs +17 -22
  67. package/dist/utils/error.js +17 -22
  68. package/package.json +5 -5
package/dist/run_trees.js CHANGED
@@ -4,11 +4,12 @@ import { isConflictingEndpointsError, ConflictingEndpointsError, } from "./utils
4
4
  import { _LC_CONTEXT_VARIABLES_KEY, _REPLICA_TRACE_ROOTS_KEY, } from "./singletons/constants.js";
5
5
  import { getContextVar, setContextVar } from "./utils/context_vars.js";
6
6
  import { getEnvironmentVariable, getRuntimeEnvironment, } from "./utils/env.js";
7
- import { getDefaultProjectName } from "./utils/project.js";
8
7
  import { getLangSmithEnvironmentVariable } from "./utils/env.js";
9
8
  import { warnOnce } from "./utils/warn.js";
10
9
  import { uuid7FromTime, nonCryptographicUuid7Deterministic, } from "./utils/_uuid.js";
11
10
  import { v5 as uuidv5 } from "./utils/uuid/src/index.js";
11
+ import { checkAddress, logUntraced, rejectConflicting, resolveFromEnv, } from "./utils/agent_addressing.js";
12
+ import { EnvAddressError, fromWire, sameAddress, seed as addressSeed, toWire, wireKeys, } from "./address.js";
12
13
  const TIMESTAMP_LENGTH = 36;
13
14
  // DNS namespace for UUID v5 (same as Python's uuid.NAMESPACE_DNS)
14
15
  const UUID_NAMESPACE_DNS = "6ba7b810-9dad-11d1-80b4-00c04fd430c8";
@@ -43,7 +44,31 @@ const HEADER_SAFE_REPLICA_FIELDS = new Set([
43
44
  "primary",
44
45
  "updates",
45
46
  "reroot",
47
+ ...wireKeys(),
46
48
  ]);
49
+ // URL-encoded JSON of the address's wire fields.
50
+ const LANGSMITH_ADDRESS = "langsmith-address";
51
+ /** Pops the address wire keys off untrusted `values`; never throws. */
52
+ function takeHeaderAddress(values) {
53
+ const wire = {};
54
+ for (const key of wireKeys()) {
55
+ if (key in values) {
56
+ wire[key] = values[key];
57
+ delete values[key];
58
+ }
59
+ }
60
+ try {
61
+ return {
62
+ named: Object.keys(wire).length > 0,
63
+ address: fromWire(wire),
64
+ };
65
+ }
66
+ catch {
67
+ // Values are untrusted, so not logged.
68
+ console.warn("Ignoring an invalid address in a `baggage` header.");
69
+ return { named: true };
70
+ }
71
+ }
47
72
  function filterReplicaForHeaders(replica) {
48
73
  const filtered = {};
49
74
  for (const key of Object.keys(replica)) {
@@ -60,7 +85,7 @@ function filterReplicaForHeaders(replica) {
60
85
  * Baggage header information
61
86
  */
62
87
  class Baggage {
63
- constructor(metadata, tags, project_name, replicas) {
88
+ constructor(metadata, tags, project_name, replicas, address) {
64
89
  Object.defineProperty(this, "metadata", {
65
90
  enumerable: true,
66
91
  configurable: true,
@@ -85,10 +110,17 @@ class Baggage {
85
110
  writable: true,
86
111
  value: void 0
87
112
  });
113
+ Object.defineProperty(this, "address", {
114
+ enumerable: true,
115
+ configurable: true,
116
+ writable: true,
117
+ value: void 0
118
+ });
88
119
  this.metadata = metadata;
89
120
  this.tags = tags;
90
121
  this.project_name = project_name;
91
122
  this.replicas = replicas;
123
+ this.address = address;
92
124
  }
93
125
  static fromHeader(value) {
94
126
  const items = value.split(",");
@@ -96,6 +128,7 @@ class Baggage {
96
128
  let tags = [];
97
129
  let project_name;
98
130
  let replicas;
131
+ let address;
99
132
  for (const item of items) {
100
133
  const [key, uriValue] = item.split("=");
101
134
  const value = decodeURIComponent(uriValue);
@@ -108,17 +141,36 @@ class Baggage {
108
141
  else if (key === "langsmith-project") {
109
142
  project_name = value;
110
143
  }
144
+ else if (key === LANGSMITH_ADDRESS) {
145
+ let parsed;
146
+ try {
147
+ parsed = JSON.parse(value);
148
+ }
149
+ catch {
150
+ parsed = undefined;
151
+ }
152
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
153
+ address = takeHeaderAddress({ ...parsed }).address;
154
+ }
155
+ }
111
156
  else if (key === "langsmith-replicas") {
112
157
  const parsed = JSON.parse(value);
113
- replicas = parsed.map((replica) => {
158
+ replicas = parsed.flatMap((replica) => {
114
159
  if (Array.isArray(replica)) {
115
- return replica;
160
+ return [replica];
116
161
  }
117
- return filterReplicaForHeaders(replica);
162
+ const filtered = filterReplicaForHeaders(replica);
163
+ const { named, address } = takeHeaderAddress(filtered);
164
+ // A project wins over an address; a replica with an invalid
165
+ // address and no project is dropped.
166
+ if (filtered.projectName || !named) {
167
+ return [filtered];
168
+ }
169
+ return address ? [{ ...filtered, address }] : [];
118
170
  });
119
171
  }
120
172
  }
121
- return new Baggage(metadata, tags, project_name, replicas);
173
+ return new Baggage(metadata, tags, project_name, replicas, address);
122
174
  }
123
175
  toHeader() {
124
176
  const items = [];
@@ -131,6 +183,9 @@ class Baggage {
131
183
  if (this.project_name) {
132
184
  items.push(`langsmith-project=${encodeURIComponent(this.project_name)}`);
133
185
  }
186
+ if (this.address) {
187
+ items.push(`${LANGSMITH_ADDRESS}=${encodeURIComponent(JSON.stringify(toWire(this.address)))}`);
188
+ }
134
189
  return items.join(",");
135
190
  }
136
191
  }
@@ -158,12 +213,20 @@ export class RunTree {
158
213
  writable: true,
159
214
  value: void 0
160
215
  });
216
+ /** Unset for a run sent to an `address`. */
161
217
  Object.defineProperty(this, "project_name", {
162
218
  enumerable: true,
163
219
  configurable: true,
164
220
  writable: true,
165
221
  value: void 0
166
222
  });
223
+ /** (beta) Set instead of `project_name` for an addressed run. */
224
+ Object.defineProperty(this, "address", {
225
+ enumerable: true,
226
+ configurable: true,
227
+ writable: true,
228
+ value: void 0
229
+ });
167
230
  Object.defineProperty(this, "parent_run", {
168
231
  enumerable: true,
169
232
  configurable: true,
@@ -338,6 +401,7 @@ export class RunTree {
338
401
  delete config.id;
339
402
  }
340
403
  Object.assign(this, { ...defaultConfig, ...config, client });
404
+ this._resolveAddressing(config);
341
405
  this.execution_order ??= 1;
342
406
  this.child_execution_order ??= 1;
343
407
  // Generate serialized start time for ID generation
@@ -368,6 +432,31 @@ export class RunTree {
368
432
  }
369
433
  }
370
434
  }
435
+ /** Falls back to the env; a bad env leaves the run untraced. */
436
+ _resolveAddressing(config) {
437
+ const address = checkAddress(config.address);
438
+ const project = config.project_name || undefined;
439
+ rejectConflicting(project, address);
440
+ if (project || address) {
441
+ this.project_name = project;
442
+ this.address = address;
443
+ return;
444
+ }
445
+ try {
446
+ [this.project_name, this.address] = resolveFromEnv();
447
+ }
448
+ catch (e) {
449
+ if (!(e instanceof EnvAddressError)) {
450
+ throw e;
451
+ }
452
+ if (isEnvTracingEnabled(this.tracingEnabled)) {
453
+ logUntraced(e);
454
+ }
455
+ this.project_name = undefined;
456
+ this.address = undefined;
457
+ this.tracingEnabled = false;
458
+ }
459
+ }
371
460
  set metadata(metadata) {
372
461
  this.extra = {
373
462
  ...this.extra,
@@ -384,7 +473,6 @@ export class RunTree {
384
473
  const start_time = Date.now();
385
474
  return {
386
475
  run_type: "chain",
387
- project_name: getDefaultProjectName(),
388
476
  child_runs: [],
389
477
  api_url: getEnvironmentVariable("LANGCHAIN_ENDPOINT") ?? "http://localhost:1984",
390
478
  api_key: getEnvironmentVariable("LANGCHAIN_API_KEY"),
@@ -414,6 +502,7 @@ export class RunTree {
414
502
  ...config,
415
503
  parent_run: this,
416
504
  project_name: this.project_name,
505
+ address: this.address,
417
506
  replicas: childReplicas,
418
507
  client: this.client,
419
508
  tracingEnabled: this.tracingEnabled,
@@ -512,6 +601,7 @@ export class RunTree {
512
601
  inputs: run.inputs,
513
602
  outputs: run.outputs,
514
603
  session_name: run.project_name,
604
+ address: run.address,
515
605
  child_runs: child_runs,
516
606
  parent_run_id: parent_run_id,
517
607
  trace_id: run.trace_id,
@@ -568,16 +658,33 @@ export class RunTree {
568
658
  child._setReplicaTraceRoot(replicaKey, traceRootId);
569
659
  }
570
660
  }
661
+ /** A replica naming neither inherits the run tree's destination. */
662
+ _replicaAddressing(replica) {
663
+ const address = checkAddress(replica.address);
664
+ if (replica.projectName != null) {
665
+ rejectConflicting(replica.projectName, address);
666
+ return [replica.projectName, undefined];
667
+ }
668
+ if (address) {
669
+ return [undefined, address];
670
+ }
671
+ return [this.project_name, this.address];
672
+ }
571
673
  _remapForProject(params) {
572
- const { projectName, primary, runtimeEnv, excludeChildRuns = true, reroot = false, distributedParentId, apiUrl, apiKey, workspaceId, } = params;
674
+ const { projectName, address, primary, runtimeEnv, excludeChildRuns = true, reroot = false, distributedParentId, apiUrl, apiKey, workspaceId, } = params;
573
675
  const baseRun = this._convertToCreate(this, runtimeEnv, excludeChildRuns);
574
- // Preserve legacy behavior when `primary` is omitted.
575
- if (primary === undefined && projectName === this.project_name) {
676
+ // Preserve legacy behavior when `primary` is omitted, unless another replica is primary.
677
+ if (primary === undefined &&
678
+ !this.replicas?.some((r) => r.primary === true) &&
679
+ projectName === this.project_name &&
680
+ sameAddress(address, this.address)) {
576
681
  return {
577
682
  ...baseRun,
578
683
  session_name: projectName,
684
+ address,
579
685
  };
580
686
  }
687
+ const seed = projectName ?? (address ? addressSeed(address) : undefined) ?? "agent//";
581
688
  // Apply reroot logic before ID remapping
582
689
  if (reroot) {
583
690
  if (distributedParentId) {
@@ -601,7 +708,7 @@ export class RunTree {
601
708
  // Store this run's original ID in context vars so descendants know the new trace root
602
709
  // We store the original ID (before remapping) so it can be found in dotted_order
603
710
  const replicaKey = getReplicaKey({
604
- projectName,
711
+ projectName: seed,
605
712
  apiUrl,
606
713
  apiKey,
607
714
  workspaceId,
@@ -614,7 +721,7 @@ export class RunTree {
614
721
  if (!reroot) {
615
722
  const replicaTraceRoots = getContextVar(this, _REPLICA_TRACE_ROOTS_KEY) ?? {};
616
723
  const replicaKey = getReplicaKey({
617
- projectName,
724
+ projectName: seed,
618
725
  apiUrl,
619
726
  apiKey,
620
727
  workspaceId,
@@ -649,17 +756,18 @@ export class RunTree {
649
756
  return {
650
757
  ...baseRun,
651
758
  session_name: projectName,
759
+ address,
652
760
  };
653
761
  }
654
762
  // Remap IDs for the replica using nonCryptographicUuid7Deterministic
655
763
  // This ensures consistency across runs in the same replica while
656
764
  // preserving UUID7 properties (time-ordering, monotonicity)
657
765
  const oldId = baseRun.id;
658
- const newId = nonCryptographicUuid7Deterministic(oldId, projectName);
766
+ const newId = nonCryptographicUuid7Deterministic(oldId, seed);
659
767
  // Remap trace_id
660
768
  let newTraceId;
661
769
  if (baseRun.trace_id) {
662
- newTraceId = nonCryptographicUuid7Deterministic(baseRun.trace_id, projectName);
770
+ newTraceId = nonCryptographicUuid7Deterministic(baseRun.trace_id, seed);
663
771
  }
664
772
  else {
665
773
  newTraceId = newId;
@@ -667,7 +775,7 @@ export class RunTree {
667
775
  // Remap parent_run_id
668
776
  let newParentId;
669
777
  if (baseRun.parent_run_id) {
670
- newParentId = nonCryptographicUuid7Deterministic(baseRun.parent_run_id, projectName);
778
+ newParentId = nonCryptographicUuid7Deterministic(baseRun.parent_run_id, seed);
671
779
  }
672
780
  // Remap dotted_order segments
673
781
  let newDottedOrder;
@@ -676,7 +784,7 @@ export class RunTree {
676
784
  const remappedSegs = segs.map((seg) => {
677
785
  // Extract the UUID from the segment (last TIMESTAMP_LENGTH characters)
678
786
  const segId = seg.slice(-TIMESTAMP_LENGTH);
679
- const remappedId = nonCryptographicUuid7Deterministic(segId, projectName);
787
+ const remappedId = nonCryptographicUuid7Deterministic(segId, seed);
680
788
  // Replace the UUID part while keeping the timestamp prefix
681
789
  return seg.slice(0, -TIMESTAMP_LENGTH) + remappedId;
682
790
  });
@@ -689,6 +797,7 @@ export class RunTree {
689
797
  parent_run_id: newParentId,
690
798
  dotted_order: newDottedOrder,
691
799
  session_name: projectName,
800
+ address,
692
801
  };
693
802
  }
694
803
  async postRun(excludeChildRuns = true) {
@@ -699,9 +808,12 @@ export class RunTree {
699
808
  try {
700
809
  const runtimeEnv = getRuntimeEnvironment();
701
810
  if (this.replicas && this.replicas.length > 0) {
702
- for (const { projectName, primary, apiKey, apiUrl, workspaceId, reroot, client: replicaClient, } of this.replicas) {
811
+ for (const replica of this.replicas) {
812
+ const { primary, apiKey, apiUrl, workspaceId, reroot, client: replicaClient, } = replica;
813
+ const [projectName, address] = this._replicaAddressing(replica);
703
814
  const runCreate = this._remapForProject({
704
- projectName: projectName ?? this.project_name,
815
+ projectName,
816
+ address,
705
817
  primary,
706
818
  runtimeEnv,
707
819
  excludeChildRuns: true,
@@ -750,9 +862,12 @@ export class RunTree {
750
862
  async patchRun(options) {
751
863
  const excludeInputs = options?.excludeInputs ?? getExcludeInputsOnPatch();
752
864
  if (this.replicas && this.replicas.length > 0) {
753
- for (const { projectName, primary, apiKey, apiUrl, workspaceId, updates, reroot, client: replicaClient, } of this.replicas) {
865
+ for (const replica of this.replicas) {
866
+ const { primary, apiKey, apiUrl, workspaceId, updates, reroot, client: replicaClient, } = replica;
867
+ const [projectName, address] = this._replicaAddressing(replica);
754
868
  const runData = this._remapForProject({
755
- projectName: projectName ?? this.project_name,
869
+ projectName,
870
+ address,
756
871
  primary,
757
872
  runtimeEnv: undefined,
758
873
  excludeChildRuns: true,
@@ -771,6 +886,7 @@ export class RunTree {
771
886
  error: runData.error,
772
887
  parent_run_id: runData.parent_run_id,
773
888
  session_name: runData.session_name,
889
+ address: runData.address,
774
890
  reference_example_id: runData.reference_example_id,
775
891
  end_time: runData.end_time,
776
892
  dotted_order: runData.dotted_order,
@@ -813,6 +929,7 @@ export class RunTree {
813
929
  tags: this.tags,
814
930
  attachments: this.attachments,
815
931
  session_name: this.project_name,
932
+ address: this.address,
816
933
  };
817
934
  // Important that inputs is not a key in the run update
818
935
  // if excluded because it will overwrite the run create if the
@@ -873,7 +990,7 @@ export class RunTree {
873
990
  ...props,
874
991
  client,
875
992
  tracingEnabled,
876
- project_name: projectName,
993
+ project_name: props.address ? undefined : projectName,
877
994
  });
878
995
  }
879
996
  const parentRunTree = new RunTree({
@@ -928,7 +1045,19 @@ export class RunTree {
928
1045
  const baggage = Baggage.fromHeader(rawHeaders["baggage"]);
929
1046
  config.metadata = baggage.metadata;
930
1047
  config.tags = baggage.tags;
931
- config.project_name = baggage.project_name;
1048
+ if (baggage.project_name && baggage.address) {
1049
+ console.warn("Ignoring a `baggage` header that names both a project and an address.");
1050
+ return undefined;
1051
+ }
1052
+ // The header's destination outranks the caller's.
1053
+ if (baggage.project_name) {
1054
+ config.project_name = baggage.project_name;
1055
+ delete config.address;
1056
+ }
1057
+ else if (baggage.address) {
1058
+ config.address = baggage.address;
1059
+ delete config.project_name;
1060
+ }
932
1061
  config.replicas = baggage.replicas;
933
1062
  }
934
1063
  const runTree = new RunTree(config);
@@ -939,7 +1068,7 @@ export class RunTree {
939
1068
  toHeaders(headers) {
940
1069
  const result = {
941
1070
  "langsmith-trace": this.dotted_order,
942
- baggage: new Baggage(this.extra?.metadata, this.tags, this.project_name, this.replicas).toHeader(),
1071
+ baggage: new Baggage(this.extra?.metadata, this.tags, this.project_name, this.replicas, this.address).toHeader(),
943
1072
  };
944
1073
  if (headers) {
945
1074
  for (const [key, value] of Object.entries(result)) {
@@ -1075,7 +1204,8 @@ function _ensureWriteReplicas(replicas) {
1075
1204
  updates: replica[1],
1076
1205
  };
1077
1206
  }
1078
- return replica;
1207
+ const address = checkAddress(replica.address);
1208
+ return address ? { ...replica, address } : replica;
1079
1209
  })
1080
1210
  : _getWriteReplicasFromEnv();
1081
1211
  if (ensured.filter((replica) => replica.primary === true).length > 1) {
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.validateAccessDelegation = validateAccessDelegation;
4
+ const errors_js_1 = require("./errors.cjs");
5
+ /** Rejects grants the server would refuse, without the round trip. */
6
+ function validateAccessDelegation(value) {
7
+ if (value.mode !== "INHERIT" && value.mode !== "EXPLICIT") {
8
+ throw new errors_js_1.LangSmithValidationError('accessDelegation.mode must be "INHERIT" or "EXPLICIT"', "accessDelegation");
9
+ }
10
+ const { permissions } = value;
11
+ if (permissions !== undefined && !Array.isArray(permissions)) {
12
+ throw new errors_js_1.LangSmithValidationError("accessDelegation.permissions must be an array of strings", "accessDelegation");
13
+ }
14
+ if (value.mode === "INHERIT") {
15
+ if (permissions !== undefined && permissions.length > 0) {
16
+ throw new errors_js_1.LangSmithValidationError('accessDelegation.permissions is not allowed with mode "INHERIT"', "accessDelegation");
17
+ }
18
+ return { mode: "INHERIT" };
19
+ }
20
+ if (permissions === undefined || permissions.length === 0) {
21
+ throw new errors_js_1.LangSmithValidationError('accessDelegation.permissions is required with mode "EXPLICIT"', "accessDelegation");
22
+ }
23
+ return { mode: "EXPLICIT", permissions: [...permissions] };
24
+ }
@@ -0,0 +1,15 @@
1
+ /** How a delegation grant derives its permission set. */
2
+ export type AccessDelegationMode = "INHERIT" | "EXPLICIT";
3
+ /**
4
+ * Grant letting code inside a sandbox call the LangSmith API as the creator.
5
+ *
6
+ * `INHERIT` tracks everything the creator can do; `EXPLICIT` is capped to
7
+ * `permissions`. Permissions are a ceiling re-checked on every request rather
8
+ * than a snapshot, so access the creator loses is lost here too.
9
+ */
10
+ export interface AccessDelegation {
11
+ mode: AccessDelegationMode;
12
+ permissions?: string[];
13
+ }
14
+ /** Rejects grants the server would refuse, without the round trip. */
15
+ export declare function validateAccessDelegation(value: AccessDelegation): AccessDelegation;
@@ -0,0 +1,21 @@
1
+ import { LangSmithValidationError } from "./errors.js";
2
+ /** Rejects grants the server would refuse, without the round trip. */
3
+ export function validateAccessDelegation(value) {
4
+ if (value.mode !== "INHERIT" && value.mode !== "EXPLICIT") {
5
+ throw new LangSmithValidationError('accessDelegation.mode must be "INHERIT" or "EXPLICIT"', "accessDelegation");
6
+ }
7
+ const { permissions } = value;
8
+ if (permissions !== undefined && !Array.isArray(permissions)) {
9
+ throw new LangSmithValidationError("accessDelegation.permissions must be an array of strings", "accessDelegation");
10
+ }
11
+ if (value.mode === "INHERIT") {
12
+ if (permissions !== undefined && permissions.length > 0) {
13
+ throw new LangSmithValidationError('accessDelegation.permissions is not allowed with mode "INHERIT"', "accessDelegation");
14
+ }
15
+ return { mode: "INHERIT" };
16
+ }
17
+ if (permissions === undefined || permissions.length === 0) {
18
+ throw new LangSmithValidationError('accessDelegation.permissions is required with mode "EXPLICIT"', "accessDelegation");
19
+ }
20
+ return { mode: "EXPLICIT", permissions: [...permissions] };
21
+ }
@@ -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 });
@@ -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.