@hyperdrive.bot/paseo-server 0.3.40 → 0.3.42

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/server/server/agent/agent-manager.d.ts +15 -0
  2. package/dist/server/server/agent/agent-manager.js +157 -25
  3. package/dist/server/server/agent/agent-projections.js +3 -0
  4. package/dist/server/server/agent/agent-sdk-types.d.ts +23 -0
  5. package/dist/server/server/agent/agent-storage.d.ts +2 -1
  6. package/dist/server/server/agent/agent-storage.js +4 -0
  7. package/dist/server/server/agent/mcp-shared.js +5 -2
  8. package/dist/server/server/agent/providers/claude/agent.d.ts +34 -0
  9. package/dist/server/server/agent/providers/claude/agent.js +74 -0
  10. package/dist/server/server/agent/providers/claude/background-task-tracker.d.ts +38 -1
  11. package/dist/server/server/agent/providers/claude/background-task-tracker.js +114 -9
  12. package/dist/server/server/agent/providers/claude/background-work-kinds.d.ts +95 -0
  13. package/dist/server/server/agent/providers/claude/background-work-kinds.js +73 -0
  14. package/dist/server/server/agent/providers/claude/pty-session-launcher.d.ts +7 -0
  15. package/dist/server/server/agent/providers/claude/pty-session-launcher.js +9 -0
  16. package/dist/server/server/agent/providers/claude/tool-allowlist-guard.d.ts +41 -0
  17. package/dist/server/server/agent/providers/claude/tool-allowlist-guard.js +93 -0
  18. package/dist/server/server/agent/providers/claude/tool-allowlist.d.ts +68 -0
  19. package/dist/server/server/agent/providers/claude/tool-allowlist.js +133 -0
  20. package/dist/server/server/agent/providers/claude/transport/pty.d.ts +17 -0
  21. package/dist/server/server/agent/providers/claude/transport/pty.js +51 -1
  22. package/dist/server/server/agent/providers/claude/transport/tmux.d.ts +74 -0
  23. package/dist/server/server/agent/providers/claude/transport/tmux.js +157 -0
  24. package/dist/server/server/agent/providers/claude/transport/types.d.ts +6 -0
  25. package/dist/server/server/agent/providers/opencode-agent.d.ts +7 -0
  26. package/dist/server/server/agent/providers/opencode-agent.js +51 -1
  27. package/dist/server/server/agent/tools/paseo-tools.d.ts +19 -0
  28. package/dist/server/server/agent/tools/paseo-tools.js +213 -38
  29. package/dist/server/server/agent/tools/read-only-surface.d.ts +1 -0
  30. package/dist/server/server/agent/tools/read-only-surface.js +1 -0
  31. package/dist/server/server/persistence-hooks.js +2 -0
  32. package/dist/server/server/workspace-directory.js +32 -14
  33. package/dist/server/web-ui/_expo/static/js/web/{index-73ebfe5c6b82437cad59d50a40d2c8ef.js → index-9c3bcdc334cf1c08001b6bea510e0292.js} +17 -17
  34. package/dist/server/web-ui/_expo/static/js/web/index-9c3bcdc334cf1c08001b6bea510e0292.js.br +0 -0
  35. package/dist/server/web-ui/_expo/static/js/web/index-9c3bcdc334cf1c08001b6bea510e0292.js.gz +0 -0
  36. package/dist/server/web-ui/_expo/static/js/web/{index-73ebfe5c6b82437cad59d50a40d2c8ef.js.map.br → index-9c3bcdc334cf1c08001b6bea510e0292.js.map.br} +0 -0
  37. package/dist/server/web-ui/_expo/static/js/web/{index-73ebfe5c6b82437cad59d50a40d2c8ef.js.map.gz → index-9c3bcdc334cf1c08001b6bea510e0292.js.map.gz} +0 -0
  38. package/dist/server/web-ui/index.html +1 -1
  39. package/dist/server/web-ui/index.html.br +0 -0
  40. package/dist/server/web-ui/index.html.gz +0 -0
  41. package/package.json +6 -6
  42. package/dist/server/web-ui/_expo/static/js/web/index-73ebfe5c6b82437cad59d50a40d2c8ef.js.br +0 -0
  43. package/dist/server/web-ui/_expo/static/js/web/index-73ebfe5c6b82437cad59d50a40d2c8ef.js.gz +0 -0
