@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.
- package/ARCHITECTURE.md +77 -38
- package/README.md +10 -12
- package/package.json +1 -1
- package/src/__tests__/actor-token-service.test.ts +108 -522
- package/src/__tests__/channel-approval-routes.test.ts +92 -239
- package/src/__tests__/channel-approval.test.ts +100 -0
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +13 -6
- package/src/__tests__/conversation-routes.test.ts +11 -4
- package/src/__tests__/guardian-actions-endpoint.test.ts +26 -19
- package/src/__tests__/mcp-health-check.test.ts +65 -0
- package/src/__tests__/permission-types.test.ts +33 -0
- package/src/__tests__/scan-result-store.test.ts +121 -0
- package/src/__tests__/session-agent-loop.test.ts +120 -0
- package/src/__tests__/session-approval-overrides.test.ts +205 -0
- package/src/__tests__/session-surfaces-task-progress.test.ts +38 -0
- package/src/amazon/client.ts +8 -5
- package/src/approvals/guardian-decision-primitive.ts +14 -9
- package/src/approvals/guardian-request-resolvers.ts +2 -2
- package/src/calls/call-controller.ts +2 -2
- package/src/calls/twilio-routes.ts +2 -2
- package/src/cli/mcp.ts +3 -3
- package/src/cli.ts +24 -0
- package/src/config/bundled-skills/chatgpt-import/tools/chatgpt-import.ts +19 -130
- package/src/config/bundled-skills/doordash/__tests__/doordash-client.test.ts +8 -6
- package/src/config/bundled-skills/google-calendar/SKILL.md +1 -1
- package/src/config/bundled-skills/messaging/SKILL.md +49 -14
- package/src/config/bundled-skills/messaging/TOOLS.json +52 -9
- package/src/config/bundled-skills/messaging/tools/gmail-batch-archive.ts +35 -11
- package/src/config/bundled-skills/messaging/tools/gmail-draft.ts +3 -1
- package/src/config/bundled-skills/messaging/tools/gmail-forward.ts +5 -6
- package/src/config/bundled-skills/messaging/tools/gmail-outreach-scan.ts +10 -2
- package/src/config/bundled-skills/messaging/tools/gmail-send-draft.ts +20 -0
- package/src/config/bundled-skills/messaging/tools/gmail-send-with-attachments.ts +3 -4
- package/src/config/bundled-skills/messaging/tools/gmail-sender-digest.ts +16 -8
- package/src/config/bundled-skills/messaging/tools/messaging-reply.ts +76 -0
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +10 -0
- package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +11 -3
- package/src/config/bundled-skills/messaging/tools/scan-result-store.ts +86 -0
- package/src/config/bundled-skills/phone-calls/SKILL.md +2 -2
- package/src/config/bundled-skills/skills-catalog/SKILL.md +31 -8
- package/src/config/bundled-skills/slack/tools/slack-add-reaction.ts +1 -1
- package/src/config/bundled-skills/slack/tools/slack-channel-details.ts +1 -1
- package/src/config/bundled-skills/slack/tools/slack-delete-message.ts +1 -1
- package/src/config/bundled-skills/slack/tools/slack-leave-channel.ts +1 -1
- package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +79 -24
- package/src/config/bundled-skills/sms-setup/SKILL.md +1 -1
- package/src/config/bundled-skills/telegram-setup/SKILL.md +1 -1
- package/src/config/bundled-skills/twilio-setup/SKILL.md +1 -1
- package/src/daemon/approval-generators.ts +6 -3
- package/src/daemon/handlers/config-ingress.ts +2 -6
- package/src/daemon/handlers/guardian-actions.ts +1 -1
- package/src/daemon/handlers/sessions.ts +4 -1
- package/src/daemon/handlers/shared.ts +3 -0
- package/src/daemon/handlers/skills.ts +32 -0
- package/src/daemon/ipc-contract/messages.ts +3 -1
- package/src/daemon/ipc-handler.ts +24 -0
- package/src/daemon/ipc-validate.ts +1 -1
- package/src/daemon/lifecycle.ts +6 -8
- package/src/daemon/server.ts +8 -3
- package/src/daemon/session-agent-loop.ts +19 -1
- package/src/daemon/session-attachments.ts +2 -1
- package/src/daemon/session-history.ts +2 -2
- package/src/daemon/session-process.ts +5 -9
- package/src/daemon/session-surfaces.ts +17 -1
- package/src/daemon/session-tool-setup.ts +216 -69
- package/src/daemon/session.ts +24 -1
- package/src/events/domain-events.ts +1 -1
- package/src/events/tool-domain-event-publisher.ts +5 -10
- package/src/influencer/client.ts +8 -7
- package/src/messaging/providers/gmail/client.ts +33 -1
- package/src/messaging/providers/gmail/mime-builder.ts +5 -1
- package/src/messaging/providers/sms/adapter.ts +3 -7
- package/src/messaging/providers/telegram-bot/adapter.ts +3 -7
- package/src/messaging/providers/whatsapp/adapter.ts +3 -7
- package/src/notifications/adapters/sms.ts +2 -2
- package/src/notifications/adapters/telegram.ts +2 -2
- package/src/permissions/prompter.ts +2 -0
- package/src/permissions/types.ts +11 -1
- package/src/runtime/approval-conversation-turn.ts +4 -0
- package/src/runtime/auth/__tests__/context.test.ts +130 -0
- package/src/runtime/auth/__tests__/credential-service.test.ts +277 -0
- package/src/runtime/auth/__tests__/guard-tests.test.ts +289 -0
- package/src/runtime/auth/__tests__/ipc-auth-context.test.ts +71 -0
- package/src/runtime/auth/__tests__/middleware.test.ts +239 -0
- package/src/runtime/auth/__tests__/policy.test.ts +29 -0
- package/src/runtime/auth/__tests__/route-policy.test.ts +166 -0
- package/src/runtime/auth/__tests__/scopes.test.ts +109 -0
- package/src/runtime/auth/__tests__/subject.test.ts +149 -0
- package/src/runtime/auth/__tests__/token-service.test.ts +263 -0
- package/src/runtime/auth/context.ts +62 -0
- package/src/runtime/{actor-refresh-token-service.ts → auth/credential-service.ts} +112 -79
- package/src/runtime/auth/external-assistant-id.ts +69 -0
- package/src/runtime/auth/index.ts +37 -0
- package/src/runtime/auth/middleware.ts +127 -0
- package/src/runtime/auth/policy.ts +17 -0
- package/src/runtime/auth/route-policy.ts +261 -0
- package/src/runtime/auth/scopes.ts +64 -0
- package/src/runtime/auth/subject.ts +68 -0
- package/src/runtime/auth/token-service.ts +275 -0
- package/src/runtime/auth/types.ts +79 -0
- package/src/runtime/channel-approval-parser.ts +11 -5
- package/src/runtime/channel-approval-types.ts +1 -1
- package/src/runtime/channel-approvals.ts +22 -1
- package/src/runtime/guardian-action-followup-executor.ts +2 -2
- package/src/runtime/guardian-context-resolver.ts +15 -0
- package/src/runtime/guardian-decision-types.ts +23 -6
- package/src/runtime/guardian-outbound-actions.ts +4 -22
- package/src/runtime/guardian-reply-router.ts +5 -3
- package/src/runtime/http-server.ts +210 -182
- package/src/runtime/http-types.ts +11 -1
- package/src/runtime/local-actor-identity.ts +25 -0
- package/src/runtime/pending-interactions.ts +1 -0
- package/src/runtime/routes/approval-routes.ts +42 -59
- package/src/runtime/routes/channel-route-shared.ts +9 -41
- package/src/runtime/routes/channel-routes.ts +0 -2
- package/src/runtime/routes/conversation-routes.ts +39 -49
- package/src/runtime/routes/events-routes.ts +15 -22
- package/src/runtime/routes/guardian-action-routes.ts +46 -51
- package/src/runtime/routes/guardian-approval-interception.ts +6 -5
- package/src/runtime/routes/guardian-bootstrap-routes.ts +12 -8
- package/src/runtime/routes/guardian-refresh-routes.ts +2 -2
- package/src/runtime/routes/inbound-message-handler.ts +39 -45
- package/src/runtime/routes/pairing-routes.ts +9 -9
- package/src/runtime/routes/secret-routes.ts +90 -45
- package/src/runtime/routes/surface-action-routes.ts +12 -2
- package/src/runtime/routes/trust-rules-routes.ts +13 -0
- package/src/runtime/routes/twilio-routes.ts +3 -3
- package/src/runtime/session-approval-overrides.ts +86 -0
- package/src/security/keychain-to-encrypted-migration.ts +8 -1
- package/src/skills/frontmatter.ts +44 -1
- package/src/tools/permission-checker.ts +226 -74
- package/src/runtime/actor-token-service.ts +0 -234
- package/src/runtime/middleware/actor-token.ts +0 -265
|
@@ -1,16 +1,26 @@
|
|
|
1
|
-
import { getConfig } from
|
|
2
|
-
import { getHookManager } from
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
import {
|
|
10
|
-
import
|
|
11
|
-
import
|
|
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(
|
|
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: (
|
|
49
|
+
sanitizeToolInput: (
|
|
50
|
+
toolName: string,
|
|
51
|
+
input: Record<string, unknown>,
|
|
52
|
+
) => Record<string, unknown>,
|
|
40
53
|
startTime: number,
|
|
41
|
-
computePreviewDiff: (
|
|
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(
|
|
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(
|
|
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
|
-
|
|
60
|
-
|
|
96
|
+
context.forcePromptSideEffects &&
|
|
97
|
+
result.decision === "allow" &&
|
|
98
|
+
isSideEffectTool(name, input)
|
|
61
99
|
) {
|
|
62
|
-
result.decision =
|
|
63
|
-
result.reason =
|
|
100
|
+
result.decision = "prompt";
|
|
101
|
+
result.reason =
|
|
102
|
+
"Private thread: side-effect tools require explicit approval";
|
|
64
103
|
}
|
|
65
104
|
|
|
66
|
-
if (result.decision ===
|
|
105
|
+
if (result.decision === "deny") {
|
|
67
106
|
const durationMs = Date.now() - startTime;
|
|
68
107
|
emitLifecycleEvent({
|
|
69
|
-
type:
|
|
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:
|
|
117
|
+
decision: "deny",
|
|
79
118
|
reason: result.reason,
|
|
80
119
|
durationMs,
|
|
81
120
|
});
|
|
82
|
-
return {
|
|
121
|
+
return {
|
|
122
|
+
allowed: false,
|
|
123
|
+
decision: "denied",
|
|
124
|
+
riskLevel,
|
|
125
|
+
content: result.reason,
|
|
126
|
+
};
|
|
83
127
|
}
|
|
84
128
|
|
|
85
|
-
if (result.decision ===
|
|
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(
|
|
134
|
+
log.info(
|
|
135
|
+
{ toolName: name, riskLevel },
|
|
136
|
+
"Auto-denying prompt for non-interactive session",
|
|
137
|
+
);
|
|
91
138
|
emitLifecycleEvent({
|
|
92
|
-
type:
|
|
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:
|
|
102
|
-
reason:
|
|
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:
|
|
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
|
-
|
|
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 ===
|
|
196
|
+
if (name === "bash" && typeof input.command === "string") {
|
|
119
197
|
const cfg = getConfig();
|
|
120
|
-
const sandboxConfig =
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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 ===
|
|
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:
|
|
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(
|
|
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(
|
|
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 ===
|
|
182
|
-
const contextualDenial =
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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:
|
|
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:
|
|
299
|
+
decision: "deny",
|
|
203
300
|
reason: denialReason,
|
|
204
301
|
durationMs,
|
|
205
302
|
});
|
|
206
|
-
return {
|
|
303
|
+
return {
|
|
304
|
+
allowed: false,
|
|
305
|
+
decision,
|
|
306
|
+
riskLevel,
|
|
307
|
+
content: denialMessage,
|
|
308
|
+
};
|
|
207
309
|
}
|
|
208
310
|
|
|
209
|
-
if (response.decision ===
|
|
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 =
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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(
|
|
324
|
+
addRule(
|
|
325
|
+
name,
|
|
326
|
+
response.selectedPattern!,
|
|
327
|
+
effectiveDenyScope!,
|
|
328
|
+
"deny",
|
|
329
|
+
);
|
|
218
330
|
}
|
|
219
|
-
const denialReason = ruleSaved
|
|
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:
|
|
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:
|
|
348
|
+
decision: "always_deny",
|
|
235
349
|
reason: denialReason,
|
|
236
350
|
durationMs,
|
|
237
351
|
});
|
|
238
|
-
return {
|
|
352
|
+
return {
|
|
353
|
+
allowed: false,
|
|
354
|
+
decision,
|
|
355
|
+
riskLevel,
|
|
356
|
+
content: denialMessage,
|
|
357
|
+
};
|
|
239
358
|
}
|
|
240
359
|
|
|
241
360
|
if (
|
|
242
|
-
persistentDecisionsAllowed
|
|
243
|
-
|
|
244
|
-
|
|
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 ===
|
|
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 =
|
|
263
|
-
|
|
264
|
-
|
|
382
|
+
const effectiveScope =
|
|
383
|
+
scopeOptions.length === 0
|
|
384
|
+
? (response.selectedScope ?? "everywhere")
|
|
385
|
+
: response.selectedScope;
|
|
265
386
|
if (effectiveScope) {
|
|
266
|
-
addRule(
|
|
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:
|
|
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;
|