@workerdeck/core 0.21.0 → 0.23.0

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/build/index.d.mts CHANGED
@@ -510,7 +510,21 @@ type AiSdkRunnerConfig = Omit<CreateSessionRequest, 'cwd'> & {
510
510
  timeoutMs?: number;
511
511
  memoryLimitBytes?: number;
512
512
  }; /** Which backend the executor represents, for `execution_dispatched` events. */
513
- executionBackend?: ToolExecutionBackend; /** Swap models mid-session (`set_model`). Unset = setModel() is rejected. */
513
+ executionBackend?: ToolExecutionBackend;
514
+ /**
515
+ * Decide per call whether the user must approve a tool execution before it
516
+ * dispatches. When the session's permission mode is `'default'` and this
517
+ * callback returns `true`, the runner emits `permission_requested`, parks,
518
+ * and waits for {@link AiSdkRunner.resolvePermission}. Modes
519
+ * `'bypassPermissions'` and `'dontAsk'` skip the check entirely.
520
+ *
521
+ * Unset = every tool dispatches immediately (the pre-§7 behavior).
522
+ */
523
+ shouldApprove?: (call: {
524
+ toolName: string;
525
+ input: unknown;
526
+ }) => boolean; /** Timeout for permission prompts, in ms. Default 120 000 (2 min). */
527
+ approvalTimeoutMs?: number; /** Swap models mid-session (`set_model`). Unset = setModel() is rejected. */
514
528
  resolveModel?: (modelId: string | undefined) => LanguageModel$1;
515
529
  /**
516
530
  * Live MCP status for this session, when the host wired MCP at all. Unlike
@@ -656,7 +670,7 @@ declare class AiSdkRunner implements Runner {
656
670
  resolveToolCall(toolCallId: string, output: ToolCallOutput, options?: {
657
671
  isError?: boolean;
658
672
  }): boolean;
659
- resolvePermission(_requestId: string, _decision: PermissionDecision): boolean;
673
+ resolvePermission(requestId: string, decision: PermissionDecision): boolean;
660
674
  /** Emit `file_delivered` — the deliver_file tool's hand-over event (wired by
661
675
  * createEngineSession via ToolContextOptions.onFileDelivered). */
