@hyperdrive.bot/paseo-server 0.3.40 → 0.3.41

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 (30) hide show
  1. package/dist/server/server/agent/agent-manager.js +15 -0
  2. package/dist/server/server/agent/agent-projections.js +3 -0
  3. package/dist/server/server/agent/agent-sdk-types.d.ts +23 -0
  4. package/dist/server/server/agent/agent-storage.d.ts +2 -1
  5. package/dist/server/server/agent/agent-storage.js +4 -0
  6. package/dist/server/server/agent/mcp-shared.js +5 -2
  7. package/dist/server/server/agent/providers/claude/agent.d.ts +34 -0
  8. package/dist/server/server/agent/providers/claude/agent.js +74 -0
  9. package/dist/server/server/agent/providers/claude/pty-session-launcher.d.ts +7 -0
  10. package/dist/server/server/agent/providers/claude/pty-session-launcher.js +6 -0
  11. package/dist/server/server/agent/providers/claude/tool-allowlist-guard.d.ts +41 -0
  12. package/dist/server/server/agent/providers/claude/tool-allowlist-guard.js +93 -0
  13. package/dist/server/server/agent/providers/claude/tool-allowlist.d.ts +68 -0
  14. package/dist/server/server/agent/providers/claude/tool-allowlist.js +133 -0
  15. package/dist/server/server/agent/tools/paseo-tools.d.ts +19 -0
  16. package/dist/server/server/agent/tools/paseo-tools.js +213 -38
  17. package/dist/server/server/agent/tools/read-only-surface.d.ts +1 -0
  18. package/dist/server/server/agent/tools/read-only-surface.js +1 -0
  19. package/dist/server/server/persistence-hooks.js +2 -0
  20. package/dist/server/web-ui/_expo/static/js/web/{index-73ebfe5c6b82437cad59d50a40d2c8ef.js → index-cb251ddad56c08c3021036af3a43fc0f.js} +5 -5
  21. package/dist/server/web-ui/_expo/static/js/web/index-cb251ddad56c08c3021036af3a43fc0f.js.br +0 -0
  22. package/dist/server/web-ui/_expo/static/js/web/index-cb251ddad56c08c3021036af3a43fc0f.js.gz +0 -0
  23. package/dist/server/web-ui/_expo/static/js/web/{index-73ebfe5c6b82437cad59d50a40d2c8ef.js.map.br → index-cb251ddad56c08c3021036af3a43fc0f.js.map.br} +0 -0
  24. package/dist/server/web-ui/_expo/static/js/web/{index-73ebfe5c6b82437cad59d50a40d2c8ef.js.map.gz → index-cb251ddad56c08c3021036af3a43fc0f.js.map.gz} +0 -0
  25. package/dist/server/web-ui/index.html +1 -1
  26. package/dist/server/web-ui/index.html.br +0 -0
  27. package/dist/server/web-ui/index.html.gz +0 -0
  28. package/package.json +6 -6
  29. package/dist/server/web-ui/_expo/static/js/web/index-73ebfe5c6b82437cad59d50a40d2c8ef.js.br +0 -0
  30. package/dist/server/web-ui/_expo/static/js/web/index-73ebfe5c6b82437cad59d50a40d2c8ef.js.gz +0 -0
