managed-deepagents 0.0.3-dev.46 → 0.0.3-dev.47

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 (58) hide show
  1. package/dist/connectors/langsmith.d.ts +255 -4
  2. package/dist/connectors/langsmith.d.ts.map +1 -1
  3. package/dist/connectors/langsmith.js +167 -4
  4. package/dist/connectors/langsmith.js.map +1 -1
  5. package/dist/identity/index.d.ts +88 -0
  6. package/dist/identity/index.d.ts.map +1 -0
  7. package/dist/{identity.js → identity/index.js} +6 -125
  8. package/dist/identity/index.js.map +1 -0
  9. package/dist/identity/supabase.d.ts +48 -0
  10. package/dist/identity/supabase.d.ts.map +1 -0
  11. package/dist/identity/supabase.js +139 -0
  12. package/dist/identity/supabase.js.map +1 -0
  13. package/dist/{identity.d.ts → identity/types.d.ts} +4 -111
  14. package/dist/identity/types.d.ts.map +1 -0
  15. package/dist/identity/types.js +8 -0
  16. package/dist/identity/types.js.map +1 -0
  17. package/dist/index.d.ts +2 -2
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +1 -1
  20. package/dist/index.js.map +1 -1
  21. package/dist/runtime/auth.d.ts +1 -1
  22. package/dist/runtime/auth.d.ts.map +1 -1
  23. package/dist/runtime/connector.d.ts +1 -1
  24. package/dist/runtime/connector.d.ts.map +1 -1
  25. package/dist/runtime/credentials.d.ts +1 -1
  26. package/dist/runtime/credentials.d.ts.map +1 -1
  27. package/dist/runtime/identity-http.d.ts +1 -1
  28. package/dist/runtime/identity-http.d.ts.map +1 -1
  29. package/dist/runtime/identity-runtime.d.ts +1 -1
  30. package/dist/runtime/identity-runtime.d.ts.map +1 -1
  31. package/dist/runtime/index.d.ts.map +1 -1
  32. package/dist/runtime/index.js +41 -67
  33. package/dist/runtime/index.js.map +1 -1
  34. package/dist/runtime/langsmith-connector.d.ts +110 -12
  35. package/dist/runtime/langsmith-connector.d.ts.map +1 -1
  36. package/dist/runtime/langsmith-connector.js +467 -40
  37. package/dist/runtime/langsmith-connector.js.map +1 -1
  38. package/dist/runtime/managed-middleware.d.ts +1 -1
  39. package/dist/runtime/managed-middleware.d.ts.map +1 -1
  40. package/dist/runtime/managed-tools.d.ts +1 -1
  41. package/dist/runtime/managed-tools.d.ts.map +1 -1
  42. package/dist/runtime/sandbox-manager.d.ts +3 -2
  43. package/dist/runtime/sandbox-manager.d.ts.map +1 -1
  44. package/dist/runtime/sandbox-manager.js.map +1 -1
  45. package/dist/runtime/setup-script.d.ts +4 -7
  46. package/dist/runtime/setup-script.d.ts.map +1 -1
  47. package/dist/runtime/setup-script.js.map +1 -1
  48. package/dist/runtime/types.d.ts +1 -15
  49. package/dist/runtime/types.d.ts.map +1 -1
  50. package/dist/runtime/validated-token.d.ts +20 -3
  51. package/dist/runtime/validated-token.d.ts.map +1 -1
  52. package/dist/runtime/validated-token.js +71 -20
  53. package/dist/runtime/validated-token.js.map +1 -1
  54. package/dist/types.d.ts +1 -1
  55. package/dist/types.d.ts.map +1 -1
  56. package/package.json +7 -7
  57. package/dist/identity.d.ts.map +0 -1
  58. package/dist/identity.js.map +0 -1
@@ -18,6 +18,10 @@ import { Client } from "langsmith";
18
18
  /** An error carrying an HTTP status the engine surfaces to the caller. */
