@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
|
@@ -6,31 +6,50 @@
|
|
|
6
6
|
* keeping the constructor body focused on wiring.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { generateAllowlistOptions, generateScopeOptions, normalizeWebFetchUrl } from '../permissions/checker.js';
|
|
10
|
-
import type { PermissionPrompter } from '../permissions/prompter.js';
|
|
11
|
-
import type { SecretPrompter } from '../permissions/secret-prompter.js';
|
|
12
|
-
import { addRule, findHighestPriorityRule } from '../permissions/trust-store.js';
|
|
13
|
-
import type { Message, ToolDefinition } from '../providers/types.js';
|
|
14
|
-
import type { ToolExecutor } from '../tools/executor.js';
|
|
15
|
-
import type { ToolExecutionResult, ToolLifecycleEventHandler } from '../tools/types.js';
|
|
16
|
-
import { getLogger } from '../util/logger.js';
|
|
17
|
-
import { isDoordashCommand, markDoordashStepInProgress } from './doordash-steps.js';
|
|
18
|
-
import type { ServerMessage, UiSurfaceShow } from './ipc-protocol.js';
|
|
19
|
-
import { runPostExecutionSideEffects } from './tool-side-effects.js';
|
|
20
|
-
|
|
21
|
-
const log = getLogger('session-tool-setup');
|
|
22
|
-
import { coreAppProxyTools } from '../tools/apps/definitions.js';
|
|
23
|
-
import { registerSessionSender } from '../tools/browser/browser-screencast.js';
|
|
24
|
-
import { requestComputerControlTool } from '../tools/computer-use/request-computer-control.js';
|
|
25
|
-
import type { ProxyApprovalCallback, ProxyApprovalRequest } from '../tools/network/script-proxy/index.js';
|
|
26
|
-
import { getAllToolDefinitions } from '../tools/registry.js';
|
|
27
|
-
import { allUiSurfaceTools } from '../tools/ui-surface/definitions.js';
|
|
28
|
-
import type { GuardianRuntimeContext } from './session-runtime-assembly.js';
|
|
29
|
-
import { projectSkillTools, type SkillProjectionCache } from './session-skill-tools.js';
|
|
30
|
-
import type { SurfaceSessionContext } from './session-surfaces.js';
|
|
31
9
|
import {
|
|
32
|
-
|
|
33
|
-
|
|
10
|
+
generateAllowlistOptions,
|
|
11
|
+
generateScopeOptions,
|
|
12
|
+
normalizeWebFetchUrl,
|
|
13
|
+
} from "../permissions/checker.js";
|
|
14
|
+
import type { PermissionPrompter } from "../permissions/prompter.js";
|
|
15
|
+
import type { SecretPrompter } from "../permissions/secret-prompter.js";
|
|
16
|
+
import {
|
|
17
|
+
addRule,
|
|
18
|
+
findHighestPriorityRule,
|
|
19
|
+
} from "../permissions/trust-store.js";
|
|
20
|
+
import { isAllowDecision } from "../permissions/types.js";
|
|
21
|
+
import type { Message, ToolDefinition } from "../providers/types.js";
|
|
22
|
+
import { getEffectiveMode } from "../runtime/session-approval-overrides.js";
|
|
23
|
+
import type { ToolExecutor } from "../tools/executor.js";
|
|
24
|
+
import type {
|
|
25
|
+
ToolExecutionResult,
|
|
26
|
+
ToolLifecycleEventHandler,
|
|
27
|
+
} from "../tools/types.js";
|
|
28
|
+
import { getLogger } from "../util/logger.js";
|
|
29
|
+
import {
|
|
30
|
+
isDoordashCommand,
|
|
31
|
+
markDoordashStepInProgress,
|
|
32
|
+
} from "./doordash-steps.js";
|
|
33
|
+
import type { ServerMessage, UiSurfaceShow } from "./ipc-protocol.js";
|
|
34
|
+
import { runPostExecutionSideEffects } from "./tool-side-effects.js";
|
|
35
|
+
|
|
36
|
+
const log = getLogger("session-tool-setup");
|
|
37
|
+
import { coreAppProxyTools } from "../tools/apps/definitions.js";
|
|
38
|
+
import { registerSessionSender } from "../tools/browser/browser-screencast.js";
|
|
39
|
+
import { requestComputerControlTool } from "../tools/computer-use/request-computer-control.js";
|
|
40
|
+
import type {
|
|
41
|
+
ProxyApprovalCallback,
|
|
42
|
+
ProxyApprovalRequest,
|
|
43
|
+
} from "../tools/network/script-proxy/index.js";
|
|
44
|
+
import { getAllToolDefinitions } from "../tools/registry.js";
|
|
45
|
+
import { allUiSurfaceTools } from "../tools/ui-surface/definitions.js";
|
|
46
|
+
import type { GuardianRuntimeContext } from "./session-runtime-assembly.js";
|
|
47
|
+
import {
|
|
48
|
+
projectSkillTools,
|
|
49
|
+
type SkillProjectionCache,
|
|
50
|
+
} from "./session-skill-tools.js";
|
|
51
|
+
import type { SurfaceSessionContext } from "./session-surfaces.js";
|
|
52
|
+
import { surfaceProxyResolver } from "./session-surfaces.js";
|
|
34
53
|
|
|
35
54
|
// ── Context Interface ────────────────────────────────────────────────
|
|
36
55
|
|
|
@@ -94,11 +113,19 @@ export function createToolExecutor(
|
|
|
94
113
|
ctx: ToolSetupContext,
|
|
95
114
|
handleToolLifecycleEvent: ToolLifecycleEventHandler,
|
|
96
115
|
broadcastToAllClients?: (msg: ServerMessage) => void,
|
|
97
|
-
): (
|
|
116
|
+
): (
|
|
117
|
+
name: string,
|
|
118
|
+
input: Record<string, unknown>,
|
|
119
|
+
onOutput?: (chunk: string) => void,
|
|
120
|
+
) => Promise<ToolExecutionResult> {
|
|
98
121
|
// Register the session's sendToClient for browser screencast surface messages
|
|
99
122
|
registerSessionSender(ctx.conversationId, (msg) => ctx.sendToClient(msg));
|
|
100
123
|
|
|
101
|
-
return async (
|
|
124
|
+
return async (
|
|
125
|
+
name: string,
|
|
126
|
+
input: Record<string, unknown>,
|
|
127
|
+
onOutput?: (chunk: string) => void,
|
|
128
|
+
) => {
|
|
102
129
|
if (isDoordashCommand(name, input)) {
|
|
103
130
|
markDoordashStepInProgress(ctx, input);
|
|
104
131
|
}
|
|
@@ -110,10 +137,11 @@ export function createToolExecutor(
|
|
|
110
137
|
assistantId: ctx.assistantId,
|
|
111
138
|
requestId: ctx.currentRequestId,
|
|
112
139
|
taskRunId: ctx.taskRunId,
|
|
113
|
-
guardianTrustClass: ctx.guardianContext?.trustClass ??
|
|
140
|
+
guardianTrustClass: ctx.guardianContext?.trustClass ?? "guardian",
|
|
114
141
|
executionChannel: ctx.guardianContext?.sourceChannel,
|
|
115
142
|
callSessionId: ctx.callSessionId,
|
|
116
|
-
triggeredBySurfaceAction:
|
|
143
|
+
triggeredBySurfaceAction:
|
|
144
|
+
ctx.surfaceActionRequestIds?.has(ctx.currentRequestId ?? "") ?? false,
|
|
117
145
|
requesterExternalUserId: ctx.guardianContext?.requesterExternalUserId,
|
|
118
146
|
requesterChatId: ctx.guardianContext?.requesterChatId,
|
|
119
147
|
onOutput,
|
|
@@ -127,7 +155,7 @@ export function createToolExecutor(
|
|
|
127
155
|
// Tool context's sendToClient uses a loose { type: string; [key: string]: unknown }
|
|
128
156
|
// signature, but at runtime these are always ServerMessage instances.
|
|
129
157
|
ctx.sendToClient(msg as ServerMessage);
|
|
130
|
-
if (msg.type ===
|
|
158
|
+
if (msg.type === "ui_surface_show") {
|
|
131
159
|
const s = msg as unknown as UiSurfaceShow;
|
|
132
160
|
ctx.currentTurnSurfaces.push({
|
|
133
161
|
surfaceId: s.surfaceId,
|
|
@@ -140,31 +168,59 @@ export function createToolExecutor(
|
|
|
140
168
|
}
|
|
141
169
|
},
|
|
142
170
|
isInteractive: !ctx.hasNoClient && !ctx.headlessLock,
|
|
143
|
-
proxyToolResolver: (
|
|
171
|
+
proxyToolResolver: (
|
|
172
|
+
toolName: string,
|
|
173
|
+
proxyInput: Record<string, unknown>,
|
|
174
|
+
) => surfaceProxyResolver(ctx, toolName, proxyInput),
|
|
144
175
|
proxyApprovalCallback: createProxyApprovalCallback(prompter, ctx),
|
|
145
176
|
requestSecret: async (params) => {
|
|
146
177
|
return secretPrompter.prompt(
|
|
147
|
-
params.service,
|
|
148
|
-
params.
|
|
178
|
+
params.service,
|
|
179
|
+
params.field,
|
|
180
|
+
params.label,
|
|
181
|
+
params.description,
|
|
182
|
+
params.placeholder,
|
|
149
183
|
ctx.conversationId,
|
|
150
|
-
params.purpose,
|
|
184
|
+
params.purpose,
|
|
185
|
+
params.allowedTools,
|
|
186
|
+
params.allowedDomains,
|
|
151
187
|
);
|
|
152
188
|
},
|
|
153
189
|
requestConfirmation: async (req) => {
|
|
154
190
|
// Check trust store before prompting
|
|
155
191
|
const existingRule = findHighestPriorityRule(
|
|
156
|
-
|
|
157
|
-
[req.toolName, `cc:${req.toolName}`,
|
|
192
|
+
"cc:" + req.toolName,
|
|
193
|
+
[req.toolName, `cc:${req.toolName}`, "cc:*"],
|
|
158
194
|
ctx.workingDir,
|
|
159
195
|
);
|
|
160
|
-
if (existingRule && existingRule.decision !==
|
|
196
|
+
if (existingRule && existingRule.decision !== "ask") {
|
|
161
197
|
return {
|
|
162
|
-
decision:
|
|
198
|
+
decision:
|
|
199
|
+
existingRule.decision === "allow"
|
|
200
|
+
? ("allow" as const)
|
|
201
|
+
: ("deny" as const),
|
|
163
202
|
};
|
|
164
203
|
}
|
|
204
|
+
// Auto-approve sub-tool confirmations when a temporary approval
|
|
205
|
+
// override is active for this conversation (guardian only).
|
|
206
|
+
const guardianTrust = ctx.guardianContext?.trustClass ?? "guardian";
|
|
207
|
+
if (
|
|
208
|
+
guardianTrust === "guardian" &&
|
|
209
|
+
getEffectiveMode(ctx.conversationId) !== undefined
|
|
210
|
+
) {
|
|
211
|
+
return { decision: "allow" as const };
|
|
212
|
+
}
|
|
165
213
|
const allowlistOptions = [
|
|
166
|
-
{
|
|
167
|
-
|
|
214
|
+
{
|
|
215
|
+
label: `cc:${req.toolName}`,
|
|
216
|
+
description: `Claude Code ${req.toolName}`,
|
|
217
|
+
pattern: `cc:${req.toolName}`,
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
label: "cc:*",
|
|
221
|
+
description: "All Claude Code sub-tools",
|
|
222
|
+
pattern: "cc:*",
|
|
223
|
+
},
|
|
168
224
|
];
|
|
169
225
|
const scopeOptions = generateScopeOptions(ctx.workingDir);
|
|
170
226
|
const response = await prompter.prompt(
|
|
@@ -173,26 +229,69 @@ export function createToolExecutor(
|
|
|
173
229
|
req.riskLevel,
|
|
174
230
|
allowlistOptions,
|
|
175
231
|
scopeOptions,
|
|
176
|
-
undefined,
|
|
232
|
+
undefined,
|
|
233
|
+
undefined,
|
|
177
234
|
ctx.conversationId,
|
|
178
235
|
req.executionTarget,
|
|
179
236
|
);
|
|
180
|
-
if (
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
237
|
+
if (
|
|
238
|
+
(response.decision === "always_allow" ||
|
|
239
|
+
response.decision === "always_allow_high_risk") &&
|
|
240
|
+
response.selectedPattern &&
|
|
241
|
+
response.selectedScope
|
|
242
|
+
) {
|
|
243
|
+
log.info(
|
|
244
|
+
{
|
|
245
|
+
toolName: "cc:" + req.toolName,
|
|
246
|
+
pattern: response.selectedPattern,
|
|
247
|
+
scope: response.selectedScope,
|
|
248
|
+
highRisk: response.decision === "always_allow_high_risk",
|
|
249
|
+
},
|
|
250
|
+
"Persisting always-allow trust rule",
|
|
251
|
+
);
|
|
252
|
+
addRule(
|
|
253
|
+
"cc:" + req.toolName,
|
|
254
|
+
response.selectedPattern,
|
|
255
|
+
response.selectedScope,
|
|
256
|
+
"allow",
|
|
257
|
+
100,
|
|
258
|
+
response.decision === "always_allow_high_risk"
|
|
259
|
+
? { allowHighRisk: true }
|
|
260
|
+
: undefined,
|
|
261
|
+
);
|
|
184
262
|
}
|
|
185
|
-
if (
|
|
186
|
-
|
|
187
|
-
|
|
263
|
+
if (
|
|
264
|
+
response.decision === "always_deny" &&
|
|
265
|
+
response.selectedPattern &&
|
|
266
|
+
response.selectedScope
|
|
267
|
+
) {
|
|
268
|
+
log.info(
|
|
269
|
+
{
|
|
270
|
+
toolName: "cc:" + req.toolName,
|
|
271
|
+
pattern: response.selectedPattern,
|
|
272
|
+
scope: response.selectedScope,
|
|
273
|
+
},
|
|
274
|
+
"Persisting always-deny trust rule",
|
|
275
|
+
);
|
|
276
|
+
addRule(
|
|
277
|
+
"cc:" + req.toolName,
|
|
278
|
+
response.selectedPattern,
|
|
279
|
+
response.selectedScope,
|
|
280
|
+
"deny",
|
|
281
|
+
);
|
|
188
282
|
}
|
|
189
283
|
return {
|
|
190
|
-
decision: (response.decision
|
|
284
|
+
decision: isAllowDecision(response.decision)
|
|
285
|
+
? ("allow" as const)
|
|
286
|
+
: ("deny" as const),
|
|
191
287
|
};
|
|
192
288
|
},
|
|
193
289
|
});
|
|
194
290
|
|
|
195
|
-
runPostExecutionSideEffects(name, input, result, {
|
|
291
|
+
runPostExecutionSideEffects(name, input, result, {
|
|
292
|
+
ctx,
|
|
293
|
+
broadcastToAllClients,
|
|
294
|
+
});
|
|
196
295
|
|
|
197
296
|
return result;
|
|
198
297
|
};
|
|
@@ -216,19 +315,20 @@ export function createProxyApprovalCallback(
|
|
|
216
315
|
|
|
217
316
|
// Use the standard network_request tool name so trust rules align with
|
|
218
317
|
// the checker's URL-based candidate generation and allowlist options.
|
|
219
|
-
const toolName =
|
|
318
|
+
const toolName = "network_request";
|
|
220
319
|
const { scheme } = decision.target;
|
|
221
|
-
const url = `${scheme}://${hostname}${port ?
|
|
320
|
+
const url = `${scheme}://${hostname}${port ? ":" + port : ""}${path}`;
|
|
222
321
|
|
|
223
322
|
const input: Record<string, unknown> = {
|
|
224
323
|
url,
|
|
225
324
|
proxy_session_id: request.sessionId,
|
|
226
325
|
};
|
|
227
|
-
if (decision.kind ===
|
|
326
|
+
if (decision.kind === "ask_missing_credential") {
|
|
228
327
|
input.matching_patterns = decision.matchingPatterns;
|
|
229
328
|
}
|
|
230
329
|
|
|
231
|
-
const riskLevel =
|
|
330
|
+
const riskLevel =
|
|
331
|
+
decision.kind === "ask_missing_credential" ? "high" : "medium";
|
|
232
332
|
|
|
233
333
|
// Check trust store before prompting — build candidates that mirror
|
|
234
334
|
// buildCommandCandidates() in checker.ts for network_request.
|
|
@@ -242,16 +342,23 @@ export function createProxyApprovalCallback(
|
|
|
242
342
|
// Deduplicate
|
|
243
343
|
const uniqueCandidates = [...new Set(candidates)];
|
|
244
344
|
|
|
245
|
-
const existingRule = findHighestPriorityRule(
|
|
246
|
-
|
|
247
|
-
|
|
345
|
+
const existingRule = findHighestPriorityRule(
|
|
346
|
+
toolName,
|
|
347
|
+
uniqueCandidates,
|
|
348
|
+
ctx.workingDir,
|
|
349
|
+
);
|
|
350
|
+
if (existingRule && existingRule.decision !== "ask") {
|
|
351
|
+
if (existingRule.decision === "deny") return false;
|
|
248
352
|
// For high-risk proxy decisions, a plain allow rule (without allowHighRisk)
|
|
249
353
|
// must fall through to prompting — mirroring the checker's behavior.
|
|
250
|
-
if (riskLevel !==
|
|
354
|
+
if (riskLevel !== "high" || existingRule.allowHighRisk === true)
|
|
355
|
+
return true;
|
|
251
356
|
}
|
|
252
357
|
|
|
253
358
|
// Use the checker's built-in allowlist generation for network_request
|
|
254
|
-
const allowlistOptions = await generateAllowlistOptions(
|
|
359
|
+
const allowlistOptions = await generateAllowlistOptions("network_request", {
|
|
360
|
+
url,
|
|
361
|
+
});
|
|
255
362
|
|
|
256
363
|
const scopeOptions = generateScopeOptions(ctx.workingDir);
|
|
257
364
|
|
|
@@ -261,6 +368,11 @@ export function createProxyApprovalCallback(
|
|
|
261
368
|
return false;
|
|
262
369
|
}
|
|
263
370
|
|
|
371
|
+
// Proxied network requests require per-invocation approval and must
|
|
372
|
+
// not be auto-approved by temporary overrides (allow_10m / allow_thread).
|
|
373
|
+
// Unlike regular tool invocations, these represent outbound network
|
|
374
|
+
// actions that should always receive explicit confirmation.
|
|
375
|
+
|
|
264
376
|
const response = await prompter.prompt(
|
|
265
377
|
toolName,
|
|
266
378
|
input,
|
|
@@ -273,19 +385,54 @@ export function createProxyApprovalCallback(
|
|
|
273
385
|
);
|
|
274
386
|
|
|
275
387
|
// Persist trust rule if the user chose "always allow" or "always deny"
|
|
276
|
-
if (
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
388
|
+
if (
|
|
389
|
+
(response.decision === "always_allow" ||
|
|
390
|
+
response.decision === "always_allow_high_risk") &&
|
|
391
|
+
response.selectedPattern &&
|
|
392
|
+
response.selectedScope
|
|
393
|
+
) {
|
|
394
|
+
log.info(
|
|
395
|
+
{
|
|
396
|
+
toolName,
|
|
397
|
+
pattern: response.selectedPattern,
|
|
398
|
+
scope: response.selectedScope,
|
|
399
|
+
highRisk: response.decision === "always_allow_high_risk",
|
|
400
|
+
},
|
|
401
|
+
"Persisting always-allow trust rule (proxy)",
|
|
402
|
+
);
|
|
403
|
+
addRule(
|
|
404
|
+
toolName,
|
|
405
|
+
response.selectedPattern,
|
|
406
|
+
response.selectedScope,
|
|
407
|
+
"allow",
|
|
408
|
+
100,
|
|
409
|
+
response.decision === "always_allow_high_risk"
|
|
410
|
+
? { allowHighRisk: true }
|
|
411
|
+
: undefined,
|
|
412
|
+
);
|
|
280
413
|
}
|
|
281
|
-
if (
|
|
282
|
-
|
|
283
|
-
|
|
414
|
+
if (
|
|
415
|
+
response.decision === "always_deny" &&
|
|
416
|
+
response.selectedPattern &&
|
|
417
|
+
response.selectedScope
|
|
418
|
+
) {
|
|
419
|
+
log.info(
|
|
420
|
+
{
|
|
421
|
+
toolName,
|
|
422
|
+
pattern: response.selectedPattern,
|
|
423
|
+
scope: response.selectedScope,
|
|
424
|
+
},
|
|
425
|
+
"Persisting always-deny trust rule (proxy)",
|
|
426
|
+
);
|
|
427
|
+
addRule(
|
|
428
|
+
toolName,
|
|
429
|
+
response.selectedPattern,
|
|
430
|
+
response.selectedScope,
|
|
431
|
+
"deny",
|
|
432
|
+
);
|
|
284
433
|
}
|
|
285
434
|
|
|
286
|
-
return response.decision
|
|
287
|
-
|| response.decision === 'always_allow'
|
|
288
|
-
|| response.decision === 'always_allow_high_risk';
|
|
435
|
+
return isAllowDecision(response.decision);
|
|
289
436
|
};
|
|
290
437
|
}
|
|
291
438
|
|
|
@@ -296,7 +443,7 @@ export function createProxyApprovalCallback(
|
|
|
296
443
|
* history or explicit preactivation. Without this, their tools are
|
|
297
444
|
* unavailable in fresh sessions until `skill_load` is called.
|
|
298
445
|
*/
|
|
299
|
-
const DEFAULT_PREACTIVATED_SKILL_IDS = [
|
|
446
|
+
const DEFAULT_PREACTIVATED_SKILL_IDS = ["tasks", "notifications"];
|
|
300
447
|
|
|
301
448
|
/**
|
|
302
449
|
* Subset of Session state that the resolveTools callback reads at each
|
package/src/daemon/session.ts
CHANGED
|
@@ -34,6 +34,8 @@ import { SecretPrompter } from '../permissions/secret-prompter.js';
|
|
|
34
34
|
import type { UserDecision } from '../permissions/types.js';
|
|
35
35
|
import type { Message } from '../providers/types.js';
|
|
36
36
|
import type { Provider } from '../providers/types.js';
|
|
37
|
+
import type { AuthContext } from '../runtime/auth/types.js';
|
|
38
|
+
import * as approvalOverrides from '../runtime/session-approval-overrides.js';
|
|
37
39
|
import { ToolExecutor } from '../tools/executor.js';
|
|
38
40
|
import type { AssistantAttachmentDraft } from './assistant-attachments.js';
|
|
39
41
|
import type { AssistantActivityState, ConfirmationStateChanged } from './ipc-contract/messages.js';
|
|
@@ -142,6 +144,7 @@ export class Session {
|
|
|
142
144
|
/** @internal */ currentPage?: string;
|
|
143
145
|
/** @internal */ channelCapabilities?: ChannelCapabilities;
|
|
144
146
|
/** @internal */ guardianContext?: GuardianRuntimeContext;
|
|
147
|
+
/** @internal */ authContext?: AuthContext;
|
|
145
148
|
/** @internal */ loadedHistoryTrustClass?: GuardianRuntimeContext['trustClass'];
|
|
146
149
|
/** @internal */ voiceCallControlPrompt?: string;
|
|
147
150
|
/** @internal */ assistantId?: string;
|
|
@@ -428,6 +431,7 @@ export class Session {
|
|
|
428
431
|
}
|
|
429
432
|
|
|
430
433
|
dispose(): void {
|
|
434
|
+
approvalOverrides.clearMode(this.conversationId);
|
|
431
435
|
disposeSession(this);
|
|
432
436
|
}
|
|
433
437
|
|
|
@@ -499,6 +503,12 @@ export class Session {
|
|
|
499
503
|
decisionText?: string;
|
|
500
504
|
},
|
|
501
505
|
): void {
|
|
506
|
+
// Guard: only proceed if the confirmation is still pending. Stale or
|
|
507
|
+
// already-resolved requests must not activate overrides or emit events.
|
|
508
|
+
if (!this.prompter.hasPendingRequest(requestId)) {
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
|
|
502
512
|
this.prompter.resolveConfirmation(
|
|
503
513
|
requestId,
|
|
504
514
|
decision,
|
|
@@ -507,6 +517,11 @@ export class Session {
|
|
|
507
517
|
decisionContext,
|
|
508
518
|
);
|
|
509
519
|
|
|
520
|
+
// Mode activation (setTimedMode / setThreadMode) is intentionally NOT
|
|
521
|
+
// done here. It is handled in permission-checker.ts where
|
|
522
|
+
// persistentDecisionsAllowed context is available — this prevents
|
|
523
|
+
// proxied bash commands from escalating into blanket auto-approval.
|
|
524
|
+
|
|
510
525
|
// Emit authoritative confirmation state and activity transition centrally
|
|
511
526
|
// so ALL callers (IPC handlers, /v1/confirm, channel bridges) get
|
|
512
527
|
// consistent events without duplicating emission logic.
|
|
@@ -566,6 +581,14 @@ export class Session {
|
|
|
566
581
|
this.guardianContext = ctx ?? undefined;
|
|
567
582
|
}
|
|
568
583
|
|
|
584
|
+
setAuthContext(ctx: AuthContext | null): void {
|
|
585
|
+
this.authContext = ctx ?? undefined;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
getAuthContext(): AuthContext | undefined {
|
|
589
|
+
return this.authContext;
|
|
590
|
+
}
|
|
591
|
+
|
|
569
592
|
setVoiceCallControlPrompt(prompt: string | null): void {
|
|
570
593
|
this.voiceCallControlPrompt = prompt ?? undefined;
|
|
571
594
|
}
|
|
@@ -617,7 +640,7 @@ export class Session {
|
|
|
617
640
|
content: string,
|
|
618
641
|
userMessageId: string,
|
|
619
642
|
onEvent: (msg: ServerMessage) => void,
|
|
620
|
-
options?: { skipPreMessageRollback?: boolean; isInteractive?: boolean; titleText?: string },
|
|
643
|
+
options?: { skipPreMessageRollback?: boolean; isInteractive?: boolean; isUserMessage?: boolean; titleText?: string },
|
|
621
644
|
): Promise<void> {
|
|
622
645
|
return runAgentLoopImpl(this, content, userMessageId, onEvent, options);
|
|
623
646
|
}
|
|
@@ -20,7 +20,7 @@ export interface ToolDomainEvents {
|
|
|
20
20
|
sessionId: string;
|
|
21
21
|
requestId?: string;
|
|
22
22
|
toolName: string;
|
|
23
|
-
decision: 'allow' | 'always_allow' | 'deny' | 'always_deny';
|
|
23
|
+
decision: 'allow' | 'allow_10m' | 'allow_thread' | 'always_allow' | 'deny' | 'always_deny';
|
|
24
24
|
riskLevel: string;
|
|
25
25
|
decidedAtMs: number;
|
|
26
26
|
};
|
|
@@ -1,13 +1,8 @@
|
|
|
1
|
+
import { isAllowDecision, type UserDecision } from '../permissions/types.js';
|
|
1
2
|
import type { ToolLifecycleEventHandler } from '../tools/types.js';
|
|
2
3
|
import type { EventBus } from './bus.js';
|
|
3
4
|
import type { AssistantDomainEvents } from './domain-events.js';
|
|
4
5
|
|
|
5
|
-
const allowDecisions = new Set(['allow', 'always_allow']);
|
|
6
|
-
|
|
7
|
-
function isAllowDecision(decision: string): decision is 'allow' | 'always_allow' {
|
|
8
|
-
return allowDecisions.has(decision);
|
|
9
|
-
}
|
|
10
|
-
|
|
11
6
|
export function createToolDomainEventPublisher(
|
|
12
7
|
eventBus: EventBus<AssistantDomainEvents>,
|
|
13
8
|
): ToolLifecycleEventHandler {
|
|
@@ -45,13 +40,13 @@ export function createToolDomainEventPublisher(
|
|
|
45
40
|
});
|
|
46
41
|
break;
|
|
47
42
|
case 'executed':
|
|
48
|
-
if (isAllowDecision(event.decision)) {
|
|
43
|
+
if (isAllowDecision(event.decision as UserDecision)) {
|
|
49
44
|
await eventBus.emit('tool.permission.decided', {
|
|
50
45
|
conversationId: event.conversationId,
|
|
51
46
|
sessionId: event.sessionId,
|
|
52
47
|
requestId: event.requestId,
|
|
53
48
|
toolName: event.toolName,
|
|
54
|
-
decision: event.decision,
|
|
49
|
+
decision: event.decision as AssistantDomainEvents['tool.permission.decided']['decision'],
|
|
55
50
|
riskLevel: event.riskLevel,
|
|
56
51
|
decidedAtMs: Date.now(),
|
|
57
52
|
});
|
|
@@ -69,13 +64,13 @@ export function createToolDomainEventPublisher(
|
|
|
69
64
|
});
|
|
70
65
|
break;
|
|
71
66
|
case 'error':
|
|
72
|
-
if (isAllowDecision(event.decision)) {
|
|
67
|
+
if (isAllowDecision(event.decision as UserDecision)) {
|
|
73
68
|
await eventBus.emit('tool.permission.decided', {
|
|
74
69
|
conversationId: event.conversationId,
|
|
75
70
|
sessionId: event.sessionId,
|
|
76
71
|
requestId: event.requestId,
|
|
77
72
|
toolName: event.toolName,
|
|
78
|
-
decision: event.decision,
|
|
73
|
+
decision: event.decision as AssistantDomainEvents['tool.permission.decided']['decision'],
|
|
79
74
|
riskLevel: event.riskLevel,
|
|
80
75
|
decidedAtMs: Date.now(),
|
|
81
76
|
});
|
package/src/influencer/client.ts
CHANGED
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
import type { ExtensionCommand, ExtensionResponse } from '../browser-extension-relay/protocol.js';
|
|
49
49
|
import { extensionRelayServer } from '../browser-extension-relay/server.js';
|
|
50
50
|
import { getGatewayInternalBaseUrl } from '../config/env.js';
|
|
51
|
-
import {
|
|
51
|
+
import { isSigningKeyInitialized, mintEdgeRelayToken } from '../runtime/auth/token-service.js';
|
|
52
52
|
|
|
53
53
|
// ---------------------------------------------------------------------------
|
|
54
54
|
// Types
|
|
@@ -126,13 +126,14 @@ async function sendRelayCommand(command: Record<string, unknown>): Promise<Exten
|
|
|
126
126
|
return extensionRelayServer.sendCommand(command as Omit<ExtensionCommand, 'id'>);
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
// Fall back to HTTP relay endpoint
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
);
|
|
129
|
+
// Fall back to HTTP relay endpoint via the gateway.
|
|
130
|
+
// The gateway validates edge JWTs (aud=vellum-gateway) and mints an
|
|
131
|
+
// exchange token for the runtime. Without the signing key (CLI
|
|
132
|
+
// out-of-process), we cannot mint JWTs at all.
|
|
133
|
+
if (!isSigningKeyInitialized()) {
|
|
134
|
+
throw new Error('Auth signing key not initialized — browser-relay commands require the daemon to be running');
|
|
135
135
|
}
|
|
136
|
+
const token = mintEdgeRelayToken();
|
|
136
137
|
|
|
137
138
|
const resp = await fetch(`${getGatewayInternalBaseUrl()}/v1/browser-relay/command`, {
|
|
138
139
|
method: 'POST',
|
|
@@ -193,12 +193,17 @@ export async function createDraft(
|
|
|
193
193
|
subject: string,
|
|
194
194
|
body: string,
|
|
195
195
|
inReplyTo?: string,
|
|
196
|
+
cc?: string,
|
|
197
|
+
bcc?: string,
|
|
198
|
+
threadId?: string,
|
|
196
199
|
): Promise<GmailDraft> {
|
|
197
200
|
const headers = [
|
|
198
201
|
`To: ${to}`,
|
|
199
202
|
`Subject: ${subject}`,
|
|
200
203
|
'Content-Type: text/plain; charset=utf-8',
|
|
201
204
|
];
|
|
205
|
+
if (cc) headers.push(`Cc: ${cc}`);
|
|
206
|
+
if (bcc) headers.push(`Bcc: ${bcc}`);
|
|
202
207
|
if (inReplyTo) {
|
|
203
208
|
headers.push(`In-Reply-To: ${inReplyTo}`);
|
|
204
209
|
headers.push(`References: ${inReplyTo}`);
|
|
@@ -208,9 +213,36 @@ export async function createDraft(
|
|
|
208
213
|
.replace(/\+/g, '-')
|
|
209
214
|
.replace(/\//g, '_')
|
|
210
215
|
.replace(/=+$/, '');
|
|
216
|
+
const message: Record<string, unknown> = { raw };
|
|
217
|
+
if (threadId) message.threadId = threadId;
|
|
218
|
+
return request<GmailDraft>(token, '/drafts', {
|
|
219
|
+
method: 'POST',
|
|
220
|
+
body: JSON.stringify({ message }),
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Create a draft from a pre-built base64url MIME payload. */
|
|
225
|
+
export async function createDraftRaw(
|
|
226
|
+
token: string,
|
|
227
|
+
raw: string,
|
|
228
|
+
threadId?: string,
|
|
229
|
+
): Promise<GmailDraft> {
|
|
230
|
+
const message: Record<string, unknown> = { raw };
|
|
231
|
+
if (threadId) message.threadId = threadId;
|
|
211
232
|
return request<GmailDraft>(token, '/drafts', {
|
|
212
233
|
method: 'POST',
|
|
213
|
-
body: JSON.stringify({ message
|
|
234
|
+
body: JSON.stringify({ message }),
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** Send an existing draft by ID. */
|
|
239
|
+
export async function sendDraft(
|
|
240
|
+
token: string,
|
|
241
|
+
draftId: string,
|
|
242
|
+
): Promise<GmailMessage> {
|
|
243
|
+
return request<GmailMessage>(token, '/drafts/send', {
|
|
244
|
+
method: 'POST',
|
|
245
|
+
body: JSON.stringify({ id: draftId }),
|
|
214
246
|
});
|
|
215
247
|
}
|
|
216
248
|
|
|
@@ -16,6 +16,8 @@ export interface MimeMessageOptions {
|
|
|
16
16
|
subject: string;
|
|
17
17
|
body: string;
|
|
18
18
|
inReplyTo?: string;
|
|
19
|
+
cc?: string;
|
|
20
|
+
bcc?: string;
|
|
19
21
|
attachments: MimeAttachment[];
|
|
20
22
|
}
|
|
21
23
|
|
|
@@ -32,7 +34,7 @@ function toBase64Url(input: Buffer): string {
|
|
|
32
34
|
* Returns a base64url-encoded string ready for Gmail's messages.send `raw` field.
|
|
33
35
|
*/
|
|
34
36
|
export function buildMultipartMime(options: MimeMessageOptions): string {
|
|
35
|
-
const { to, subject, body, inReplyTo, attachments } = options;
|
|
37
|
+
const { to, subject, body, inReplyTo, cc, bcc, attachments } = options;
|
|
36
38
|
const boundary = `----=_Part_${randomBytes(16).toString('hex')}`;
|
|
37
39
|
|
|
38
40
|
const headers = [
|
|
@@ -41,6 +43,8 @@ export function buildMultipartMime(options: MimeMessageOptions): string {
|
|
|
41
43
|
'MIME-Version: 1.0',
|
|
42
44
|
`Content-Type: multipart/mixed; boundary="${boundary}"`,
|
|
43
45
|
];
|
|
46
|
+
if (cc) headers.push(`Cc: ${cc}`);
|
|
47
|
+
if (bcc) headers.push(`Bcc: ${bcc}`);
|
|
44
48
|
if (inReplyTo) {
|
|
45
49
|
headers.push(`In-Reply-To: ${inReplyTo}`);
|
|
46
50
|
headers.push(`References: ${inReplyTo}`);
|