@vellumai/assistant 0.4.13 → 0.4.15

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 (133) hide show
  1. package/ARCHITECTURE.md +77 -38
  2. package/README.md +10 -12
  3. package/package.json +1 -1
  4. package/src/__tests__/actor-token-service.test.ts +108 -522
  5. package/src/__tests__/channel-approval-routes.test.ts +92 -239
  6. package/src/__tests__/channel-approval.test.ts +100 -0
  7. package/src/__tests__/conversation-routes-guardian-reply.test.ts +13 -6
  8. package/src/__tests__/conversation-routes.test.ts +11 -4
  9. package/src/__tests__/guardian-actions-endpoint.test.ts +26 -19
  10. package/src/__tests__/mcp-health-check.test.ts +65 -0
  11. package/src/__tests__/permission-types.test.ts +33 -0
  12. package/src/__tests__/scan-result-store.test.ts +121 -0
  13. package/src/__tests__/session-agent-loop.test.ts +120 -0
  14. package/src/__tests__/session-approval-overrides.test.ts +205 -0
  15. package/src/__tests__/session-surfaces-task-progress.test.ts +38 -0
  16. package/src/amazon/client.ts +8 -5
  17. package/src/approvals/guardian-decision-primitive.ts +14 -9
  18. package/src/approvals/guardian-request-resolvers.ts +2 -2
  19. package/src/calls/call-controller.ts +2 -2
  20. package/src/calls/twilio-routes.ts +2 -2
  21. package/src/cli/mcp.ts +3 -3
  22. package/src/cli.ts +24 -0
  23. package/src/config/bundled-skills/chatgpt-import/tools/chatgpt-import.ts +19 -130
  24. package/src/config/bundled-skills/doordash/__tests__/doordash-client.test.ts +8 -6
  25. package/src/config/bundled-skills/google-calendar/SKILL.md +1 -1
  26. package/src/config/bundled-skills/messaging/SKILL.md +49 -14
  27. package/src/config/bundled-skills/messaging/TOOLS.json +52 -9
  28. package/src/config/bundled-skills/messaging/tools/gmail-batch-archive.ts +35 -11
  29. package/src/config/bundled-skills/messaging/tools/gmail-draft.ts +3 -1
  30. package/src/config/bundled-skills/messaging/tools/gmail-forward.ts +5 -6
  31. package/src/config/bundled-skills/messaging/tools/gmail-outreach-scan.ts +10 -2
  32. package/src/config/bundled-skills/messaging/tools/gmail-send-draft.ts +20 -0
  33. package/src/config/bundled-skills/messaging/tools/gmail-send-with-attachments.ts +3 -4
  34. package/src/config/bundled-skills/messaging/tools/gmail-sender-digest.ts +16 -8
  35. package/src/config/bundled-skills/messaging/tools/messaging-reply.ts +76 -0
  36. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +10 -0
  37. package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +11 -3
  38. package/src/config/bundled-skills/messaging/tools/scan-result-store.ts +86 -0
  39. package/src/config/bundled-skills/phone-calls/SKILL.md +2 -2
  40. package/src/config/bundled-skills/skills-catalog/SKILL.md +31 -8
  41. package/src/config/bundled-skills/slack/tools/slack-add-reaction.ts +1 -1
  42. package/src/config/bundled-skills/slack/tools/slack-channel-details.ts +1 -1
  43. package/src/config/bundled-skills/slack/tools/slack-delete-message.ts +1 -1
  44. package/src/config/bundled-skills/slack/tools/slack-leave-channel.ts +1 -1
  45. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +79 -24
  46. package/src/config/bundled-skills/sms-setup/SKILL.md +1 -1
  47. package/src/config/bundled-skills/telegram-setup/SKILL.md +1 -1
  48. package/src/config/bundled-skills/twilio-setup/SKILL.md +1 -1
  49. package/src/daemon/approval-generators.ts +6 -3
  50. package/src/daemon/handlers/config-ingress.ts +2 -6
  51. package/src/daemon/handlers/guardian-actions.ts +1 -1
  52. package/src/daemon/handlers/sessions.ts +4 -1
  53. package/src/daemon/handlers/shared.ts +3 -0
  54. package/src/daemon/handlers/skills.ts +32 -0
  55. package/src/daemon/ipc-contract/messages.ts +3 -1
  56. package/src/daemon/ipc-handler.ts +24 -0
  57. package/src/daemon/ipc-validate.ts +1 -1
  58. package/src/daemon/lifecycle.ts +6 -8
  59. package/src/daemon/server.ts +8 -3
  60. package/src/daemon/session-agent-loop.ts +19 -1
  61. package/src/daemon/session-attachments.ts +2 -1
  62. package/src/daemon/session-history.ts +2 -2
  63. package/src/daemon/session-process.ts +5 -9
  64. package/src/daemon/session-surfaces.ts +17 -1
  65. package/src/daemon/session-tool-setup.ts +216 -69
  66. package/src/daemon/session.ts +24 -1
  67. package/src/events/domain-events.ts +1 -1
  68. package/src/events/tool-domain-event-publisher.ts +5 -10
  69. package/src/influencer/client.ts +8 -7
  70. package/src/messaging/providers/gmail/client.ts +33 -1
  71. package/src/messaging/providers/gmail/mime-builder.ts +5 -1
  72. package/src/messaging/providers/sms/adapter.ts +3 -7
  73. package/src/messaging/providers/telegram-bot/adapter.ts +3 -7
  74. package/src/messaging/providers/whatsapp/adapter.ts +3 -7
  75. package/src/notifications/adapters/sms.ts +2 -2
  76. package/src/notifications/adapters/telegram.ts +2 -2
  77. package/src/permissions/prompter.ts +2 -0
  78. package/src/permissions/types.ts +11 -1
  79. package/src/runtime/approval-conversation-turn.ts +4 -0
  80. package/src/runtime/auth/__tests__/context.test.ts +130 -0
  81. package/src/runtime/auth/__tests__/credential-service.test.ts +277 -0
  82. package/src/runtime/auth/__tests__/guard-tests.test.ts +289 -0
  83. package/src/runtime/auth/__tests__/ipc-auth-context.test.ts +71 -0
  84. package/src/runtime/auth/__tests__/middleware.test.ts +239 -0
  85. package/src/runtime/auth/__tests__/policy.test.ts +29 -0
  86. package/src/runtime/auth/__tests__/route-policy.test.ts +166 -0
  87. package/src/runtime/auth/__tests__/scopes.test.ts +109 -0
  88. package/src/runtime/auth/__tests__/subject.test.ts +149 -0
  89. package/src/runtime/auth/__tests__/token-service.test.ts +263 -0
  90. package/src/runtime/auth/context.ts +62 -0
  91. package/src/runtime/{actor-refresh-token-service.ts → auth/credential-service.ts} +112 -79
  92. package/src/runtime/auth/external-assistant-id.ts +69 -0
  93. package/src/runtime/auth/index.ts +37 -0
  94. package/src/runtime/auth/middleware.ts +127 -0
  95. package/src/runtime/auth/policy.ts +17 -0
  96. package/src/runtime/auth/route-policy.ts +261 -0
  97. package/src/runtime/auth/scopes.ts +64 -0
  98. package/src/runtime/auth/subject.ts +68 -0
  99. package/src/runtime/auth/token-service.ts +275 -0
  100. package/src/runtime/auth/types.ts +79 -0
  101. package/src/runtime/channel-approval-parser.ts +11 -5
  102. package/src/runtime/channel-approval-types.ts +1 -1
  103. package/src/runtime/channel-approvals.ts +22 -1
  104. package/src/runtime/guardian-action-followup-executor.ts +2 -2
  105. package/src/runtime/guardian-context-resolver.ts +15 -0
  106. package/src/runtime/guardian-decision-types.ts +23 -6
  107. package/src/runtime/guardian-outbound-actions.ts +4 -22
  108. package/src/runtime/guardian-reply-router.ts +5 -3
  109. package/src/runtime/http-server.ts +210 -182
  110. package/src/runtime/http-types.ts +11 -1
  111. package/src/runtime/local-actor-identity.ts +25 -0
  112. package/src/runtime/pending-interactions.ts +1 -0
  113. package/src/runtime/routes/approval-routes.ts +42 -59
  114. package/src/runtime/routes/channel-route-shared.ts +9 -41
  115. package/src/runtime/routes/channel-routes.ts +0 -2
  116. package/src/runtime/routes/conversation-routes.ts +39 -49
  117. package/src/runtime/routes/events-routes.ts +15 -22
  118. package/src/runtime/routes/guardian-action-routes.ts +46 -51
  119. package/src/runtime/routes/guardian-approval-interception.ts +6 -5
  120. package/src/runtime/routes/guardian-bootstrap-routes.ts +12 -8
  121. package/src/runtime/routes/guardian-refresh-routes.ts +2 -2
  122. package/src/runtime/routes/inbound-message-handler.ts +39 -45
  123. package/src/runtime/routes/pairing-routes.ts +9 -9
  124. package/src/runtime/routes/secret-routes.ts +90 -45
  125. package/src/runtime/routes/surface-action-routes.ts +12 -2
  126. package/src/runtime/routes/trust-rules-routes.ts +13 -0
  127. package/src/runtime/routes/twilio-routes.ts +3 -3
  128. package/src/runtime/session-approval-overrides.ts +86 -0
  129. package/src/security/keychain-to-encrypted-migration.ts +8 -1
  130. package/src/skills/frontmatter.ts +44 -1
  131. package/src/tools/permission-checker.ts +226 -74
  132. package/src/runtime/actor-token-service.ts +0 -234
  133. package/src/runtime/middleware/actor-token.ts +0 -265