19
19
  export class EnforcementError extends Error {
20
20
  status;
21
+ /**
22
+ * @param status - HTTP status code returned to the caller.
23
+ * @param message - Human-readable error detail.
24
+ */
21
25
  constructor(status, message) {
22
26
  super(message);
23
27
  this.status = status;
@@ -28,6 +32,10 @@ export class EnforcementError extends Error {
28
32
  * Mount the capability route on the connector's namespaced sub-router. Runs the
29
33
  * identity-dependent authoring checks at mount time (fails startup on an illegal
30
34
  * scope/ingress combination), then registers the uniform POST endpoint.
35
+ *
36
+ * @param ctx - HTTP mount context with the namespaced router and identity config.
37
+ * @param capabilities - Declared LangSmith capabilities to expose.
38
+ * @param client - LangSmith client used for server-side operations; defaults to {@link defaultClient}.
31
39
  */
32
40
  export function mountLangSmithRoutes(ctx, capabilities, client = defaultClient()) {
33
41
  validateAgainstIdentity(capabilities, ctx.identity);
@@ -41,7 +49,14 @@ export function mountLangSmithRoutes(ctx, capabilities, client = defaultClient()
41
49
  // second argument.
42
50
  ctx.router.post("/capabilities/:id", (request, identity) => handleCapabilityRequest(request, identity, deps));
43
51
  }
44
- /** Identity-aware authoring validation, run when the app is constructed. */
52
+ /**
53
+ * Identity-aware authoring validation, run when the app is constructed.
54
+ * Rejects illegal scope/ingress combinations before any request is served.
55
+ *
56
+ * @param capabilities - Declared LangSmith capabilities to validate.
57
+ * @param identity - Deployment identity config, or `undefined` when identity is not configured.
58
+ * @throws {Error} When a capability's scope, exposure, or tenancy is illegal for this deployment.
59
+ */
45
60
  export function validateAgainstIdentity(capabilities, identity) {
46
61
  const httpMode = identity?.ingress.http;
47
62
  const hasValidatedToken = httpMode !== undefined && httpMode !== "trusted_backend";
@@ -74,7 +89,16 @@ export function validateAgainstIdentity(capabilities, identity) {
74
89
  }
75
90
  }
76
91
  }
77
- /** The per-request enforcement pipeline. Returns a JSON `Response`. */
92
+ /**
93
+ * Per-request enforcement pipeline for a capability POST.
94
+ * Resolves the capability, checks surface/action, proves thread ownership when
95
+ * needed, enforces scope, performs the LangSmith call, and shapes the response.
96
+ *
97
+ * @param request - Incoming HTTP request for `/capabilities/:id`.
98
+ * @param identity - Frozen runtime identity resolved by the secure router.
99
+ * @param deps - Closed-over capabilities map, LangSmith client, and optional access prover.
100
+ * @returns JSON `Response` with `{ data }` on success or `{ error }` on failure.
101
+ */
78
102
  export async function handleCapabilityRequest(request, identity, deps) {
79
103
  try {
80
104
  const capabilityId = capabilityIdFromUrl(request.url);
@@ -91,9 +115,15 @@ export async function handleCapabilityRequest(request, identity, deps) {
91
115
  if (!capability.actions.includes(action)) {
92
116
  throw new EnforcementError(403, `action "${action}" is not allowed for capability "${capability.id}"`);
93
117
  }
94
- // Connector HTTP has no LangGraph configurable.thread_id; browser callers
95
- // supply thread context in the body so thread/run scopes can bind.
96
- const scopedIdentity = bindIdentityThread(identity, body);
118
+ // Connector HTTP has no LangGraph configurable.thread_id. Resolve a thread
119
+ // id from identity / run / body, prove access when it is client-supplied,
120
+ // then bind the proven id onto identity for the rest of the request.
121
+ const assertThreadAccess = deps.assertThreadAccess ??
122
+ defaultAssertThreadAccess(request, identity, deps.identityConfig);
123
+ const provenThreadId = await resolveProvenThreadId(capability, identity, body, deps.client, assertThreadAccess);
124
+ const scopedIdentity = provenThreadId
125
+ ? withIdentityThread(identity, provenThreadId)
126
+ : identity;
97
127
  await assertCapabilityScope(capability, scopedIdentity, body, deps.client);
98
128
  const raw = toRecord(await performLangSmithCall(capability, action, body, deps, scopedIdentity));
99
129
  const shaped = shapeResponse(raw, capability);
@@ -107,7 +137,13 @@ export async function handleCapabilityRequest(request, identity, deps) {
107
137
  return jsonResponse(status, { error: errorMessage(error) });
108
138
  }
109
139
  }
110
- /** Map the resolved identity + ingress mode to a caller surface. */
140
+ /**
141
+ * Map the resolved identity + ingress mode to a caller surface.
142
+ *
143
+ * @param identity - Frozen runtime identity for this request.
144
+ * @param identityConfig - Deployment identity config (ingress mode).
145
+ * @returns The surface used for `exposeTo` checks (`browser`, `trusted_backend`, `channel`, or `schedule`).
146
+ */
111
147
  export function callerSurface(identity, identityConfig) {
112
148
  const provider = identity.source.provider;
113
149
  if (provider === "slack") {
@@ -122,7 +158,12 @@ export function callerSurface(identity, identityConfig) {
122
158
  : "browser";
123
159
  }
124
160
  // --- Ownership + constraints -----------------------------------------------
125
- /** Thread id on the frozen identity envelope (`source.threadId`). */
161
+ /**
162
+ * Read the thread id from the frozen identity envelope (`source.threadId`).
163
+ *
164
+ * @param identity - Frozen runtime identity.
165
+ * @returns Non-empty thread id, or `undefined` when absent.
166
+ */
126
167
  function identityThreadId(identity) {
127
168
  const threadId = identity.source.threadId;
128
169
  return typeof threadId === "string" && threadId.length > 0
@@ -130,29 +171,192 @@ function identityThreadId(identity) {
130
171
  : undefined;
131
172
  }
132
173
  /**
133
- * Bind a request-body thread id onto identity when the envelope has none.
174
+ * Attach a proven thread id onto a copy of the identity envelope.
175
+ * Returns the same object when the identity already carries that thread id.
134
176
  *
135
- * Agent runs already carry `configurable.thread_id` `source.threadId`.
136
- * Browser connector calls do not; callers must pass `threadId`/`thread_id` in
137
- * the body so thread/run-scoped capabilities can prove ownership.
177
+ * @param identity - Frozen runtime identity to copy.
178
+ * @param threadId - Proven thread id to bind onto `source.threadId`.
179
+ * @returns Identity with `source.threadId` set to `threadId`.
138
180
  */
139
- export function bindIdentityThread(identity, body) {
140
- const fromIdentity = identityThreadId(identity);
141
- const fromBody = optionalBodyString(body, "threadId", "thread_id");
142
- if (fromIdentity && fromBody && fromIdentity !== fromBody) {
143
- throw new EnforcementError(403, "body thread id does not match identity thread");
144
- }
145
- if (fromIdentity || !fromBody) {
181
+ export function withIdentityThread(identity, threadId) {
182
+ if (identityThreadId(identity) === threadId) {
146
183
  return identity;
147
184
  }
148
185
  return {
149
186
  ...identity,
150
- source: { ...identity.source, threadId: fromBody },
187
+ source: { ...identity.source, threadId },
188
+ };
189
+ }
190
+ /**
191
+ * Resolve the thread id for a thread/run-scoped capability and prove access
192
+ * when it did not come from the trusted identity envelope.
193
+ *
194
+ * - identity `source.threadId` (server-derived from agent runs) is trusted
195
+ * - LangSmith run session/thread (when `run_id` is present) is authoritative
196
+ * - bare body `threadId`/`thread_id` (no identity thread, no run) requires
197
+ * {@link AssertThreadAccess}
198
+ *
199
+ * @param capability - Capability whose `scope` drives whether a thread id is required.
200
+ * @param identity - Frozen runtime identity for this request.
201
+ * @param body - Parsed JSON request body.
202
+ * @param client - LangSmith client used to load a run when `run_id` is present.
203
+ * @param assertThreadAccess - Ownership prover for bare body thread ids.
204
+ * @returns Proven thread id, or the identity thread id for non-thread/run scopes.
205
+ * @throws {EnforcementError} On missing/mismatched thread ids or failed ownership proof.
206
+ */
207
+ export async function resolveProvenThreadId(capability, identity, body, client, assertThreadAccess) {
208
+ if (capability.scope !== "thread" && capability.scope !== "run") {
209
+ return identityThreadId(identity);
210
+ }
211
+ const fromIdentity = identityThreadId(identity);
212
+ const fromBody = optionalBodyString(body, "threadId", "thread_id");
213
+ const runId = optionalBodyString(body, "runId", "run_id");
214
+ let fromRun;
215
+ if (runId) {
216
+ const run = toRecord(await client.readRun({ runId }));
217
+ fromRun = runThreadId(run);
218
+ if (!fromRun) {
219
+ throw new EnforcementError(403, "run has no thread id");
220
+ }
221
+ }
222
+ else if (capability.scope === "run") {
223
+ // update/delete may omit runId; require identity or body thread below
224
+ }
225
+ for (const [left, right, label] of [
226
+ [fromIdentity, fromBody, "body thread id does not match identity thread"],
227
+ [fromIdentity, fromRun, "run does not belong to identity thread"],
228
+ [fromBody, fromRun, "body thread id does not match run thread"],
229
+ ]) {
230
+ if (left && right && left !== right) {
231
+ throw new EnforcementError(403, label);
232
+ }
233
+ }
234
+ const threadId = fromIdentity ?? fromRun ?? fromBody;
235
+ if (!threadId) {
236
+ throw new EnforcementError(403, `${capability.scope} scope requires thread id (identity source, run, or request body)`);
237
+ }
238
+ // Identity- and run-derived thread ids are already proven. Only a bare body
239
+ // thread id (no identity thread, no run) needs a live ownership check.
240
+ if (!fromIdentity && !fromRun) {
241
+ await assertThreadAccess(threadId);
242
+ }
243
+ return threadId;
244
+ }
245
+ /**
246
+ * Build the default ownership proof: `GET /threads/{id}` with the caller's
247
+ * credentials and require `metadata.owner` to match the identity's thread-owner key.
248
+ *
249
+ * @param request - Incoming request whose auth headers are forwarded to the thread GET.
250
+ * @param identity - Frozen runtime identity used to derive the expected owner key.
251
+ * @param identityConfig - Deployment identity config (`scoping.threads`).
252
+ * @returns An `AssertThreadAccess` function closed over the request and expected owner.
253
+ */
254
+ export function defaultAssertThreadAccess(request, identity, identityConfig) {
255
+ const expectedOwner = threadOwnerFromIdentity(identity, identityConfig);
256
+ return async (threadId) => {
257
+ if (!expectedOwner) {
258
+ throw new EnforcementError(403, "thread access requires identity.actor.id");
259
+ }
260
+ const threadUrl = new URL(request.url);
261
+ threadUrl.pathname = `/threads/${encodeURIComponent(threadId)}`;
262
+ threadUrl.search = "";
263
+ const headers = forwardAuthHeaders(request);
264
+ let response;
265
+ try {
266
+ response = await fetch(threadUrl, { method: "GET", headers });
267
+ }
268
+ catch {
269
+ throw new EnforcementError(502, "failed to verify thread access");
270
+ }
271
+ if (response.status === 401) {
272
+ throw new EnforcementError(401, "unauthorized");
273
+ }
274
+ if (response.status === 403 || response.status === 404) {
275
+ throw new EnforcementError(403, "thread is not accessible to this identity");
276
+ }
277
+ if (!response.ok) {
278
+ throw new EnforcementError(502, `failed to verify thread access (${response.status})`);
279
+ }
280
+ let payload;
281
+ try {
282
+ payload = await response.json();
283
+ }
284
+ catch {
285
+ return;
286
+ }
287
+ const owner = threadOwnerFromPayload(payload);
288
+ if (owner && owner !== expectedOwner) {
289
+ throw new EnforcementError(403, "thread is not accessible to this identity");
290
+ }
151
291
  };
152
292
  }
293
+ /**
294
+ * Owner key used by `@auth.on.threads` for this identity.
295
+ *
296
+ * @param identity - Frozen runtime identity.
297
+ * @param identityConfig - Deployment identity config (`scoping.threads`).
298
+ * @returns Actor or tenant id used as the thread `metadata.owner`, or `undefined` when missing.
299
+ */
300
+ function threadOwnerFromIdentity(identity, identityConfig) {
301
+ const scope = identityConfig?.scoping?.threads ?? "actor";
302
+ if (scope === "tenant") {
303
+ return identity.tenant?.id ?? identity.actor?.id;
304
+ }
305
+ return identity.actor?.id;
306
+ }
307
+ /**
308
+ * Extract `metadata.owner` from a LangGraph thread payload.
309
+ *
310
+ * @param payload - Parsed JSON body from `GET /threads/{id}`.
311
+ * @returns Non-empty owner string, or `undefined` when absent/malformed.
312
+ */
313
+ function threadOwnerFromPayload(payload) {
314
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
315
+ return undefined;
316
+ }
317
+ const metadata = payload.metadata;
318
+ if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) {
319
+ return undefined;
320
+ }
321
+ const owner = metadata.owner;
322
+ return typeof owner === "string" && owner.length > 0 ? owner : undefined;
323
+ }
324
+ const FORWARDED_AUTH_HEADERS = [
325
+ "authorization",
326
+ "cookie",
327
+ "x-api-key",
328
+ "x-auth-key",
329
+ "x-supabase-region",
330
+ "x-mda-ingress-secret",
331
+ "x-mda-actor-id",
332
+ "x-mda-tenant-id",
333
+ ];
334
+ /**
335
+ * Copy auth-related headers from the inbound request for same-origin thread GETs.
336
+ *
337
+ * @param request - Incoming capability request.
338
+ * @returns Headers containing only the allowlisted auth fields that were present.
339
+ */
340
+ function forwardAuthHeaders(request) {
341
+ const headers = new Headers();
342
+ for (const name of FORWARDED_AUTH_HEADERS) {
343
+ const value = request.headers.get(name);
344
+ if (value) {
345
+ headers.set(name, value);
346
+ }
347
+ }
348
+ return headers;
349
+ }
153
350
  /**
154
351
  * Prove the capability's ownership scope from `runtime.identity` before the
155
- * LangSmith call.
352
+ * LangSmith call. Thread id must already be present on identity when required
353
+ * (see {@link resolveProvenThreadId}).
354
+ *
355
+ * @param capability - Capability whose `scope` is enforced.
356
+ * @param identity - Identity with any proven thread id already bound.
357
+ * @param body - Parsed JSON request body (used for run-scoped `run_id` checks).
358
+ * @param client - LangSmith client used to load runs for run-scope verification.
359
+ * @throws {EnforcementError} When the identity lacks the required scope fields or the run is foreign.
156
360
  */
157
361
  export async function assertCapabilityScope(capability, identity, body, client) {
158
362
  switch (capability.scope) {
@@ -173,18 +377,20 @@ export async function assertCapabilityScope(capability, identity, body, client)
173
377
  return;
174
378
  }
175
379
  case "thread": {
176
- const threadId = identityThreadId(identity);
177
- if (!threadId) {
178
- throw new EnforcementError(403, "thread scope requires thread id (identity source or request body)");
380
+ if (!identityThreadId(identity)) {
381
+ throw new EnforcementError(403, "thread scope requires thread id (identity source, run, or request body)");
179
382
  }
180
383
  return;
181
384
  }
182
385
  case "run": {
183
386
  const threadId = identityThreadId(identity);
184
387
  if (!threadId) {
185
- throw new EnforcementError(403, "run scope requires thread id (identity source or request body)");
388
+ throw new EnforcementError(403, "run scope requires thread id (identity source, run, or request body)");
389
+ }
390
+ const runId = await resolveRunIdForScope(body, client);
391
+ if (!runId) {
392
+ throw new EnforcementError(403, "run scope requires run id or feedback id");
186
393
  }
187
- const runId = bodyString(body, "runId", "run_id");
188
394
  const run = toRecord(await client.readRun({ runId }));
189
395
  const runThread = runThreadId(run);
190
396
  if (!runThread || runThread !== threadId) {
@@ -196,25 +402,43 @@ export async function assertCapabilityScope(capability, identity, body, client)
196
402
  throw new EnforcementError(400, `unknown capability scope "${String(capability.scope)}"`);
197
403
  }
198
404
  }
199
- /** Resolve a run's thread/session id from known LangSmith field locations. */
405
+ /**
406
+ * Resolve a LangGraph thread id from a LangSmith run.
407
+ *
408
+ * Prefer explicit `thread_id` fields. Metadata may also carry `session_id` as
409
+ * the conversation/thread id. Do **not** use the run's top-level `session_id`:
410
+ * on LangSmith that field is the tracing project/session UUID, not the
411
+ * LangGraph thread.
412
+ *
413
+ * @param run - LangSmith run record (top-level and metadata bags).
414
+ * @returns First non-empty LangGraph thread id found, or `undefined`.
415
+ */
200
416
  function runThreadId(run) {
201
- const keys = ["session_id", "sessionId", "thread_id", "threadId"];
202
- for (const key of keys) {
417
+ const threadKeys = ["thread_id", "threadId"];
418
+ const metadataKeys = [
419
+ "thread_id",
420
+ "threadId",
421
+ "session_id",
422
+ "sessionId",
423
+ ];
424
+ for (const key of threadKeys) {
203
425
  const value = run[key];
204
426
  if (typeof value === "string" && value.length > 0) {
205
427
  return value;
206
428
  }
207
429
  }
430
+ const metadataBags = [run.metadata];
208
431
  const extra = run.extra;
209
432
  if (extra && typeof extra === "object" && !Array.isArray(extra)) {
210
433
  const extraRecord = extra;
211
- const metadata = extraRecord.metadata &&
212
- typeof extraRecord.metadata === "object" &&
213
- !Array.isArray(extraRecord.metadata)
214
- ? extraRecord.metadata
215
- : extraRecord;
216
- for (const key of keys) {
217
- const value = metadata[key];
434
+ metadataBags.push(extraRecord.metadata ?? extraRecord);
435
+ }
436
+ for (const bag of metadataBags) {
437
+ if (!bag || typeof bag !== "object" || Array.isArray(bag))
438
+ continue;
439
+ const record = bag;
440
+ for (const key of metadataKeys) {
441
+ const value = record[key];
218
442
  if (typeof value === "string" && value.length > 0) {
219
443
  return value;
220
444
  }
@@ -222,6 +446,34 @@ function runThreadId(run) {
222
446
  }
223
447
  return undefined;
224
448
  }
449
+ /**
450
+ * Resolve the run id for run-scoped ownership checks.
451
+ * Prefer an explicit body `runId`/`run_id`; otherwise load the feedback and
452
+ * use its `run_id` so update/delete cannot skip thread ownership.
453
+ *
454
+ * @param body - Parsed JSON request body.
455
+ * @param client - LangSmith client used to load feedback when needed.
456
+ * @returns Run id to verify against the identity thread, or `undefined`.
457
+ */
458
+ async function resolveRunIdForScope(body, client) {
459
+ const fromBody = optionalBodyString(body, "runId", "run_id");
460
+ if (fromBody) {
461
+ return fromBody;
462
+ }
463
+ const feedbackId = optionalBodyString(body, "feedbackId", "feedback_id");
464
+ if (!feedbackId) {
465
+ return undefined;
466
+ }
467
+ const feedback = toRecord(await client.readFeedback({ feedbackId }));
468
+ const runId = feedback.run_id ?? feedback.runId;
469
+ return typeof runId === "string" && runId.length > 0 ? runId : undefined;
470
+ }
471
+ /**
472
+ * Read the MDA actor id stamped onto feedback metadata (`mda_actor_id`).
473
+ *
474
+ * @param feedback - LangSmith feedback record.
475
+ * @returns Actor id from top-level or `feedback_source` metadata, or `undefined`.
476
+ */
225
477
  function feedbackActorId(feedback) {
226
478
  const direct = feedback.metadata;
227
479
  if (direct && typeof direct === "object" && !Array.isArray(direct)) {
@@ -242,6 +494,12 @@ function feedbackActorId(feedback) {
242
494
  }
243
495
  return undefined;
244
496
  }
497
+ /**
498
+ * Read the MDA thread id stamped onto example metadata (`mda_thread_id`).
499
+ *
500
+ * @param example - LangSmith example record.
501
+ * @returns Thread id from `metadata.mda_thread_id`, or `undefined`.
502
+ */
245
503
  function exampleThreadId(example) {
246
504
  const metadata = example.metadata;
247
505
  if (metadata && typeof metadata === "object" && !Array.isArray(metadata)) {
@@ -252,7 +510,17 @@ function exampleThreadId(example) {
252
510
  }
253
511
  return undefined;
254
512
  }
255
- // --- Call mapping ----------------------------------------------------------
513
+ /**
514
+ * Dispatch a capability action to the matching LangSmith resource handler.
515
+ *
516
+ * @param capability - Capability declaring the resource and constraints.
517
+ * @param action - Requested action name from the body.
518
+ * @param body - Parsed JSON request body.
519
+ * @param deps - Handler dependencies including the LangSmith client.
520
+ * @param identity - Scoped identity (with proven thread id when applicable).
521
+ * @returns Raw LangSmith SDK result before response shaping.
522
+ * @throws {EnforcementError} When the resource is unsupported or the action fails validation.
523
+ */
256
524
  async function performLangSmithCall(capability, action, body, deps, identity) {
257
525
  switch (capability.resource) {
258
526
  case "feedback":
@@ -271,6 +539,18 @@ async function performLangSmithCall(capability, action, body, deps, identity) {
271
539
  throw new EnforcementError(400, `resource "${capability.resource}" is not supported in v0`);
272
540
  }
273
541
  }
542
+ /**
543
+ * Execute a feedback create/update/delete against LangSmith, applying
544
+ * capability constraints (`keys`, `scores`, `maxCommentChars`, `onePerActor`).
545
+ *
546
+ * @param capability - Feedback capability with constraints.
547
+ * @param action - One of `create`, `update`, or `delete`.
548
+ * @param body - Parsed JSON request body.
549
+ * @param client - LangSmith client.
550
+ * @param identity - Runtime identity (actor id for `onePerActor` and metadata).
551
+ * @returns Created feedback, or `void` for update/delete.
552
+ * @throws {EnforcementError} On invalid action, constraint violation, or duplicate actor feedback.
553
+ */
274
554
  async function feedbackCall(capability, action, body, client, identity) {
275
555
  const c = capability.constraints ?? {};
276
556
  if (action === "create") {
@@ -325,6 +605,15 @@ async function feedbackCall(capability, action, body, client, identity) {
325
605
  }
326
606
  throw new EnforcementError(400, `unknown feedback action "${action}"`);
327
607
  }
608
+ /**
609
+ * Execute a runs read or share against LangSmith.
610
+ *
611
+ * @param action - One of `read` or `share`.
612
+ * @param body - Parsed JSON request body (`runId` / `run_id`).
613
+ * @param client - LangSmith client.
614
+ * @returns Run record or share URL payload.
615
+ * @throws {EnforcementError} On unknown action or missing run id.
616
+ */
328
617
  async function runsCall(action, body, client) {
329
618
  const runId = bodyString(body, "runId", "run_id");
330
619
  if (action === "read") {
@@ -335,6 +624,17 @@ async function runsCall(action, body, client) {
335
624
  }
336
625
  throw new EnforcementError(400, `unknown runs action "${action}"`);
337
626
  }
627
+ /**
628
+ * Create a LangSmith example in the capability's dataset, applying field and
629
+ * per-thread caps and stamping MDA metadata.
630
+ *
631
+ * @param capability - Examples capability with `dataset` and related constraints.
632
+ * @param body - Parsed JSON request body (`inputs`, optional `outputs`).
633
+ * @param client - LangSmith client.
634
+ * @param identity - Runtime identity (thread/actor metadata and `maxExamplesPerThread`).
635
+ * @returns Created example.
636
+ * @throws {EnforcementError} On missing dataset, disallowed fields, or thread example cap.
637
+ */
338
638
  async function examplesCall(capability, body, client, identity) {
339
639
  const c = capability.constraints ?? {};
340
640
  const dataset = c.dataset;
@@ -383,6 +683,15 @@ async function examplesCall(capability, body, client, identity) {
383
683
  ...(Object.keys(metadata).length > 0 ? { metadata } : {}),
384
684
  });
385
685
  }
686
+ /**
687
+ * Enqueue a run onto the capability's annotation queue.
688
+ *
689
+ * @param capability - Annotation-queue capability with `queue` and related constraints.
690
+ * @param body - Parsed JSON request body (`runId`, optional `priority` / `note`).
691
+ * @param client - LangSmith client.
692
+ * @returns Resolves when the run is enqueued.
693
+ * @throws {EnforcementError} On missing queue or disallowed priority/note.
694
+ */
386
695
  async function annotationQueueCall(capability, body, client) {
387
696
  const c = capability.constraints ?? {};
388
697
  const queue = c.queue;
@@ -401,6 +710,16 @@ async function annotationQueueCall(capability, body, client) {
401
710
  ...(note !== undefined ? { note } : {}),
402
711
  });
403
712
  }
713
+ /**
714
+ * Execute a threads read or metadata update against LangSmith.
715
+ *
716
+ * @param capability - Threads capability with metadata constraints.
717
+ * @param action - One of `read` or `update_metadata`.
718
+ * @param body - Parsed JSON request body (`threadId`, optional `metadata`).
719
+ * @param client - LangSmith client.
720
+ * @returns Thread/run record, or `void` for metadata updates.
721
+ * @throws {EnforcementError} On unknown action or disallowed metadata keys/title length.
722
+ */
404
723
  async function threadsCall(capability, action, body, client) {
405
724
  const threadId = bodyString(body, "threadId", "thread_id");
406
725
  if (action === "read") {
@@ -428,7 +747,13 @@ async function threadsCall(capability, action, body, client) {
428
747
  throw new EnforcementError(400, `unknown threads action "${action}"`);
429
748
  }
430
749
  // --- Response shaping ------------------------------------------------------
431
- /** Apply the capability's `include` allowlist, then drop any `redact` roots. */
750
+ /**
751
+ * Apply the capability's `include` allowlist, then drop any `redact` roots.
752
+ *
753
+ * @param raw - Full LangSmith result coerced to a plain record.
754
+ * @param capability - Capability whose `response.include` / `response.redact` apply.
755
+ * @returns Allowlisted (and redacted) response object returned to the caller.
756
+ */
432
757
  export function shapeResponse(raw, capability) {
433
758
  const include = capability.response?.include ?? [];
434
759
  const redact = new Set(capability.response?.redact ?? []);
@@ -452,6 +777,13 @@ export function shapeResponse(raw, capability) {
452
777
  }
453
778
  return picked;
454
779
  }
780
+ /**
781
+ * Walk a dotted path into a nested value.
782
+ *
783
+ * @param value - Root value to traverse.
784
+ * @param path - Remaining path segments after the root key.
785
+ * @returns Nested value at `path`, or `undefined` if any segment is missing.
786
+ */
455
787
  function pickNested(value, path) {
456
788
  let current = value;
457
789
  for (const segment of path) {
@@ -465,6 +797,13 @@ function pickNested(value, path) {
465
797
  return current;
466
798
  }
467
799
  // --- Helpers ---------------------------------------------------------------
800
+ /**
801
+ * Extract the capability id from a `/capabilities/{id}` request URL.
802
+ *
803
+ * @param url - Absolute or relative request URL.
804
+ * @returns Decoded capability id path segment.
805
+ * @throws {EnforcementError} When the path has no `/capabilities/` marker.
806
+ */
468
807
  function capabilityIdFromUrl(url) {
469
808
  const { pathname } = new URL(url);
470
809
  const marker = "/capabilities/";
@@ -474,6 +813,13 @@ function capabilityIdFromUrl(url) {
474
813
  }
475
814
  return decodeURIComponent(pathname.slice(index + marker.length));
476
815
  }
816
+ /**
817
+ * Parse the request body as a JSON object.
818
+ *
819
+ * @param request - Incoming HTTP request.
820
+ * @returns Parsed object body.
821
+ * @throws {EnforcementError} When the body is missing, non-JSON, or not an object.
822
+ */
477
823
  async function parseJsonBody(request) {
478
824
  try {
479
825
  const parsed = await request.json();
@@ -486,6 +832,14 @@ async function parseJsonBody(request) {
486
832
  }
487
833
  throw new EnforcementError(400, "request body must be a JSON object");
488
834
  }
835
+ /**
836
+ * Read a required non-empty string field from the body.
837
+ *
838
+ * @param body - Parsed JSON request body.
839
+ * @param key - Exact field name to read.
840
+ * @returns Non-empty string value.
841
+ * @throws {EnforcementError} When the field is missing, non-string, or empty.
842
+ */
489
843
  function requireString(body, key) {
490
844
  const value = body[key];
491
845
  if (typeof value !== "string" || value.length === 0) {
@@ -493,7 +847,15 @@ function requireString(body, key) {
493
847
  }
494
848
  return value;
495
849
  }
496
- /** Read a required string from the body, accepting either camelCase or snake_case. */
850
+ /**
851
+ * Read a required string from the body, accepting either camelCase or snake_case.
852
+ *
853
+ * @param body - Parsed JSON request body.
854
+ * @param camel - Preferred camelCase field name (used in error messages).
855
+ * @param snake - Alternate snake_case field name.
856
+ * @returns Non-empty string from either key.
857
+ * @throws {EnforcementError} When neither key yields a non-empty string.
858
+ */
497
859
  export function bodyString(body, camel, snake) {
498
860
  const value = optionalBodyString(body, camel, snake);
499
861
  if (value === undefined) {
@@ -501,6 +863,14 @@ export function bodyString(body, camel, snake) {
501
863
  }
502
864
  return value;
503
865
  }
866
+ /**
867
+ * Read an optional string from the body, accepting either camelCase or snake_case.
868
+ *
869
+ * @param body - Parsed JSON request body.
870
+ * @param camel - Preferred camelCase field name.
871
+ * @param snake - Alternate snake_case field name.
872
+ * @returns Non-empty string from either key, or `undefined` when both are absent.
873
+ */
504
874
  function optionalBodyString(body, camel, snake) {
505
875
  const camelValue = body[camel];
506
876
  if (typeof camelValue === "string" && camelValue.length > 0) {
@@ -512,6 +882,14 @@ function optionalBodyString(body, camel, snake) {
512
882
  }
513
883
  return undefined;
514
884
  }
885
+ /**
886
+ * Read a required plain-object field from the body.
887
+ *
888
+ * @param body - Parsed JSON request body.
889
+ * @param key - Field name to read.
890
+ * @returns Object value (not an array).
891
+ * @throws {EnforcementError} When the field is missing, not an object, or an array.
892
+ */
515
893
  function requireObject(body, key) {
516
894
  const value = body[key];
517
895
  if (!value || typeof value !== "object" || Array.isArray(value)) {
@@ -519,11 +897,28 @@ function requireObject(body, key) {
519
897
  }
520
898
  return value;
521
899
  }
900
+ /**
901
+ * Enforce an optional allowlist for a string value.
902
+ *
903
+ * @param allowed - Allowed values, or `undefined` to skip the check.
904
+ * @param value - Candidate value.
905
+ * @param label - Field label used in the error message.
906
+ * @throws {EnforcementError} When `allowed` is set and does not include `value`.
907
+ */
522
908
  function assertAllowed(allowed, value, label) {
523
909
  if (allowed && !allowed.includes(value)) {
524
910
  throw new EnforcementError(400, `${label} "${value}" is not allowed`);
525
911
  }
526
912
  }
913
+ /**
914
+ * Read an optional string comment/note from the body and enforce a max length.
915
+ *
916
+ * @param body - Parsed JSON request body.
917
+ * @param maxChars - Maximum allowed length, or `undefined` for no limit.
918
+ * @param field - Body field name (defaults to `"comment"`).
919
+ * @returns Trimmed-present string, or `undefined` when the field is nullish.
920
+ * @throws {EnforcementError} When the field is non-string or exceeds `maxChars`.
921
+ */
527
922
  function optionalComment(body, maxChars, field = "comment") {
528
923
  const value = body[field];
529
924
  if (value === undefined || value === null) {
@@ -537,13 +932,25 @@ function optionalComment(body, maxChars, field = "comment") {
537
932
  }
538
933
  return value;
539
934
  }
935
+ /**
936
+ * Build a JSON HTTP response with a fixed content type.
937
+ *
938
+ * @param status - HTTP status code.
939
+ * @param body - Response payload (coerced via {@link jsonSafe}).
940
+ * @returns `Response` with `application/json` body.
941
+ */
540
942
  function jsonResponse(status, body) {
541
943
  return new Response(JSON.stringify(jsonSafe(body)), {
542
944
  status,
543
945
  headers: { "content-type": "application/json" },
544
946
  });
545
947
  }
546
- /** Coerce values LangSmith may return (Date, etc.) into JSON-serializable form. */
948
+ /**
949
+ * Coerce values LangSmith may return (Date, etc.) into JSON-serializable form.
950
+ *
951
+ * @param value - Arbitrary value from the SDK or engine.
952
+ * @returns JSON-safe clone (Dates → ISO strings; objects/arrays walked recursively).
953
+ */
547
954
  function jsonSafe(value) {
548
955
  if (value === null || typeof value !== "object") {
549
956
  return value;
@@ -560,6 +967,12 @@ function jsonSafe(value) {
560
967
  }
561
968
  return out;
562
969
  }
970
+ /**
971
+ * Map an unknown thrown value to an HTTP status for the error response.
972
+ *
973
+ * @param error - Caught value that is not an {@link EnforcementError}.
974
+ * @returns Status in `400..599` when present on the error; otherwise `502`.
975
+ */
563
976
  function statusFromUnknownError(error) {
564
977
  const status = error?.status;
565
978
  if (typeof status === "number" && status >= 400 && status <= 599) {
@@ -567,13 +980,24 @@ function statusFromUnknownError(error) {
567
980
  }
568
981
  return 502;
569
982
  }
983
+ /**
984
+ * Extract a human-readable message from an unknown thrown value.
985
+ *
986
+ * @param error - Caught value.
987
+ * @returns `Error.message` when available; otherwise `"internal error"`.
988
+ */
570
989
  function errorMessage(error) {
571
990
  if (error instanceof Error) {
572
991
  return error.message;
573
992
  }
574
993
  return "internal error";
575
994
  }
576
- /** Coerce an SDK result (object | void | null) into a plain record for shaping. */
995
+ /**
996
+ * Coerce an SDK result (object | void | null) into a plain record for shaping.
997
+ *
998
+ * @param value - Raw LangSmith call result.
999
+ * @returns Shallow-copied object, or `{}` when `value` is not an object.
1000
+ */
577
1001
  function toRecord(value) {
578
1002
  if (value && typeof value === "object") {
579
1003
  return { ...value };
@@ -587,6 +1011,8 @@ function toRecord(value) {
587
1011
  *
588
1012
  * A connector "score" is numeric/boolean → LangSmith `score`, or categorical
589
1013
  * (string/object) → LangSmith feedback `value`.
1014
+ *
1015
+ * @returns `LangSmithClient` adapter over the official SDK.
590
1016
  */
591
1017
  export function defaultClient() {
592
1018
  let client;
@@ -617,6 +1043,7 @@ export function defaultClient() {
617
1043
  }
618
1044
  return out;
619
1045
  },
1046
+ readFeedback: ({ feedbackId }) => sdk().readFeedback(feedbackId),
620
1047
  readRun: ({ runId }) => sdk().readRun(runId),
621
1048
  shareRun: async ({ runId }) => ({ url: await sdk().shareRun(runId) }),
622
1049
  createExample: ({ dataset, inputs, outputs, metadata }) => sdk().createExample({