662
676
  emitFileDelivered(file: {
@@ -1114,9 +1128,21 @@ type EngineSessionOptions = {
1114
1128
  * Executor for sandboxed tools. Return the browser bridge when a client is
1115
1129
  * attached and the server sandbox otherwise; the seam makes them
1116
1130
  * interchangeable, so this is the only place the choice is made.
1131
+ *
1132
+ * A function accepting a {@link ToolExecutionCall} selects **per call**:
1133
+ * `eval_script` may run on the in-process sandbox while a custom tool goes
1134
+ * to the browser, with no coupling between them.
1117
1135
  */
1118
- selectExecutor: () => ToolExecutor; /** Which backend `selectExecutor` returned, for the execution_* events. */
1119
- backend?: 'server' | 'browser' | 'managed' | 'remote'; /** Backends for the granted capabilities. Omitted ones are simply not granted. */
1136
+ selectExecutor: (() => ToolExecutor) | ((call: ToolExecutionCall) => ToolExecutor);
1137
+ /**
1138
+ * Which backend `selectExecutor` returned, for the `execution_dispatched`
1139
+ * events. A function returns the backend per call — pair it with a per-call
1140
+ * `selectExecutor` so the event matches the executor that actually ran.
1141
+ *
1142
+ * When `selectExecutor` is per-call and `backend` is static, every call is
1143
+ * reported under one label. When omitted, falls back to `'server'`.
1144
+ */
1145
+ backend?: 'server' | 'browser' | 'managed' | 'remote' | ((call: ToolExecutionCall) => 'server' | 'browser' | 'managed' | 'remote'); /** Backends for the granted capabilities. Omitted ones are simply not granted. */
1120
1146
  capabilities?: {
1121
1147
  search?: ToolContextOptions['search'];
1122
1148
  download?: ToolContextOptions['download'];
@@ -1175,6 +1201,21 @@ type EngineSessionOptions = {
1175
1201
  timeoutMs?: number;
1176
1202
  memoryLimitBytes?: number;
1177
1203
  };
1204
+ /**
1205
+ * Gate tool execution behind user approval. When this returns `true` for a
1206
+ * given call and the session's permission mode is `'default'`, the runner
1207
+ * emits `permission_requested` and waits for the user to approve or deny
1208
+ * before dispatching. Bypass modes skip the check entirely.
1209
+ *
1210
+ * By default nothing requires approval — tools dispatch as soon as the model
1211
+ * calls them, which is the right call for trusted pipelines and the pre-§7
1212
+ * behavior.
1213
+ */
1214
+ shouldApprove?: (call: {
1215
+ toolName: string;
1216
+ input: unknown;
1217
+ }) => boolean; /** Timeout for permission prompts (ms). Default 120 000. */
1218
+ approvalTimeoutMs?: number;
1178
1219
  /**
1179
1220
  * Initial scratch-filesystem contents for a **new** session, and the safe way
1180
1221
  * to seed one: it is ignored outright when `config.restore` is set, because a
package/build/index.mjs CHANGED
@@ -1899,6 +1899,8 @@ var AiSdkRunner = class {
1899
1899
  /** Model alias as requested (not the resolved provider id) — what set_model was
1900
1900
  * given, so a rehydrated session can re-resolve the same choice. */
1901
1901
  #modelAlias;
1902
+ /** Permission prompts awaiting a client decision. Keyed by request id. */
1903
+ #pendingApprovals = /* @__PURE__ */ new Map();
1902
1904
  constructor(config, id = randomUUID()) {
1903
1905
  const mode = config.permissionMode ?? "default";
1904
1906
  if (!SUPPORTED_PERMISSION_MODES.includes(mode)) throw new Error(`permission mode '${mode}' is not supported by the AI SDK engine`);
@@ -1957,7 +1959,7 @@ var AiSdkRunner = class {
1957
1959
  return [...this.#pendingToolCalls.values()];
1958
1960
  }
1959
1961
  get pendingApprovals() {
1960
- return [];
1962
+ return [...this.#pendingApprovals.values()].map((a) => a.request);
1961
1963
  }
1962
1964
  /** The session's scratch filesystem (see Runner.vfs) — the server's file
1963
1965
  * routes serve deliverables straight from it. */
@@ -1971,14 +1973,17 @@ var AiSdkRunner = class {
1971
1973
  cwd: this.#config.cwd ?? "",
1972
1974
  profile: this.#config.profile,
1973
1975
  engine: "provider",
1974
- capabilities: ENGINE_CAPABILITIES.provider,
1976
+ capabilities: this.#config.shouldApprove ? {
1977
+ ...ENGINE_CAPABILITIES.provider,
1978
+ interactiveApprovals: true
1979
+ } : ENGINE_CAPABILITIES.provider,
1975
1980
  model: this.#modelId(),
1976
1981
  permissionMode: this.#permissionMode,
1977
1982
  createdAt: this.createdAt,
1978
1983
  lastSeq: this.#seq,
1979
1984
  activityCount: this.#activityCount,
1980
1985
  contextUsage: this.#contextUsage,
1981
- pendingPermissionCount: 0,
1986
+ pendingPermissionCount: this.#pendingApprovals.size,
1982
1987
  meta: this.#config.meta,
1983
1988
  scope: this.#config.scope,
1984
1989
  title: this.#title(),
@@ -2163,8 +2168,38 @@ var AiSdkRunner = class {
2163
2168
  });
2164
2169
  return true;
2165
2170
  }
2166
- resolvePermission(_requestId, _decision) {
2167
- return false;
2171
+ resolvePermission(requestId, decision) {
2172
+ const approval = this.#pendingApprovals.get(requestId);
2173
+ if (!approval) return false;
2174
+ clearTimeout(approval.timer);
2175
+ this.#pendingApprovals.delete(requestId);
2176
+ const source = "client";
2177
+ if (decision.behavior === "allow") {
2178
+ this.#emit({
2179
+ type: "permission_resolved",
2180
+ requestId,
2181
+ behavior: "allow",
2182
+ resolvedBy: source
2183
+ });
2184
+ this.#dispatchSingle(approval.toolCallId);
2185
+ } else {
2186
+ const message = decision.message ?? "Permission denied by user";
2187
+ this.#emit({
2188
+ type: "permission_resolved",
2189
+ requestId,
2190
+ behavior: "deny",
2191
+ resolvedBy: source,
2192
+ message
2193
+ });
2194
+ this.#applyExecutionResult(approval.toolCallId, {
2195
+ status: "failed",
2196
+ reason: "permission_denied",
2197
+ error: message
2198
+ });
2199
+ if (decision.interrupt) this.interrupt();
2200
+ }
2201
+ if (this.#pendingApprovals.size === 0 && this.#pendingToolCalls.size === 0) {}
2202
+ return true;
2168
2203
  }
2169
2204
  /** Emit `file_delivered` — the deliver_file tool's hand-over event (wired by
2170
2205
  * createEngineSession via ToolContextOptions.onFileDelivered). */
@@ -2283,6 +2318,8 @@ var AiSdkRunner = class {
2283
2318
  this.#abort?.abort();
2284
2319
  this.#pendingToolCalls.clear();
2285
2320
  this.#dispatched.clear();
2321
+ for (const { timer } of this.#pendingApprovals.values()) clearTimeout(timer);
2322
+ this.#pendingApprovals.clear();
2286
2323
  this.#emit({
2287
2324
  type: "session_closed",
2288
2325
  reason
@@ -2315,50 +2352,111 @@ var AiSdkRunner = class {
2315
2352
  this.#applyExecutionResult(executionId, result);
2316
2353
  return true;
2317
2354
  }
2318
- /** Hand every parked call the executor owns to it. */
2355
+ /** Hand every parked call the executor owns to it, gating on approval when
2356
+ * the permission mode requires it. */
2319
2357
  #dispatchPending() {
2320
2358
  const executor = this.#config.executor;
2321
2359
  if (!executor) return;
2322
2360
  const executable = this.#config.executableTools;
2361
+ const needsApproval = this.#permissionMode === "default" && this.#config.shouldApprove;
2323
2362
  const inFlight = [];
2324
2363
  let anyDeferred = false;
2364
+ let anyAwaiting = false;
2325
2365
  for (const call of Array.from(this.#pendingToolCalls.values())) {
2326
2366
  if (executable && !executable.includes(call.toolName)) continue;
2327
2367
  if (this.#dispatched.has(call.toolCallId)) continue;
2328
- this.#dispatched.add(call.toolCallId);
2329
- const toolCall = {
2330
- executionId: call.toolCallId,
2331
- sessionId: this.id,
2332
- tool: call.toolName,
2333
- input: call.input,
2334
- vfs: this.#config.vfs,
2335
- limits: this.#config.executionLimits,
2336
- signal: this.#abort?.signal
2337
- };
2338
- const profile = executor.describe?.(toolCall) ?? {};
2339
- call.deferred = profile.deferred === true ? true : void 0;
2340
- call.expiresAt = profile.timeoutMs === void 0 ? void 0 : Date.now() + profile.timeoutMs;
2341
- anyDeferred ||= call.deferred === true;
2342
- this.#emit({
2343
- type: "execution_dispatched",
2344
- executionId: call.toolCallId,
2368
+ if (needsApproval && needsApproval({
2345
2369
  toolName: call.toolName,
2346
- backend: profile.backend ?? this.#config.executionBackend ?? "server",
2347
- deferred: call.deferred,
2348
- expiresAt: call.expiresAt
2349
- });
2350
- inFlight.push(executor.dispatch(toolCall).then((dispatch) => {
2351
- if (dispatch.status === "settled") this.#applyExecutionResult(call.toolCallId, dispatch.result);
2352
- }).catch((error) => {
2353
- this.#applyExecutionResult(call.toolCallId, {
2354
- status: "failed",
2355
- reason: "dispatch_error",
2356
- error: error instanceof Error ? error.message : String(error)
2370
+ input: call.input
2371
+ })) {
2372
+ if ([...this.#pendingApprovals.values()].some((a) => a.toolCallId === call.toolCallId)) {
2373
+ anyAwaiting = true;
2374
+ continue;
2375
+ }
2376
+ const requestId = randomUUID();
2377
+ const timeoutMs = this.#config.approvalTimeoutMs ?? 12e4;
2378
+ const request = {
2379
+ id: requestId,
2380
+ toolName: call.toolName,
2381
+ input: call.input,
2382
+ toolUseId: call.toolCallId,
2383
+ title: `Agent wants to run ${call.toolName}`,
2384
+ displayName: call.toolName,
2385
+ expiresAt: Date.now() + timeoutMs
2386
+ };
2387
+ const timer = setTimeout(() => {
2388
+ if (!this.#pendingApprovals.has(requestId)) return;
2389
+ this.resolvePermission(requestId, {
2390
+ behavior: "deny",
2391
+ message: "Approval timed out"
2392
+ });
2393
+ }, timeoutMs);
2394
+ this.#pendingApprovals.set(requestId, {
2395
+ request,
2396
+ toolCallId: call.toolCallId,
2397
+ timer
2357
2398
  });
2358
- }));
2399
+ this.#emit({
2400
+ type: "permission_requested",
2401
+ request
2402
+ });
2403
+ anyAwaiting = true;
2404
+ continue;
2405
+ }
2406
+ this.#dispatched.add(call.toolCallId);
2407
+ const dispatched = this.#dispatchCall(executor, call);
2408
+ anyDeferred ||= dispatched.deferred;
2409
+ inFlight.push(dispatched.promise);
2359
2410
  }
2411
+ if (anyAwaiting) this.#setStatus("awaiting_approval");
2360
2412
  if (anyDeferred) Promise.allSettled(inFlight).then(() => this.#announceParked());
2361
2413
  }
2414
+ /** Dispatch a single tool call that was held behind an approval gate. */
2415
+ #dispatchSingle(toolCallId) {
2416
+ const executor = this.#config.executor;
2417
+ if (!executor) return;
2418
+ const call = this.#pendingToolCalls.get(toolCallId);
2419
+ if (!call || this.#dispatched.has(toolCallId)) return;
2420
+ this.#dispatched.add(toolCallId);
2421
+ const dispatched = this.#dispatchCall(executor, call);
2422
+ if (dispatched.deferred) dispatched.promise.then(() => this.#announceParked());
2423
+ }
2424
+ /** The actual dispatch + event emission for one tool call. */
2425
+ #dispatchCall(executor, call) {
2426
+ const toolCall = {
2427
+ executionId: call.toolCallId,
2428
+ sessionId: this.id,
2429
+ tool: call.toolName,
2430
+ input: call.input,
2431
+ vfs: this.#config.vfs,
2432
+ limits: this.#config.executionLimits,
2433
+ signal: this.#abort?.signal
2434
+ };
2435
+ const profile = executor.describe?.(toolCall) ?? {};
2436
+ call.deferred = profile.deferred === true ? true : void 0;
2437
+ call.expiresAt = profile.timeoutMs === void 0 ? void 0 : Date.now() + profile.timeoutMs;
2438
+ this.#emit({
2439
+ type: "execution_dispatched",
2440
+ executionId: call.toolCallId,
2441
+ toolName: call.toolName,
2442
+ backend: profile.backend ?? this.#config.executionBackend ?? "server",
2443
+ deferred: call.deferred,
2444
+ expiresAt: call.expiresAt
2445
+ });
2446
+ const promise = executor.dispatch(toolCall).then((dispatch) => {
2447
+ if (dispatch.status === "settled") this.#applyExecutionResult(call.toolCallId, dispatch.result);
2448
+ }).catch((error) => {
2449
+ this.#applyExecutionResult(call.toolCallId, {
2450
+ status: "failed",
2451
+ reason: "dispatch_error",
2452
+ error: error instanceof Error ? error.message : String(error)
2453
+ });
2454
+ });
2455
+ return {
2456
+ deferred: call.deferred === true,
2457
+ promise
2458
+ };
2459
+ }
2362
2460
  /**
2363
2461
  * The turn has come to rest on deferred executions: nothing is in flight, and
2364
2462
  * only a host-delivered result can move it. `status_changed: 'parked'` is the
@@ -3579,7 +3677,19 @@ const CAPABILITY_TOOLS = {
3579
3677
  */