@@ -142,6 +142,14 @@ interface ManagedAgentBase {
142
142
  lastUsage?: AgentUsage;
143
143
  lastError?: string;
144
144
  attention: AttentionState;
145
+ /**
146
+ * How many times a "finished" attention event was suppressed on this agent
147
+ * because it still owned armed background work (see checkAndSetAttention).
148
+ * Diagnostic only — never persisted, never projected to clients. It exists so
149
+ * the deliberately-accepted gap (a prose question swallowed mid-monitor) can
150
+ * be MEASURED from real sessions instead of argued about.
151
+ */
152
+ suppressedAttentionCount?: number;
145
153
  foregroundTurnWaiters: Set<ForegroundTurnWaiter>;
146
154
  finalizedForegroundTurnIds: Set<string>;
147
155
  unsubscribeSession: (() => void) | null;
@@ -472,6 +480,13 @@ export declare class AgentManager {
472
480
  private recordTimeline;
473
481
  private emitState;
474
482
  private syncFeaturesFromSession;
483
+ /**
484
+ * Live background work owned by this agent: monitors, crons, and backgrounded
485
+ * shells the provider is still tracking. Mirrors what `agent-projections`
486
+ * publishes as `activeBackgroundTaskCount`, read through the same optional
487
+ * hook so providers that do not implement it simply report zero.
488
+ */
489
+ private getActiveBackgroundTaskCount;
475
490
  private checkAndSetAttention;
476
491
  private enqueueBackgroundPersist;
477
492
  private enqueueDurableTimelineAppend;
@@ -59,6 +59,20 @@ function buildStoredAgentConfig(record) {
59
59
  return stripInternalPaseoMcpServer(config);
60
60
  }
61
61
  export { AGENT_LIFECYCLE_STATUSES };
62
+ /**
63
+ * Reject a create call that asks for a tool allowlist the chosen provider cannot
64
+ * enforce. Silently dropping it would hand the caller a guarantee that does not
65
+ * exist, so the failure is loud and names the provider.
66
+ */
67
+ function requireToolAllowlistSupport(client, config) {
68
+ if (!config.allowedTools || config.allowedTools.length === 0)
69
+ return;
70
+ if (client.supportsToolAllowlist === true)
71
+ return;
72
+ throw new Error(`Provider '${config.provider}' cannot enforce a tool allowlist (allowedTools). ` +
73
+ "Only the claude provider (and providers derived from it) supports runtime tool " +
74
+ "enforcement. Re-run without --allowed-tools, or use a claude-based provider.");
75
+ }
62
76
  function resolveInitialAttention(input) {
63
77
  if (input == null || !input.requiresAttention) {
64
78
  return { requiresAttention: false };
@@ -214,7 +228,10 @@ export class AgentManager {
214
228
  this.mcpAuthToken = options?.mcpAuthToken ?? null;
215
229
  this.configurePaseoTools(options);
216
230
  this.appendSystemPrompt = options.appendSystemPrompt ?? "";
217
- this.logger = options.logger.child({ module: "agent", component: "agent-manager" });
231
+ this.logger = options.logger.child({
232
+ module: "agent",
233
+ component: "agent-manager",
234
+ });
218
235
  this.rescueTimeouts = {
219
236
  reloadSessionCloseMs: options.rescueTimeouts?.reloadSessionCloseMs ?? RELOAD_SESSION_CLOSE_TIMEOUT_MS,
220
237
  interruptSessionMs: options.rescueTimeouts?.interruptSessionMs ?? INTERRUPT_SESSION_TIMEOUT_MS,
@@ -453,7 +470,9 @@ export class AgentManager {
453
470
  }
454
471
  }
455
472
  async listDraftCommands(config) {
456
- const normalizedConfig = await this.normalizeConfig(config, { resolveDefaultModel: false });
473
+ const normalizedConfig = await this.normalizeConfig(config, {
474
+ resolveDefaultModel: false,
475
+ });
457
476
  const client = this.requireClient(normalizedConfig.provider);
458
477
  if (!normalizedConfig.model) {
459
478
  return [];
@@ -482,7 +501,9 @@ export class AgentManager {
482
501
  }
483
502
  }
484
503
  async listDraftFeatures(config) {
485
- const normalizedConfig = await this.normalizeConfig(config, { resolveDefaultModel: false });
504
+ const normalizedConfig = await this.normalizeConfig(config, {
505
+ resolveDefaultModel: false,
506
+ });
486
507
  const client = this.requireClient(normalizedConfig.provider);
487
508
  if (!normalizedConfig.model) {
488
509
  return [];
@@ -541,6 +562,7 @@ export class AgentManager {
541
562
  const client = await this.requireAvailableClient({
542
563
  provider: storedConfig.provider,
543
564
  });
565
+ requireToolAllowlistSupport(client, storedConfig);
544
566
  const launchContext = await this.buildLaunchContext(resolvedAgentId, client, options?.env);
545
567
  const providerLaunchConfig = this.resolveProviderLaunchConfig(launchConfig, launchContext);
546
568
  const createOptions = this.buildCreateSessionOptions(options);
@@ -585,7 +607,9 @@ export class AgentManager {
585
607
  async importProviderSession(input) {
586
608
  const resolvedAgentId = validateAgentId(this.idFactory(), "importProviderSession");
587
609
  this.requireEnabledProvider(input.provider);
588
- const client = await this.requireAvailableClient({ provider: input.provider });
610
+ const client = await this.requireAvailableClient({
611
+ provider: input.provider,
612
+ });
589
613
  if (!client.importSession) {
590
614
  throw new Error(`Provider '${input.provider}' does not support importing sessions`);
591
615
  }
@@ -893,7 +917,10 @@ export class AgentManager {
893
917
  async markRecordArchived(record) {
894
918
  const registry = this.requireRegistry();
895
919
  const archivedAt = new Date().toISOString();
896
- const archivedRecord = buildArchivedAgentRecord(record, { archivedAt, updatedAt: archivedAt });
920
+ const archivedRecord = buildArchivedAgentRecord(record, {
921
+ archivedAt,
922
+ updatedAt: archivedAt,
923
+ });
897
924
  await registry.upsert(archivedRecord);
898
925
  await this.archiveNativeSessionBestEffort(record.provider, record.persistence);
899
926
  if (this.agents.has(record.id)) {
@@ -1009,7 +1036,10 @@ export class AgentManager {
1009
1036
  throw new Error("Agent session does not support setting features");
1010
1037
  }
1011
1038
  await agent.session.setFeature(featureId, value);
1012
- agent.config.featureValues = { ...agent.config.featureValues, [featureId]: value };
1039
+ agent.config.featureValues = {
1040
+ ...agent.config.featureValues,
1041
+ [featureId]: value,
1042
+ };
1013
1043
  this.touchUpdatedAt(agent);
1014
1044
  this.emitState(agent);
1015
1045
  }
@@ -1057,7 +1087,9 @@ export class AgentManager {
1057
1087
  const priorColumn = touchesColumn && this.registry
1058
1088
  ? ((await this.registry.get(agentId))?.labels?.[KANBAN_LABEL_KEY] ?? null)
1059
1089
  : null;
1060
- const nextRecord = await this.writeStoredMetadata(agentId, { labels: patch });
1090
+ const nextRecord = await this.writeStoredMetadata(agentId, {
1091
+ labels: patch,
1092
+ });
1061
1093
  if (touchesColumn) {
1062
1094
  this.cardMoveLog?.record(agentId, priorColumn, nextRecord.labels?.[KANBAN_LABEL_KEY] ?? null);
1063
1095
  }
@@ -1092,7 +1124,9 @@ export class AgentManager {
1092
1124
  }
1093
1125
  return { record, live: true, previousParentAgentId: null };
1094
1126
  }
1095
- const { record } = await this.writeLabels(agentId, { [PARENT_AGENT_ID_LABEL]: null });
1127
+ const { record } = await this.writeLabels(agentId, {
1128
+ [PARENT_AGENT_ID_LABEL]: null,
1129
+ });
1096
1130
  if (!record) {
1097
1131
  throw new Error(`Agent not found in storage after detach: ${agentId}`);
1098
1132
  }
@@ -1106,7 +1140,9 @@ export class AgentManager {
1106
1140
  if (!previousParentAgentId) {
1107
1141
  return { record, live: false, previousParentAgentId: null };
1108
1142
  }
1109
- const result = await this.writeLabels(agentId, { [PARENT_AGENT_ID_LABEL]: null });
1143
+ const result = await this.writeLabels(agentId, {
1144
+ [PARENT_AGENT_ID_LABEL]: null,
1145
+ });
1110
1146
  if (!result.record) {
1111
1147
  throw new Error(`Agent not found in storage after detach: ${agentId}`);
1112
1148
  }
@@ -1277,7 +1313,9 @@ export class AgentManager {
1277
1313
  });
1278
1314
  return;
1279
1315
  }
1280
- this.dispatchStream(agent.id, event, { timestamp: new Date().toISOString() });
1316
+ this.dispatchStream(agent.id, event, {
1317
+ timestamp: new Date().toISOString(),
1318
+ });
1281
1319
  };
1282
1320
  void (async () => {
1283
1321
  try {
@@ -1417,7 +1455,10 @@ export class AgentManager {
1417
1455
  mutableAgent.lifecycle = nextLifecycle;
1418
1456
  const persistenceHandle = mutableAgent.session.describePersistence() ??
1419
1457
  (mutableAgent.runtimeInfo?.sessionId
1420
- ? { provider: mutableAgent.provider, sessionId: mutableAgent.runtimeInfo.sessionId }
1458
+ ? {
1459
+ provider: mutableAgent.provider,
1460
+ sessionId: mutableAgent.runtimeInfo.sessionId,
1461
+ }
1421
1462
  : null);
1422
1463
  if (persistenceHandle) {
1423
1464
  mutableAgent.persistence = attachPersistenceCwd(persistenceHandle, mutableAgent.cwd);
@@ -1574,7 +1615,9 @@ export class AgentManager {
1574
1615
  const bufferedResolution = agent.bufferedPermissionResolutions.get(requestId);
1575
1616
  if (bufferedResolution) {
1576
1617
  agent.bufferedPermissionResolutions.delete(requestId);
1577
- this.dispatchStream(agent.id, bufferedResolution, { timestamp: new Date().toISOString() });
1618
+ this.dispatchStream(agent.id, bufferedResolution, {
1619
+ timestamp: new Date().toISOString(),
1620
+ });
1578
1621
  }
1579
1622
  return result;
1580
1623
  }
@@ -1611,7 +1654,11 @@ export class AgentManager {
1611
1654
  continue;
1612
1655
  }
1613
1656
  for (const task of agent.session.listBackgroundTasks()) {
1614
- rows.push({ agentId: agent.id, agentTitle: agent.config.title ?? null, task });
1657
+ rows.push({
1658
+ agentId: agent.id,
1659
+ agentTitle: agent.config.title ?? null,
1660
+ task,
1661
+ });
1615
1662
  }
1616
1663
  }
1617
1664
  return rows;
@@ -1752,7 +1799,10 @@ export class AgentManager {
1752
1799
  this.logger.info({ agentId, provider: agent.provider, messageId, mode }, "agent.rewind.start");
1753
1800
  await invokeRewindCapability(agent.session, { messageId, mode });
1754
1801
  if (mode !== "files") {
1755
- await this.hydrateTimelineFromProvider(agentId, { force: true, broadcast: true });
1802
+ await this.hydrateTimelineFromProvider(agentId, {
1803
+ force: true,
1804
+ broadcast: true,
1805
+ });
1756
1806
  }
1757
1807
  await this.refreshRuntimeInfo(agent);
1758
1808
  await this.persistSnapshot(agent);
@@ -1998,14 +2048,18 @@ export class AgentManager {
1998
2048
  this.agents.set(resolvedAgentId, managed);
1999
2049
  // Initialize previousStatus to track transitions
2000
2050
  this.previousStatuses.set(resolvedAgentId, managed.lifecycle);
2001
- await this.refreshRuntimeInfo(managed, { emit: !options?.publishWhenReady });
2051
+ await this.refreshRuntimeInfo(managed, {
2052
+ emit: !options?.publishWhenReady,
2053
+ });
2002
2054
  await this.persistSnapshot(managed, {
2003
2055
  title: initialPersistedTitle,
2004
2056
  });
2005
2057
  if (!options?.publishWhenReady) {
2006
2058
  this.emitState(managed, { persist: false });
2007
2059
  }
2008
- await this.refreshSessionState(managed, { emit: !options?.publishWhenReady });
2060
+ await this.refreshSessionState(managed, {
2061
+ emit: !options?.publishWhenReady,
2062
+ });
2009
2063
  managed.lifecycle = "idle";
2010
2064
  await this.persistSnapshot(managed);
2011
2065
  this.emitState(managed, { persist: false });
@@ -2265,7 +2319,9 @@ export class AgentManager {
2265
2319
  this.agentStreamCoalescer.flushAndDiscard(agent.id);
2266
2320
  await this.deleteCommittedTimeline(agent.id);
2267
2321
  this.timelineStore.delete(agent.id);
2268
- this.timelineStore.initialize(agent.id, { timestamp: new Date().toISOString() });
2322
+ this.timelineStore.initialize(agent.id, {
2323
+ timestamp: new Date().toISOString(),
2324
+ });
2269
2325
  agent.historyPrimed = true;
2270
2326
  for (const event of historyEvents) {
2271
2327
  const row = this.recordTimeline(agent.id, event.item, event.timestamp ? { timestamp: event.timestamp } : undefined);
@@ -2333,7 +2389,10 @@ export class AgentManager {
2333
2389
  }
2334
2390
  this.agentStreamCoalescer.flushFor(agent.id);
2335
2391
  }
2336
- const flags = { shouldDispatchEvent: true, shouldNotifyWaiters: true };
2392
+ const flags = {
2393
+ shouldDispatchEvent: true,
2394
+ shouldNotifyWaiters: true,
2395
+ };
2337
2396
  const dispatchPromise = this.dispatchStreamEventByType({
2338
2397
  agent,
2339
2398
  event,
@@ -2349,7 +2408,9 @@ export class AgentManager {
2349
2408
  this.finalizeForegroundTurn(agent, eventTurnId);
2350
2409
  }
2351
2410
  if (!options?.fromHistory && flags.shouldDispatchEvent) {
2352
- this.dispatchStream(agent.id, event, { timestamp: new Date().toISOString() });
2411
+ this.dispatchStream(agent.id, event, {
2412
+ timestamp: new Date().toISOString(),
2413
+ });
2353
2414
  }
2354
2415
  this.traceHandleStreamEventEnd(agent, event, eventTurnId, flags);
2355
2416
  return flags.shouldNotifyWaiters;
@@ -2402,7 +2463,10 @@ export class AgentManager {
2402
2463
  agent.currentModeId = event.currentModeId;
2403
2464
  agent.availableModes = event.availableModes;
2404
2465
  if (agent.runtimeInfo) {
2405
- agent.runtimeInfo = { ...agent.runtimeInfo, modeId: event.currentModeId };
2466
+ agent.runtimeInfo = {
2467
+ ...agent.runtimeInfo,
2468
+ modeId: event.currentModeId,
2469
+ };
2406
2470
  }
2407
2471
  flags.shouldDispatchEvent = false;
2408
2472
  this.emitState(agent);
@@ -2410,7 +2474,10 @@ export class AgentManager {
2410
2474
  case "model_changed":
2411
2475
  agent.runtimeInfo = event.runtimeInfo;
2412
2476
  if (!agent.persistence && event.runtimeInfo.sessionId) {
2413
- agent.persistence = attachPersistenceCwd({ provider: agent.provider, sessionId: event.runtimeInfo.sessionId }, agent.cwd);
2477
+ agent.persistence = attachPersistenceCwd({
2478
+ provider: agent.provider,
2479
+ sessionId: event.runtimeInfo.sessionId,
2480
+ }, agent.cwd);
2414
2481
  }
2415
2482
  agent.currentModeId = event.runtimeInfo.modeId ?? agent.currentModeId;
2416
2483
  flags.shouldDispatchEvent = false;
@@ -2427,9 +2494,20 @@ export class AgentManager {
2427
2494
  this.emitState(agent);
2428
2495
  return undefined;
2429
2496
  case "timeline":
2430
- return this.onStreamTimelineEvent({ agent, event, options, isForegroundEvent, flags });
2497
+ return this.onStreamTimelineEvent({
2498
+ agent,
2499
+ event,
2500
+ options,
2501
+ isForegroundEvent,
2502
+ flags,
2503
+ });
2431
2504
  case "turn_completed":
2432
- this.onStreamTurnCompleted({ agent, event, eventTurnId, isForegroundEvent });
2505
+ this.onStreamTurnCompleted({
2506
+ agent,
2507
+ event,
2508
+ eventTurnId,
2509
+ isForegroundEvent,
2510
+ });
2433
2511
  return undefined;
2434
2512
  case "turn_failed":
2435
2513
  return this.onStreamTurnFailed({
@@ -2440,7 +2518,13 @@ export class AgentManager {
2440
2518
  options,
2441
2519
  });
2442
2520
  case "turn_canceled":
2443
- this.onStreamTurnCanceled({ agent, event, eventTurnId, isForegroundEvent, options });
2521
+ this.onStreamTurnCanceled({
2522
+ agent,
2523
+ event,
2524
+ eventTurnId,
2525
+ isForegroundEvent,
2526
+ options,
2527
+ });
2444
2528
  return undefined;
2445
2529
  case "turn_started":
2446
2530
  this.onStreamTurnStarted({ agent, eventTurnId, isForegroundEvent });
@@ -2689,6 +2773,18 @@ export class AgentManager {
2689
2773
  agent.features = agent.session.features;
2690
2774
  }
2691
2775
  }
2776
+ /**
2777
+ * Live background work owned by this agent: monitors, crons, and backgrounded
2778
+ * shells the provider is still tracking. Mirrors what `agent-projections`
2779
+ * publishes as `activeBackgroundTaskCount`, read through the same optional
2780
+ * hook so providers that do not implement it simply report zero.
2781
+ */
2782
+ getActiveBackgroundTaskCount(agent) {
2783
+ if (!("session" in agent)) {
2784
+ return 0;
2785
+ }
2786
+ return agent.session?.getActiveBackgroundTaskCount?.() ?? 0;
2787
+ }
2692
2788
  checkAndSetAttention(agent) {
2693
2789
  const previousStatus = this.previousStatuses.get(agent.id);
2694
2790
  const currentStatus = agent.lifecycle;
@@ -2704,6 +2800,40 @@ export class AgentManager {
2704
2800
  }
2705
2801
  // Check if agent transitioned from running to idle (finished)
2706
2802
  if (previousStatus === "running" && currentStatus === "idle") {
2803
+ // ...but "the lifecycle went running -> idle" is NOT the same fact as
2804
+ // "this agent is finished and wants you".
2805
+ //
2806
+ // A monitor tick, a cron firing, or a background shell reporting progress
2807
+ // all WAKE the session: status goes idle -> running, the agent prints a
2808
+ // progress line ("1/12 done, I'll let you know"), the turn ends, and the
2809
+ // lifecycle settles back to idle. Under the old rule that transition set
2810
+ // requiresAttention="finished" on EVERY tick, which lit the Unread chip,
2811
+ // the dock badge and a push notification for output nobody needs to act
2812
+ // on. With several sessions in parallel that made Unread — the one chip
2813
+ // you actually watch — useless.
2814
+ //
2815
+ // So: if the agent still owns armed background work, the turn ending is a
2816
+ // progress beat, not a handoff. Stay quiet and let the bucket derivation
2817
+ // surface it as "pending" (alive, will come back on its own) instead.
2818
+ //
2819
+ // KNOWN GAP, deliberately named rather than silently accepted: a turn that
2820
+ // ends with the agent asking a real question in PROSE (not a permission
2821
+ // request, which still routes through needs_input and is unaffected) is
2822
+ // also suppressed while background work is armed. Prose questions are not
2823
+ // machine-detectable here. The counter below exists so that gap is
2824
+ // MEASURED rather than assumed — feed it to the Jarvis judge in shadow
2825
+ // mode (PASEO_JARVIS_SHADOW=1) to size the real distribution before
2826
+ // deciding whether it needs rescuing.
2827
+ const activeBackgroundTaskCount = this.getActiveBackgroundTaskCount(agent);
2828
+ if (activeBackgroundTaskCount > 0) {
2829
+ agent.suppressedAttentionCount = (agent.suppressedAttentionCount ?? 0) + 1;
2830
+ this.logger.debug({
2831
+ agentId: agent.id,
2832
+ activeBackgroundTaskCount,
2833
+ suppressedAttentionCount: agent.suppressedAttentionCount,
2834
+ }, "Suppressed finished-attention: agent still owns armed background work");
2835
+ return;
2836
+ }
2707
2837
  agent.attention = {
2708
2838
  requiresAttention: true,
2709
2839
  attentionReason: "finished",
@@ -2923,7 +3053,9 @@ export class AgentManager {
2923
3053
  if (this.paseoToolsEnabled &&
2924
3054
  client.capabilities.supportsNativePaseoTools &&
2925
3055
  this.paseoToolCatalogFactory) {
2926
- context.paseoTools = await this.paseoToolCatalogFactory({ callerAgentId: agentId });
3056
+ context.paseoTools = await this.paseoToolCatalogFactory({
3057
+ callerAgentId: agentId,
3058
+ });
2927
3059
  }
2928
3060
  return context;
2929
3061
  }
@@ -263,6 +263,9 @@ function buildSerializableConfig(config) {
263
263
  if (config.mcpServers) {
264
264
  serializable.mcpServers = config.mcpServers;
265
265
  }
266
+ if (config.allowedTools?.length) {
267
+ serializable.allowedTools = [...config.allowedTools];
268
+ }
266
269
  return Object.keys(serializable).length ? serializable : null;
267
270
  }
268
271
  function sanitizePendingPermissions(pending) {
@@ -521,6 +521,17 @@ export interface AgentSessionConfig {
521
521
  claude?: Partial<ClaudeAgentOptions>;
522
522
  };
523
523
  mcpServers?: Record<string, McpServerConfig>;
524
+ /**
525
+ * Per-run tool allowlist in Claude Code permission-rule syntax, e.g.
526
+ * ["Read", "Bash(git:*)", "mcp__playwright__browser_navigate"].
527
+ *
528
+ * When present, a tool absent from the list is blocked BEFORE it executes and
529
+ * the model is told why. Undefined or empty means no allowlist. Enforced only
530
+ * by clients that set `AgentClient.supportsToolAllowlist`; a create call that
531
+ * asks any other provider for an allowlist is rejected rather than run
532
+ * unenforced.
533
+ */
534
+ allowedTools?: string[];
524
535
  /**
525
536
  * Internal agents are hidden from listings and don't trigger notifications.
526
537
  * They are used for ephemeral system tasks like commit/PR generation.
@@ -673,6 +684,18 @@ export interface ProviderCatalog {
673
684
  export interface AgentClient {
674
685
  readonly provider: AgentProvider;
675
686
  readonly capabilities: AgentCapabilityFlags;
687
+ /**
688
+ * True when this client can ENFORCE `AgentSessionConfig.allowedTools` at
689
+ * runtime (block a tool outside the list before it executes), not merely
690
+ * accept the field.
691
+ *
692
+ * Absent/false means the create call is rejected when an allowlist is set. A
693
+ * tool allowlist that is quietly dropped is worse than no allowlist: the
694
+ * caller believes a restriction is in force that is not, so this fails loudly
695
+ * instead. Derived providers (`pool` extends `claude`, and so on) reuse the
696
+ * base client class and therefore inherit the correct answer.
697
+ */
698
+ readonly supportsToolAllowlist?: boolean;
676
699
  createSession(config: AgentSessionConfig, launchContext?: AgentLaunchContext, options?: AgentCreateSessionOptions): Promise<AgentSession>;
677
700
  resumeSession(handle: AgentPersistenceHandle, overrides?: Partial<AgentSessionConfig>, launchContext?: AgentLaunchContext): Promise<AgentSession>;
678
701
  /**
@@ -62,6 +62,7 @@ declare const STORED_AGENT_SCHEMA: z.ZodObject<{
62
62
  extra: z.ZodOptional<z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodAny>>>;
63
63
  systemPrompt: z.ZodOptional<z.ZodNullable<z.ZodString>>;
64
64
  mcpServers: z.ZodOptional<z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodAny>>>;
65
+ allowedTools: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodString>>>;
65
66
  }, z.core.$strip>>>;
66
67
  runtimeInfo: z.ZodOptional<z.ZodObject<{
67
68
  provider: z.ZodString;
@@ -112,7 +113,7 @@ declare const STORED_AGENT_SCHEMA: z.ZodObject<{
112
113
  internal: z.ZodOptional<z.ZodBoolean>;
113
114
  archivedAt: z.ZodOptional<z.ZodNullable<z.ZodString>>;
114
115
  }, z.core.$strip>;
115
- export type SerializableAgentConfig = Pick<AgentSessionConfig, "modeId" | "model" | "thinkingOptionId" | "featureValues" | "extra" | "systemPrompt" | "mcpServers">;
116
+ export type SerializableAgentConfig = Pick<AgentSessionConfig, "modeId" | "model" | "thinkingOptionId" | "featureValues" | "extra" | "systemPrompt" | "mcpServers" | "allowedTools">;
116
117
  export type StoredAgentRecord = z.infer<typeof STORED_AGENT_SCHEMA>;
117
118
  export declare function parseStoredAgentRecord(value: unknown): StoredAgentRecord;
118
119
  export declare class AgentStorage {
@@ -13,6 +13,10 @@ const SERIALIZABLE_CONFIG_SCHEMA = z
13
13
  extra: z.record(z.string(), z.any()).nullable().optional(),
14
14
  systemPrompt: z.string().nullable().optional(),
15
15
  mcpServers: z.record(z.string(), z.any()).nullable().optional(),
16
+ // Persisted so a daemon restart or session resume rebuilds the SAME
17
+ // restriction. An allowlist that silently evaporates on resume is worse than
18
+ // none: the operator still believes the run is fenced.
19
+ allowedTools: z.array(z.string()).nullable().optional(),
16
20
  })
17
21
  .nullable()
18
22
  .optional();
@@ -168,7 +168,7 @@ export function parseDurationString(input) {
168
168
  }
169
169
  let totalMs = 0;
170
170
  let hasMatch = false;
171
- const regex = /(\d+)([smh])/g;
171
+ const regex = /(\d+)([smhd])/g;
172
172
  let match;
173
173
  while ((match = regex.exec(trimmed)) !== null) {
174
174
  hasMatch = true;
@@ -183,10 +183,13 @@ export function parseDurationString(input) {
183
183
  case "h":
184
184
  totalMs += value * 60 * 60 * 1000;
185
185
  break;
186
+ case "d":
187
+ totalMs += value * 24 * 60 * 60 * 1000;
188
+ break;
186
189
  }
187
190
  }
188
191
  if (!hasMatch) {
189
- throw new Error(`Invalid duration format: ${input}. Use formats like: 5m, 30s, 1h, 2h30m`);
192
+ throw new Error(`Invalid duration format: ${input}. Use formats like: 5m, 30s, 1h, 2h30m, 1d`);
190
193
  }
191
194
  return totalMs;
192
195
  }
@@ -50,6 +50,7 @@ export declare function readEventIdentifiers(message: SDKMessage): EventIdentifi
50
50
  export declare class ClaudeAgentClient implements AgentClient {
51
51
  readonly provider: "claude";
52
52
  readonly capabilities: AgentCapabilityFlags;
53
+ readonly supportsToolAllowlist = true;
53
54
  private readonly defaults?;
54
55
  private readonly logger;
55
56
  private readonly runtimeSettings?;
@@ -133,6 +134,13 @@ export declare class ClaudeAgentSession implements AgentSession {
133
134
  private readonly emittedUserMessageIds;
134
135
  private readonly rewindTurnAnchors;
135
136
  private pendingFreshSessionId;
137
+ /**
138
+ * Parsed per-run tool allowlist. Empty array = no allowlist configured, which
139
+ * is the historical behaviour (everything the permission mode permits runs).
140
+ */
141
+ private readonly toolAllowRules;
142
+ /** PTY-transport guard files for the current session; removed on teardown. */
143
+ private toolAllowGuard;
136
144
  private cumulativeImageBase64Bytes;
137
145
  private cumulativeImageWarnEmitted;
138
146
  private recentStderr;
@@ -240,6 +248,32 @@ export declare class ClaudeAgentSession implements AgentSession {
240
248
  private buildAppendedSystemPrompt;
241
249
  private buildSdkEnv;
242
250
  private buildOptions;
251
+ /**
252
+ * Install the per-run tool allowlist as a `PreToolUse` hook on the SDK options.
253
+ *
254
+ * Deliberately NOT `options.allowedTools`: that field is a permission ALLOW rule
255
+ * (pre-approve, do not prompt), and this session also sets
256
+ * allowDangerouslySkipPermissions, under which permission rules are moot. A
257
+ * PreToolUse hook is the only gate that still fires; the SDK says so outright
258
+ * ("PreToolUse hook denies bypass canUseTool"). Applied last so an
259
+ * `extra.claude.hooks` override cannot displace it.
260
+ */
261
+ private applyToolAllowlistHook;
262
+ /**
263
+ * PTY-transport half of allowlist enforcement: (re)generate the hook script and
264
+ * the `--settings` file that registers it.
265
+ *
266
+ * Returns a spreadable fragment (`{}` when no allowlist is configured) so the
267
+ * caller stays branch-free. Regenerated per spawn because a session can restart
268
+ * (mode swap, model swap, rewind) and the artifacts are keyed to the session id.
269
+ */
270
+ private refreshToolAllowGuard;
271
+ /**
272
+ * SDK-transport half of allowlist enforcement. The PTY transport enforces the
273
+ * same rules through a generated hook script (see tool-allowlist-guard.ts);
274
+ * both call `isToolAllowed`, so the two transports cannot diverge.
275
+ */
276
+ private enforceToolAllowlistHook;
243
277
  private buildSettingsOptions;
244
278
  private resolveFastModeSetting;
245
279
  private normalizeMcpServers;