@@ -1,16 +1,26 @@
1
- import { getConfig } from '../config/loader.js';
2
- import { getHookManager } from '../hooks/manager.js';
3
- import { check, classifyRisk, generateAllowlistOptions, generateScopeOptions } from '../permissions/checker.js';
4
- import type { PermissionPrompter } from '../permissions/prompter.js';
5
- import { addRule } from '../permissions/trust-store.js';
6
- import { getLogger } from '../util/logger.js';
7
- import { buildPolicyContext } from './policy-context.js';
8
- import { isSideEffectTool } from './side-effects.js';
9
- import { wrapCommand } from './terminal/sandbox.js';
10
- import type { ExecutionTarget } from './types.js';
11
- import type { Tool, ToolContext, ToolLifecycleEvent } from './types.js';
1
+ import { getConfig } from "../config/loader.js";
2
+ import { getHookManager } from "../hooks/manager.js";
3
+ import {
4
+ check,
5
+ classifyRisk,
6
+ generateAllowlistOptions,
7
+ generateScopeOptions,
8
+ } from "../permissions/checker.js";
9
+ import type { PermissionPrompter } from "../permissions/prompter.js";
10
+ import { addRule } from "../permissions/trust-store.js";
11
+ import {
12
+ getEffectiveMode,
13
+ setThreadMode,
14
+ setTimedMode,
15
+ } from "../runtime/session-approval-overrides.js";
16
+ import { getLogger } from "../util/logger.js";
17
+ import { buildPolicyContext } from "./policy-context.js";
18
+ import { isSideEffectTool } from "./side-effects.js";
19
+ import { wrapCommand } from "./terminal/sandbox.js";
20
+ import type { ExecutionTarget } from "./types.js";
21
+ import type { Tool, ToolContext, ToolLifecycleEvent } from "./types.js";
12
22
 