3580
3678
  function createEngineSession(options) {
3581
3679
  const vfs = options.config.vfs ?? createVfs(options.config.restore ? options.config.restore.vfs : options.seedVfs);
3582
- const executor = options.selectExecutor();
3680
+ const executor = options.selectExecutor.length > 0 ? {
3681
+ describe(call) {
3682
+ const target = options.selectExecutor(call);
3683
+ const backend = typeof options.backend === "function" ? options.backend(call) : options.backend;
3684
+ return {
3685
+ ...target.describe?.(call),
3686
+ ...backend ? { backend } : {}
3687
+ };
3688
+ },
3689
+ dispatch(call) {
3690
+ return options.selectExecutor(call).dispatch(call);
3691
+ }
3692
+ } : options.selectExecutor();
3583
3693
  const granted = options.config.capabilities ?? options.profile?.session?.capabilities;
3584
3694
  const isGranted = (key) => granted === void 0 || granted.includes(CAPABILITY_TOOLS[key]);
3585
3695
  let runner;
@@ -3613,8 +3723,10 @@ function createEngineSession(options) {
3613
3723
  vfs,
3614
3724
  executor,
3615
3725
  executableTools: context.sandboxedToolNames,
3616
- executionBackend: options.backend ?? "server",
3726
+ executionBackend: typeof options.backend === "function" ? void 0 : options.backend ?? "server",
3617
3727
  executionLimits: options.executionLimits,
3728
+ shouldApprove: options.shouldApprove,
3729
+ approvalTimeoutMs: options.approvalTimeoutMs,
3618
3730
  reportMcpServers: options.mcp ? () => Promise.resolve(declaredServers === void 0 ? options.mcp.servers : options.mcp.servers.filter((s) => declaredServers.includes(s.name))) : void 0
3619
3731
  }, options.id);
3620
3732
  return runner;