@@ -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 };
@@ -541,6 +555,7 @@ export class AgentManager {
541
555
  const client = await this.requireAvailableClient({
542
556
  provider: storedConfig.provider,
543
557
  });
558
+ requireToolAllowlistSupport(client, storedConfig);
544
559
  const launchContext = await this.buildLaunchContext(resolvedAgentId, client, options?.env);
545
560
  const providerLaunchConfig = this.resolveProviderLaunchConfig(launchConfig, launchContext);
546
561
  const createOptions = this.buildCreateSessionOptions(options);
@@ -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;
@@ -24,7 +24,10 @@ import { normalizeProviderReplayTimestamp } from "../../provider-history-timesta
24
24
  import { composeSystemPromptParts } from "../../system-prompt.js";
25
25
  import { SdkTransport } from "./transport/sdk.js";
26
26
  import { PtyTransport } from "./transport/pty.js";
27
+ import { resolvePaseoHome } from "../../../paseo-home.js";
27
28
  import { createPtySession } from "./pty-session-launcher.js";
29
+ import { formatDenialMessage, isToolAllowed, parseToolAllowlist, } from "./tool-allowlist.js";
30
+ import { removeGuardArtifacts, writeGuardArtifacts, } from "./tool-allowlist-guard.js";
28
31
  import { trackTransportSpawned } from "../../../monitoring/telemetry.js";
29
32
  import { isProviderImageMarkdown, materializeProviderImage, renderProviderImageOutputAsAssistantMarkdown, } from "../provider-image-output.js";
30
33
  import { getAgentStreamEventTurnId, } from "../../agent-sdk-types.js";
@@ -1111,6 +1114,9 @@ export class ClaudeAgentClient {
1111
1114
  constructor(options) {
1112
1115
  this.provider = "claude";
1113
1116
  this.capabilities = CLAUDE_CAPABILITIES;
1117
+ // Enforced through a PreToolUse hook on both transports (SDK: options.hooks;
1118
+ // PTY: a generated hook script registered via `claude --settings`).
1119
+ this.supportsToolAllowlist = true;
1114
1120
  this.defaults = options.defaults;
1115
1121
  this.logger = options.logger.child({ module: "agent", provider: "claude" });
1116
1122
  this.runtimeSettings = options.runtimeSettings;
@@ -1575,11 +1581,34 @@ export class ClaudeAgentSession {
1575
1581
  this.emittedUserMessageIds = new Set();
1576
1582
  this.rewindTurnAnchors = [];
1577
1583
  this.pendingFreshSessionId = null;
1584
+ /** PTY-transport guard files for the current session; removed on teardown. */
1585
+ this.toolAllowGuard = null;
1578
1586
  this.cumulativeImageBase64Bytes = 0;
1579
1587
  this.cumulativeImageWarnEmitted = false;
1580
1588
  this.recentStderr = "";
1581
1589
  this.closed = false;
1582
1590
  this.hookEventHandlers = new Map();
1591
+ /**
1592
+ * SDK-transport half of allowlist enforcement. The PTY transport enforces the
1593
+ * same rules through a generated hook script (see tool-allowlist-guard.ts);
1594
+ * both call `isToolAllowed`, so the two transports cannot diverge.
1595
+ */
1596
+ this.enforceToolAllowlistHook = async (input) => {
1597
+ if (input.hook_event_name !== "PreToolUse")
1598
+ return {};
1599
+ const toolName = input.tool_name;
1600
+ if (isToolAllowed(toolName, input.tool_input, this.toolAllowRules))
1601
+ return {};
1602
+ const reason = formatDenialMessage(toolName, this.toolAllowRules);
1603
+ this.logger.warn({ toolName }, "tool call blocked by run tool allowlist");
1604
+ return {
1605
+ hookSpecificOutput: {
1606
+ hookEventName: "PreToolUse",
1607
+ permissionDecision: "deny",
1608
+ permissionDecisionReason: reason,
1609
+ },
1610
+ };
1611
+ };
1583
1612
  this.handlePermissionRequest = async (toolName, input, options) => {
1584
1613
  const requestId = `permission-${randomUUID()}`;
1585
1614
  const kind = resolvePermissionKind(toolName, input);
@@ -1661,6 +1690,10 @@ export class ClaudeAgentSession {
1661
1690
  this.queryFactory = options.queryFactory;
1662
1691
  this.resolveBinary = options.resolveBinary;
1663
1692
  this.contextUsage = new ClaudeContextUsageState(findClaudeModel(this.config.model)?.contextWindowMaxTokens);
1693
+ this.toolAllowRules = parseToolAllowlist(this.config.allowedTools ?? []);
1694
+ if (this.toolAllowRules.length > 0) {
1695
+ this.logger.info({ allowedTools: this.config.allowedTools }, "tool allowlist active: tools outside the list will be blocked before execution");
1696
+ }
1664
1697
  const handle = options.handle;
1665
1698
  if (handle) {
1666
1699
  if (!handle.sessionId) {
@@ -2099,6 +2132,8 @@ export class ClaudeAgentSession {
2099
2132
  hasActiveForegroundTurnId: Boolean(this.activeForegroundTurnId),
2100
2133
  }, "provider.claude.session_close.start");
2101
2134
  this.closed = true;
2135
+ removeGuardArtifacts(this.toolAllowGuard);
2136
+ this.toolAllowGuard = null;
2102
2137
  this.rejectAllPendingPermissions(new Error("Claude session closed"));
2103
2138
  this.cancelCurrentTurn?.();
2104
2139
  this.subscribers.clear();
@@ -2559,6 +2594,7 @@ export class ClaudeAgentSession {
2559
2594
  resume: Boolean(resumeId),
2560
2595
  model: this.config.model,
2561
2596
  permissionMode: this.currentMode,
2597
+ ...this.refreshToolAllowGuard(ptySessionId),
2562
2598
  appendSystemPrompt: this.buildAppendedSystemPrompt(),
2563
2599
  input: input.iterable,
2564
2600
  runtimeSettings: this.runtimeSettings,
@@ -2740,8 +2776,46 @@ export class ClaudeAgentSession {
2740
2776
  ...this.runtimeSettings.disallowedTools,
2741
2777
  ];
2742
2778
  }
2779
+ this.applyToolAllowlistHook(base);
2743
2780
  return base;
2744
2781
  }
2782
+ /**
2783
+ * Install the per-run tool allowlist as a `PreToolUse` hook on the SDK options.
2784
+ *
2785
+ * Deliberately NOT `options.allowedTools`: that field is a permission ALLOW rule
2786
+ * (pre-approve, do not prompt), and this session also sets
2787
+ * allowDangerouslySkipPermissions, under which permission rules are moot. A
2788
+ * PreToolUse hook is the only gate that still fires; the SDK says so outright
2789
+ * ("PreToolUse hook denies bypass canUseTool"). Applied last so an
2790
+ * `extra.claude.hooks` override cannot displace it.
2791
+ */
2792
+ applyToolAllowlistHook(base) {
2793
+ if (this.toolAllowRules.length === 0)
2794
+ return;
2795
+ const existing = base.hooks?.PreToolUse ?? [];
2796
+ base.hooks = {
2797
+ ...base.hooks,
2798
+ PreToolUse: [...existing, { hooks: [this.enforceToolAllowlistHook] }],
2799
+ };
2800
+ }
2801
+ /**
2802
+ * PTY-transport half of allowlist enforcement: (re)generate the hook script and
2803
+ * the `--settings` file that registers it.
2804
+ *
2805
+ * Returns a spreadable fragment (`{}` when no allowlist is configured) so the
2806
+ * caller stays branch-free. Regenerated per spawn because a session can restart
2807
+ * (mode swap, model swap, rewind) and the artifacts are keyed to the session id.
2808
+ */
2809
+ refreshToolAllowGuard(ptySessionId) {
2810
+ removeGuardArtifacts(this.toolAllowGuard);
2811
+ this.toolAllowGuard = writeGuardArtifacts({
2812
+ rules: this.toolAllowRules,
2813
+ sessionId: ptySessionId,
2814
+ baseDir: resolvePaseoHome(),
2815
+ });
2816
+ const guard = this.toolAllowGuard;
2817
+ return guard ? { settingsPath: guard.settingsPath } : {};
2818
+ }
2745
2819
  buildSettingsOptions(extraClaudeOptions, input) {
2746
2820
  const fastMode = this.resolveFastModeSetting();
2747
2821
  if (fastMode === null && !input.ultracode) {
@@ -11,6 +11,13 @@ export interface CreatePtySessionOptions {
11
11
  resume: boolean;
12
12
  model?: string;
13
13
  permissionMode: string;
14
+ /**
15
+ * Extra settings file passed as `claude --settings <path>`. Used to install the
16
+ * per-run tool-allowlist PreToolUse hook (see tool-allowlist-guard.ts). `--settings`
17
+ * LAYERS onto the user/project/local settings claude already reads, so this adds the
18
+ * guard without displacing anything the user configured.
19
+ */
20
+ settingsPath?: string;
14
21
  appendSystemPrompt?: string;
15
22
  /** The shared input channel fed by startTurn() — PtyQuery types these into the PTY. */
16
23
  input: AsyncIterable<SDKUserMessage>;
@@ -70,6 +70,12 @@ function buildInteractiveFlags(opts) {
70
70
  if (opts.permissionMode)
71
71
  flags.push("--permission-mode", opts.permissionMode);
72
72
  flags.push("--dangerously-skip-permissions");
73
+ // Order matters only for readability; claude merges --settings over its own sources.
74
+ // The tool allowlist rides here BECAUSE of the line above: --dangerously-skip-permissions
75
+ // neutralizes every permission rule, and a PreToolUse hook is the one gate it does not
76
+ // skip. Verified on claude 2.1.239.
77
+ if (opts.settingsPath)
78
+ flags.push("--settings", opts.settingsPath);
73
79
  // Inject the same MCP servers the SDK path passes (incl. the paseo MCP) so the PTY agent
74
80
  // keeps its mcp__paseo__* tools (create_agent, terminals, schedules…). claude accepts
75
81
  // --mcp-config as an inline JSON string. Without this, orchestrated agents (the pool /
@@ -0,0 +1,41 @@
1
+ import { type ToolAllowRule } from "./tool-allowlist.js";
2
+ /**
3
+ * Materializes the PTY-transport half of tool-allowlist enforcement: a
4
+ * `PreToolUse` hook script plus the `--settings` file that registers it.
5
+ *
6
+ * The SDK transport enforces the same allowlist in-process (an `options.hooks`
7
+ * callback). The PTY transport spawns a real `claude` terminal, so its gate has
8
+ * to be a command on disk.
9
+ *
10
+ * SINGLE SOURCE OF TRUTH: the guard script is generated by serializing the very
11
+ * functions the in-process path calls (`Function.prototype.toString()`), so the
12
+ * two transports cannot drift into enforcing different rules. `tool-allowlist-guard.test.ts`
13
+ * executes the generated script over the same fixture table that exercises
14
+ * `isToolAllowed()` to keep that guarantee honest. Neither the tsc build nor tsx
15
+ * minifies, so the emitted bodies are valid, type-free JS in dev and in dist.
16
+ */
17
+ export interface GuardArtifacts {
18
+ /** Directory holding both generated files. Delete it on session teardown. */
19
+ readonly dir: string;
20
+ /** Path passed to `claude --settings`. */
21
+ readonly settingsPath: string;
22
+ /** Path of the generated hook script. */
23
+ readonly guardPath: string;
24
+ }
25
+ /** The standalone `PreToolUse` hook script, with the rules baked in. */
26
+ export declare function buildGuardScript(rules: readonly ToolAllowRule[]): string;
27
+ /** The `--settings` payload that registers the guard for every tool call. */
28
+ export declare function buildGuardSettings(nodeBinary: string, guardPath: string): string;
29
+ /**
30
+ * Write the guard script + settings file for a session. Returns null when the
31
+ * allowlist is empty, i.e. no allowlist was configured and nothing is enforced.
32
+ */
33
+ export declare function writeGuardArtifacts(options: {
34
+ readonly rules: readonly ToolAllowRule[];
35
+ readonly sessionId: string;
36
+ readonly baseDir: string;
37
+ readonly nodeBinary?: string;
38
+ }): GuardArtifacts | null;
39
+ /** Best-effort teardown for `writeGuardArtifacts`. */
40
+ export declare function removeGuardArtifacts(artifacts: GuardArtifacts | null | undefined): void;
41
+ //# sourceMappingURL=tool-allowlist-guard.d.ts.map
@@ -0,0 +1,93 @@
1
+ import { mkdirSync, rmSync, writeFileSync } from "node:fs";
2
+ import * as path from "node:path";
3
+ import { SPECIFIER_SOURCES, escapeLiteral, isToolAllowed, readSpecifierValue, specifierMatches, } from "./tool-allowlist.js";
4
+ /** Quote a path for a POSIX/`sh -c` hook command line. */
5
+ function shellQuote(value) {
6
+ return `'${value.replace(/'/g, `'\\''`)}'`;
7
+ }
8
+ /** The standalone `PreToolUse` hook script, with the rules baked in. */
9
+ export function buildGuardScript(rules) {
10
+ return `#!/usr/bin/env node
11
+ // GENERATED by paseo (tool-allowlist-guard.ts). Do not edit; regenerated per session.
12
+ import { readFileSync } from "node:fs";
13
+
14
+ const RULES = ${JSON.stringify(rules)};
15
+ const SPECIFIER_SOURCES = ${JSON.stringify(SPECIFIER_SOURCES)};
16
+
17
+ const escapeLiteral = ${escapeLiteral.toString()};
18
+ const specifierMatches = ${specifierMatches.toString()};
19
+ const readSpecifierValue = ${readSpecifierValue.toString()};
20
+ const isToolAllowed = ${isToolAllowed.toString()};
21
+
22
+ let raw = "";
23
+ try {
24
+ raw = readFileSync(0, "utf8");
25
+ } catch {
26
+ raw = "";
27
+ }
28
+ let event = {};
29
+ try {
30
+ event = JSON.parse(raw);
31
+ } catch {
32
+ event = {};
33
+ }
34
+ const toolName = typeof event.tool_name === "string" ? event.tool_name : "";
35
+ const toolInput = event.tool_input ?? {};
36
+
37
+ // Fail closed: an unreadable event is not a licence to run an unknown tool.
38
+ if (!toolName || !isToolAllowed(toolName, toolInput, RULES)) {
39
+ const listed = RULES.map((r) => (r.specifier === undefined ? r.tool : r.tool + "(" + r.specifier + ")")).join(", ");
40
+ process.stderr.write(
41
+ 'Tool "' + (toolName || "<unknown>") + '" is blocked by this run\\'s tool allowlist and was not executed. ' +
42
+ "Allowed: " + listed + ". Do not retry this tool; achieve the goal with an allowed tool or report that you cannot.\\n",
43
+ );
44
+ process.exit(2);
45
+ }
46
+ process.exit(0);
47
+ `;
48
+ }
49
+ /** The `--settings` payload that registers the guard for every tool call. */
50
+ export function buildGuardSettings(nodeBinary, guardPath) {
51
+ return `${JSON.stringify({
52
+ hooks: {
53
+ PreToolUse: [
54
+ {
55
+ matcher: "*",
56
+ hooks: [
57
+ {
58
+ type: "command",
59
+ command: `${shellQuote(nodeBinary)} ${shellQuote(guardPath)}`,
60
+ },
61
+ ],
62
+ },
63
+ ],
64
+ },
65
+ }, null, 2)}\n`;
66
+ }
67
+ /**
68
+ * Write the guard script + settings file for a session. Returns null when the
69
+ * allowlist is empty, i.e. no allowlist was configured and nothing is enforced.
70
+ */
71
+ export function writeGuardArtifacts(options) {
72
+ if (options.rules.length === 0)
73
+ return null;
74
+ const dir = path.join(options.baseDir, `tool-allowlist-${options.sessionId}`);
75
+ mkdirSync(dir, { recursive: true });
76
+ const guardPath = path.join(dir, "guard.mjs");
77
+ const settingsPath = path.join(dir, "settings.json");
78
+ writeFileSync(guardPath, buildGuardScript(options.rules), { mode: 0o700 });
79
+ writeFileSync(settingsPath, buildGuardSettings(options.nodeBinary ?? process.execPath, guardPath));
80
+ return { dir, settingsPath, guardPath };
81
+ }
82
+ /** Best-effort teardown for `writeGuardArtifacts`. */
83
+ export function removeGuardArtifacts(artifacts) {
84
+ if (!artifacts)
85
+ return;
86
+ try {
87
+ rmSync(artifacts.dir, { recursive: true, force: true });
88
+ }
89
+ catch {
90
+ // teardown is best effort — a leftover temp dir must never fail a session
91
+ }
92
+ }
93
+ //# sourceMappingURL=tool-allowlist-guard.js.map
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Per-run tool allowlist enforcement for the claude provider.
3
+ *
4
+ * WHY A HOOK AND NOT `--allowedTools`
5
+ * -----------------------------------
6
+ * `--allowedTools` (CLI) and `options.allowedTools` (SDK) are *permission allow
7
+ * rules*: they pre-approve a tool so it does not prompt. They do not restrict
8
+ * anything. Both of paseo's claude transports also run the child with
9
+ * `--dangerously-skip-permissions` / `allowDangerouslySkipPermissions`, under
10
+ * which every permission rule is moot, so passing an allowlist there would be
11
+ * decorative: accepted, parsed, and silently unenforcing.
12
+ *
13
+ * A `PreToolUse` hook is the one gate that still fires in that state. The Agent
14
+ * SDK says so explicitly (sdk.d.ts, PermissionDeniedHookInput): "PreToolUse hook
15
+ * denies bypass canUseTool". Verified against claude 2.1.239 with
16
+ * `--permission-mode acceptEdits --dangerously-skip-permissions`: a hook exiting
17
+ * 2 blocked a Bash call while an allowlisted Read call went through.
18
+ *
19
+ * FAIL CLOSED
20
+ * -----------
21
+ * When an allowlist is configured, anything this module cannot positively match
22
+ * is DENIED. A matcher that is too strict fails loudly (the model is told which
23
+ * rule set rejected it, and the run keeps going), while a matcher that is too
24
+ * lax fails silently and hands back a guarantee that is not real. Strictness is
25
+ * the safe direction, so unknown rule shapes and unknown specifier sources deny.
26
+ *
27
+ * Note this is *stricter* than `claude-pool launch --allowedTools`, where tools
28
+ * that never request permission (Read, Grep, TodoWrite, ...) were unaffected by
29
+ * the allowlist. Here the allowlist means exactly what it says: a tool absent
30
+ * from it cannot run at all.
31
+ */
32
+ export interface ToolAllowRule {
33
+ /** Tool name, e.g. "Bash", "Read", "mcp__playwright-personal__browser_navigate". */
34
+ readonly tool: string;
35
+ /** Optional specifier from `Tool(specifier)` form, e.g. "git:*". */
36
+ readonly specifier?: string;
37
+ }
38
+ /** Tools whose specifier (the `Tool(spec)` argument) we know how to read off the input. */
39
+ export declare const SPECIFIER_SOURCES: Record<string, string>;
40
+ /**
41
+ * Parse CSV-or-array allowlist entries into rules. Entries are the same strings
42
+ * Claude Code permission rules use: `Bash`, `Bash(git:*)`, `Read`,
43
+ * `mcp__server__tool`. Blank entries are dropped.
44
+ */
45
+ export declare function parseToolAllowlist(entries: readonly string[]): ToolAllowRule[];
46
+ /** Escape a literal for use inside a RegExp, leaving `*` to be expanded by the caller. */
47
+ export declare function escapeLiteral(value: string): string;
48
+ /**
49
+ * Match a Claude-Code-style specifier pattern against a value.
50
+ *
51
+ * Supported shapes (anything else denies):
52
+ * - `*` any value
53
+ * - `prefix:*` value starts with `prefix` (Claude Code's command-prefix form)
54
+ * - `glob` `*` wildcards, everything else literal, anchored both ends
55
+ */
56
+ export declare function specifierMatches(pattern: string, value: string): boolean;
57
+ /** Read the specifier value a `Tool(spec)` rule compares against, or null when unknown. */
58
+ export declare function readSpecifierValue(toolName: string, input: unknown): string | null;
59
+ /**
60
+ * True when `toolName` (with `input`) is permitted by `rules`.
61
+ *
62
+ * An EMPTY rule list means "no allowlist configured" and allows everything; the
63
+ * caller is responsible for not installing the guard at all in that case.
64
+ */
65
+ export declare function isToolAllowed(toolName: string, input: unknown, rules: readonly ToolAllowRule[]): boolean;
66
+ /** The message handed back to the model when a call is blocked. */
67
+ export declare function formatDenialMessage(toolName: string, rules: readonly ToolAllowRule[]): string;
68
+ //# sourceMappingURL=tool-allowlist.d.ts.map