13
- const log = getLogger('permission-checker');
23
+ const log = getLogger("permission-checker");
14
24
 
15
25
  export type PermissionDecision =
16
26
  | { allowed: true; decision: string; riskLevel: string }
@@ -36,11 +46,32 @@ export class PermissionChecker {
36
46
  context: ToolContext,
37
47
  executionTarget: ExecutionTarget,
38
48
  emitLifecycleEvent: (event: ToolLifecycleEvent) => void,
39
- sanitizeToolInput: (toolName: string, input: Record<string, unknown>) => Record<string, unknown>,
49
+ sanitizeToolInput: (
50
+ toolName: string,
51
+ input: Record<string, unknown>,
52
+ ) => Record<string, unknown>,
40
53
  startTime: number,
41
- computePreviewDiff: (toolName: string, input: Record<string, unknown>, workingDir: string) => { filePath: string; oldContent: string; newContent: string; isNewFile: boolean } | undefined,
54
+ computePreviewDiff: (
55
+ toolName: string,
56
+ input: Record<string, unknown>,
57
+ workingDir: string,
58
+ ) =>
59
+ | {
60
+ filePath: string;
61
+ oldContent: string;
62
+ newContent: string;
63
+ isNewFile: boolean;
64
+ }
65
+ | undefined,
42
66
  ): Promise<PermissionDecision> {
43
- const risk = await classifyRisk(name, input, context.workingDir, undefined, undefined, context.signal);
67
+ const risk = await classifyRisk(
68
+ name,
69
+ input,
70
+ context.workingDir,
71
+ undefined,
72
+ undefined,
73
+ context.signal,
74
+ );
44
75
  const riskLevel: string = risk;
45
76
 
46
77
  // Wrap the rest of permission evaluation so that any exception
@@ -49,24 +80,32 @@ export class PermissionChecker {
49
80
  // low risk, degrading audit/alert accuracy for high-risk attempts.
50
81
  try {
51
82
  const policyContext = buildPolicyContext(tool, context);
52
- const result = await check(name, input, context.workingDir, policyContext, undefined, context.signal);
83
+ const result = await check(
84
+ name,
85
+ input,
86
+ context.workingDir,
87
+ policyContext,
88
+ undefined,
89
+ context.signal,
90
+ );
53
91
 
54
92
  // Private threads force prompting for side-effect tools even when a
55
93
  // trust/allow rule would auto-allow. Deny decisions are preserved —
56
94
  // only allow → prompt promotion happens here.
57
95
  if (
58
- context.forcePromptSideEffects
59
- && result.decision === 'allow'
60
- && isSideEffectTool(name, input)
96
+ context.forcePromptSideEffects &&
97
+ result.decision === "allow" &&
98
+ isSideEffectTool(name, input)
61
99
  ) {
62
- result.decision = 'prompt';
63
- result.reason = 'Private thread: side-effect tools require explicit approval';
100
+ result.decision = "prompt";
101
+ result.reason =
102
+ "Private thread: side-effect tools require explicit approval";
64
103
  }
65
104
 
66
- if (result.decision === 'deny') {
105
+ if (result.decision === "deny") {
67
106
  const durationMs = Date.now() - startTime;
68
107
  emitLifecycleEvent({
69
- type: 'permission_denied',
108
+ type: "permission_denied",
70
109
  toolName: name,
71
110
  executionTarget,
72
111
  input,
@@ -75,21 +114,29 @@ export class PermissionChecker {
75
114
  conversationId: context.conversationId,
76
115
  requestId: context.requestId,
77
116
  riskLevel,
78
- decision: 'deny',
117
+ decision: "deny",
79
118
  reason: result.reason,
80
119
  durationMs,
81
120
  });
82
- return { allowed: false, decision: 'denied', riskLevel, content: result.reason };
121
+ return {
122
+ allowed: false,
123
+ decision: "denied",
124
+ riskLevel,
125
+ content: result.reason,
126
+ };
83
127
  }
84
128
 
85
- if (result.decision === 'prompt') {
129
+ if (result.decision === "prompt") {
86
130
  // Non-interactive sessions have no client to respond to prompts —
87
131
  // deny immediately instead of blocking for the full permission timeout.
88
132
  if (context.isInteractive === false) {
89
133
  const durationMs = Date.now() - startTime;
90
- log.info({ toolName: name, riskLevel }, 'Auto-denying prompt for non-interactive session');
134
+ log.info(
135
+ { toolName: name, riskLevel },
136
+ "Auto-denying prompt for non-interactive session",
137
+ );
91
138
  emitLifecycleEvent({
92
- type: 'permission_denied',
139
+ type: "permission_denied",
93
140
  toolName: name,
94
141
  executionTarget,
95
142
  input,
@@ -98,40 +145,86 @@ export class PermissionChecker {
98
145
  conversationId: context.conversationId,
99
146
  requestId: context.requestId,
100
147
  riskLevel,
101
- decision: 'deny',
102
- reason: 'Non-interactive session: no client to approve prompt',
148
+ decision: "deny",
149
+ reason: "Non-interactive session: no client to approve prompt",
103
150
  durationMs,
104
151
  });
105
152
  return {
106
153
  allowed: false,
107
- decision: 'denied',
154
+ decision: "denied",
108
155
  riskLevel,
109
156
  content: `Permission denied: tool "${name}" requires user approval but no interactive client is connected. The tool was not executed. To allow this tool in non-interactive sessions, add a trust rule via permission settings.`,
110
157
  };
111
158
  }
112
159
 
113
- const allowlistOptions = await generateAllowlistOptions(name, input, context.signal);
160
+ // Temporary approval override: if the guardian has enabled a
161
+ // conversation-scoped "allow all" mode (allow_10m or allow_thread),
162
+ // skip the interactive prompt and auto-approve. Only applies to
163
+ // guardian actors — untrusted actors cannot leverage this to bypass
164
+ // guardian-required gates (those are enforced in pre-execution gates).
165
+ // Proxied bash commands require per-invocation approval and must not
166
+ // be auto-approved by a temporary override — they are excluded here
167
+ // by requiring persistent decisions to be allowed.
168
+ const persistentDecisionsAllowedForOverride = !(
169
+ name === "bash" && input.network_mode === "proxied"
170
+ );
171
+ if (
172
+ context.guardianTrustClass === "guardian" &&
173
+ persistentDecisionsAllowedForOverride &&
174
+ getEffectiveMode(context.conversationId) !== undefined
175
+ ) {
176
+ log.info(
177
+ {
178
+ toolName: name,
179
+ riskLevel,
180
+ conversationId: context.conversationId,
181
+ },
182
+ "Temporary approval override active — auto-approving without prompt",
183
+ );
184
+ return { allowed: true, decision: "temporary_override", riskLevel };
185
+ }
186
+
187
+ const allowlistOptions = await generateAllowlistOptions(
188
+ name,
189
+ input,
190
+ context.signal,
191
+ );
114
192
  const scopeOptions = generateScopeOptions(context.workingDir, name);
115
193
  const previewDiff = computePreviewDiff(name, input, context.workingDir);
116
194
 
117
195
  let sandboxed: boolean | undefined;
118
- if (name === 'bash' && typeof input.command === 'string') {
196
+ if (name === "bash" && typeof input.command === "string") {
119
197
  const cfg = getConfig();
120
- const sandboxConfig = context.sandboxOverride != null
121
- ? { ...cfg.sandbox, enabled: context.sandboxOverride }
122
- : cfg.sandbox;
123
- const wrapped = wrapCommand(input.command, context.workingDir, sandboxConfig);
198
+ const sandboxConfig =
199
+ context.sandboxOverride != null
200
+ ? { ...cfg.sandbox, enabled: context.sandboxOverride }
201
+ : cfg.sandbox;
202
+ const wrapped = wrapCommand(
203
+ input.command,
204
+ context.workingDir,
205
+ sandboxConfig,
206
+ );
124
207
  sandboxed = wrapped.sandboxed;
125
208
  }
126
209
 
127
210
  // Proxied bash prompts are non-persistent — no trust rule saving allowed
128
211
  const persistentDecisionsAllowed = !(
129
- name === 'bash'
130
- && input.network_mode === 'proxied'
212
+ name === "bash" && input.network_mode === "proxied"
131
213
  );
132
214
 
215
+ // Only offer temporary approval options to guardians when persistent
216
+ // decisions are allowed (proxied bash is excluded since it requires
217
+ // per-invocation approval).
218
+ const temporaryOptionsAvailable:
219
+ | Array<"allow_10m" | "allow_thread">
220
+ | undefined =
221
+ persistentDecisionsAllowed &&
222
+ context.guardianTrustClass === "guardian"
223
+ ? ["allow_10m", "allow_thread"]
224
+ : undefined;
225
+
133
226
  emitLifecycleEvent({
134
- type: 'permission_prompt',
227
+ type: "permission_prompt",
135
228
  toolName: name,
136
229
  executionTarget,
137
230
  input,
@@ -148,7 +241,7 @@ export class PermissionChecker {
148
241
  persistentDecisionsAllowed,
149
242
  });
150
243
 
151
- await getHookManager().trigger('permission-request', {
244
+ await getHookManager().trigger("permission-request", {
152
245
  toolName: name,
153
246
  input: sanitizeToolInput(name, input),
154
247
  riskLevel,
@@ -167,30 +260,34 @@ export class PermissionChecker {
167
260
  executionTarget,
168
261
  persistentDecisionsAllowed,
169
262
  context.signal,
263
+ temporaryOptionsAvailable,
170
264
  );
171
265
 
172
266
  const decision = response.decision;
173
267
 
174
- await getHookManager().trigger('permission-resolve', {
268
+ await getHookManager().trigger("permission-resolve", {
175
269
  toolName: name,
176
270
  decision: response.decision,
177
271
  riskLevel,
178
272
  sessionId: context.sessionId,
179
273
  });
180
274
 
181
- if (response.decision === 'deny') {
182
- const contextualDenial = typeof response.decisionContext === 'string'
183
- ? response.decisionContext.trim()
184
- : '';
185
- const denialMessage = contextualDenial.length > 0
186
- ? contextualDenial
187
- : `Permission denied by user. The user chose not to allow the "${name}" tool. Do NOT retry this tool call immediately. Instead, tell the user that the action was not performed because they denied permission, and ask if they would like you to try again or take a different approach. Wait for the user to explicitly respond before retrying.`;
188
- const denialReason = contextualDenial.length > 0
189
- ? `Permission denied (${name}): contextual policy`
190
- : 'Permission denied by user';
275
+ if (response.decision === "deny") {
276
+ const contextualDenial =
277
+ typeof response.decisionContext === "string"
278
+ ? response.decisionContext.trim()
279
+ : "";
280
+ const denialMessage =
281
+ contextualDenial.length > 0
282
+ ? contextualDenial
283
+ : `Permission denied by user. The user chose not to allow the "${name}" tool. Do NOT retry this tool call immediately. Instead, tell the user that the action was not performed because they denied permission, and ask if they would like you to try again or take a different approach. Wait for the user to explicitly respond before retrying.`;
284
+ const denialReason =
285
+ contextualDenial.length > 0
286
+ ? `Permission denied (${name}): contextual policy`
287
+ : "Permission denied by user";
191
288
  const durationMs = Date.now() - startTime;
192
289
  emitLifecycleEvent({
193
- type: 'permission_denied',
290
+ type: "permission_denied",
194
291
  toolName: name,
195
292
  executionTarget,
196
293
  input,
@@ -199,30 +296,47 @@ export class PermissionChecker {
199
296
  conversationId: context.conversationId,
200
297
  requestId: context.requestId,
201
298
  riskLevel,
202
- decision: 'deny',
299
+ decision: "deny",
203
300
  reason: denialReason,
204
301
  durationMs,
205
302
  });
206
- return { allowed: false, decision, riskLevel, content: denialMessage };
303
+ return {
304
+ allowed: false,
305
+ decision,
306
+ riskLevel,
307
+ content: denialMessage,
308
+ };
207
309
  }
208
310
 
209
- if (response.decision === 'always_deny') {
311
+ if (response.decision === "always_deny") {
210
312
  // For non-scoped tools (empty scopeOptions), default to 'everywhere' since
211
313
  // the client has no scope picker and will send undefined.
212
- const effectiveDenyScope = scopeOptions.length === 0
213
- ? (response.selectedScope ?? 'everywhere')
214
- : response.selectedScope;
215
- const ruleSaved = !!(persistentDecisionsAllowed && response.selectedPattern && effectiveDenyScope);
314
+ const effectiveDenyScope =
315
+ scopeOptions.length === 0
316
+ ? (response.selectedScope ?? "everywhere")
317
+ : response.selectedScope;
318
+ const ruleSaved = !!(
319
+ persistentDecisionsAllowed &&
320
+ response.selectedPattern &&
321
+ effectiveDenyScope
322
+ );
216
323
  if (ruleSaved) {
217
- addRule(name, response.selectedPattern!, effectiveDenyScope!, 'deny');
324
+ addRule(
325
+ name,
326
+ response.selectedPattern!,
327
+ effectiveDenyScope!,
328
+ "deny",
329
+ );
218
330
  }
219
- const denialReason = ruleSaved ? 'Permission denied by user (rule saved)' : 'Permission denied by user';
331
+ const denialReason = ruleSaved
332
+ ? "Permission denied by user (rule saved)"
333
+ : "Permission denied by user";
220
334
  const denialMessage = ruleSaved
221
335
  ? `Permission denied by user, and a rule was saved to always deny the "${name}" tool for this pattern. Do NOT retry this tool call. Inform the user that this action has been permanently blocked by their preference. If the user wants to allow it in the future, they can update their permission rules.`
222
336
  : `Permission denied by user. The user chose not to allow the "${name}" tool. Do NOT retry this tool call immediately. Instead, tell the user that the action was not performed because they denied permission, and ask if they would like you to try again or take a different approach. Wait for the user to explicitly respond before retrying.`;
223
337
  const durationMs = Date.now() - startTime;
224
338
  emitLifecycleEvent({
225
- type: 'permission_denied',
339
+ type: "permission_denied",
226
340
  toolName: name,
227
341
  executionTarget,
228
342
  input,
@@ -231,24 +345,30 @@ export class PermissionChecker {
231
345
  conversationId: context.conversationId,
232
346
  requestId: context.requestId,
233
347
  riskLevel,
234
- decision: 'always_deny',
348
+ decision: "always_deny",
235
349
  reason: denialReason,
236
350
  durationMs,
237
351
  });
238
- return { allowed: false, decision, riskLevel, content: denialMessage };
352
+ return {
353
+ allowed: false,
354
+ decision,
355
+ riskLevel,
356
+ content: denialMessage,
357
+ };
239
358
  }
240
359
 
241
360
  if (
242
- persistentDecisionsAllowed
243
- && (response.decision === 'always_allow' || response.decision === 'always_allow_high_risk')
244
- && response.selectedPattern
361
+ persistentDecisionsAllowed &&
362
+ (response.decision === "always_allow" ||
363
+ response.decision === "always_allow_high_risk") &&
364
+ response.selectedPattern
245
365
  ) {
246
366
  const ruleOptions: {
247
367
  allowHighRisk?: boolean;
248
368
  executionTarget?: string;
249
369
  } = {};
250
370
 
251
- if (response.decision === 'always_allow_high_risk') {
371
+ if (response.decision === "always_allow_high_risk") {
252
372
  ruleOptions.allowHighRisk = true;
253
373
  }
254
374
 
@@ -259,19 +379,51 @@ export class PermissionChecker {
259
379
  const hasOptions = Object.keys(ruleOptions).length > 0;
260
380
  // Only default to 'everywhere' for non-scoped tools (empty scopeOptions).
261
381
  // For scoped tools, require an explicit scope to prevent silent permission widening.
262
- const effectiveScope = scopeOptions.length === 0
263
- ? (response.selectedScope ?? 'everywhere')
264
- : response.selectedScope;
382
+ const effectiveScope =
383
+ scopeOptions.length === 0
384
+ ? (response.selectedScope ?? "everywhere")
385
+ : response.selectedScope;
265
386
  if (effectiveScope) {
266
- addRule(name, response.selectedPattern, effectiveScope, 'allow', 100, hasOptions ? ruleOptions : undefined);
387
+ addRule(
388
+ name,
389
+ response.selectedPattern,
390
+ effectiveScope,
391
+ "allow",
392
+ 100,
393
+ hasOptions ? ruleOptions : undefined,
394
+ );
267
395
  }
268
396
  }
269
397
 
398
+ // Activate temporary approval mode when the user chooses a
399
+ // time-limited or thread-scoped override. Subsequent tool
400
+ // invocations in this conversation will auto-approve without
401
+ // prompting (checked above in the temporary override block).
402
+ // Gated on persistentDecisionsAllowed so that proxied bash
403
+ // commands (which require per-invocation approval) cannot
404
+ // escalate into blanket auto-approval.
405
+ if (persistentDecisionsAllowed && response.decision === "allow_10m") {
406
+ setTimedMode(context.conversationId);
407
+ log.info(
408
+ { toolName: name, conversationId: context.conversationId },
409
+ "Activated timed (10m) temporary approval mode",
410
+ );
411
+ } else if (
412
+ persistentDecisionsAllowed &&
413
+ response.decision === "allow_thread"
414
+ ) {
415
+ setThreadMode(context.conversationId);
416
+ log.info(
417
+ { toolName: name, conversationId: context.conversationId },
418
+ "Activated thread-scoped temporary approval mode",
419
+ );
420
+ }
421
+
270
422
  return { allowed: true, decision, riskLevel };
271
423
  }
272
424
 
273
425
  // result.decision === 'allow'
274
- return { allowed: true, decision: 'allow', riskLevel };
426
+ return { allowed: true, decision: "allow", riskLevel };
275
427
  } catch (err) {
276
428
  if (err instanceof Error) {
277
429
  (err as Error & { riskLevel?: string }).riskLevel = riskLevel;