@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
|
@@ -156,7 +156,7 @@ Now link the user's phone number as the trusted SMS guardian. Tell the user: "No
|
|
|
156
156
|
|
|
157
157
|
Load the **guardian-verify-setup** skill to handle the verification flow:
|
|
158
158
|
|
|
159
|
-
- Call `skill_load` with `
|
|
159
|
+
- Call `skill_load` with `skill: "guardian-verify-setup"` to load the dependency skill.
|
|
160
160
|
|
|
161
161
|
When invoking the skill, indicate the channel is `sms`. The guardian-verify-setup skill manages the full outbound verification flow, including:
|
|
162
162
|
|
|
@@ -69,7 +69,7 @@ Now link the user's Telegram account as the trusted guardian for this bot. Tell
|
|
|
69
69
|
|
|
70
70
|
Load the **guardian-verify-setup** skill to handle the verification flow:
|
|
71
71
|
|
|
72
|
-
- Call `skill_load` with `
|
|
72
|
+
- Call `skill_load` with `skill: "guardian-verify-setup"` to load the dependency skill.
|
|
73
73
|
|
|
74
74
|
The guardian-verify-setup skill manages the full outbound verification flow for Telegram, including:
|
|
75
75
|
|
|
@@ -229,7 +229,7 @@ Now link the user's phone number as the trusted guardian for SMS and/or voice ch
|
|
|
229
229
|
|
|
230
230
|
Load the **guardian-verify-setup** skill to handle the verification flow:
|
|
231
231
|
|
|
232
|
-
- Call `skill_load` with `
|
|
232
|
+
- Call `skill_load` with `skill: "guardian-verify-setup"` to load the dependency skill.
|
|
233
233
|
|
|
234
234
|
The guardian-verify-setup skill manages the full outbound verification flow for **one channel at a time** (sms, voice, or telegram). Each invocation handles:
|
|
235
235
|
|
|
@@ -37,11 +37,12 @@ const APPROVAL_CONVERSATION_TOOL_SCHEMA = {
|
|
|
37
37
|
properties: {
|
|
38
38
|
disposition: {
|
|
39
39
|
type: 'string',
|
|
40
|
-
enum: ['keep_pending', 'approve_once', 'approve_always', 'reject'],
|
|
40
|
+
enum: ['keep_pending', 'approve_once', 'approve_10m', 'approve_thread', 'approve_always', 'reject'],
|
|
41
41
|
description:
|
|
42
42
|
'The decision: keep_pending if the user is asking questions or unclear, '
|
|
43
|
-
+ 'approve_once to approve this single request,
|
|
44
|
-
+ '
|
|
43
|
+
+ 'approve_once to approve this single request, approve_10m to approve all '
|
|
44
|
+
+ 'requests for 10 minutes, approve_thread to approve all requests in this '
|
|
45
|
+
+ 'thread, approve_always to approve this tool permanently, reject to deny the request.',
|
|
45
46
|
},
|
|
46
47
|
replyText: {
|
|
47
48
|
type: 'string',
|
|
@@ -61,6 +62,8 @@ const APPROVAL_CONVERSATION_TOOL_SCHEMA = {
|
|
|
61
62
|
const VALID_DISPOSITIONS: ReadonlySet<string> = new Set([
|
|
62
63
|
'keep_pending',
|
|
63
64
|
'approve_once',
|
|
65
|
+
'approve_10m',
|
|
66
|
+
'approve_thread',
|
|
64
67
|
'approve_always',
|
|
65
68
|
'reject',
|
|
66
69
|
]);
|
|
@@ -17,8 +17,8 @@ import {
|
|
|
17
17
|
getTwilioVoiceWebhookUrl,
|
|
18
18
|
type IngressConfig,
|
|
19
19
|
} from '../../inbound/public-ingress-urls.js';
|
|
20
|
+
import { mintDaemonDeliveryToken } from '../../runtime/auth/token-service.js';
|
|
20
21
|
import { getSecureKey } from '../../security/secure-keys.js';
|
|
21
|
-
import { readHttpToken } from '../../util/platform.js';
|
|
22
22
|
import type { IngressConfigRequest } from '../ipc-protocol.js';
|
|
23
23
|
import { CONFIG_RELOAD_DEBOUNCE_MS, defineHandlers, type HandlerContext,log } from './shared.js';
|
|
24
24
|
|
|
@@ -47,11 +47,7 @@ export function computeGatewayTarget(): string {
|
|
|
47
47
|
*/
|
|
48
48
|
export function triggerGatewayReconcile(ingressPublicBaseUrl: string | undefined): void {
|
|
49
49
|
const gatewayBase = computeGatewayTarget();
|
|
50
|
-
const token =
|
|
51
|
-
if (!token) {
|
|
52
|
-
log.debug('Skipping gateway reconcile trigger: no HTTP bearer token available');
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
50
|
+
const token = mintDaemonDeliveryToken();
|
|
55
51
|
|
|
56
52
|
const url = `${gatewayBase}/internal/telegram/reconcile`;
|
|
57
53
|
const body = JSON.stringify({ ingressPublicBaseUrl: ingressPublicBaseUrl ?? '' });
|
|
@@ -8,7 +8,7 @@ import { listGuardianDecisionPrompts } from '../../runtime/routes/guardian-actio
|
|
|
8
8
|
import type { GuardianActionDecision, GuardianActionsPendingRequest } from '../ipc-protocol.js';
|
|
9
9
|
import { defineHandlers, log } from './shared.js';
|
|
10
10
|
|
|
11
|
-
const VALID_ACTIONS = new Set<string>(['approve_once', 'approve_always', 'reject']);
|
|
11
|
+
const VALID_ACTIONS = new Set<string>(['approve_once', 'approve_10m', 'approve_thread', 'approve_always', 'reject']);
|
|
12
12
|
|
|
13
13
|
export const guardianActionsHandlers = defineHandlers({
|
|
14
14
|
guardian_actions_pending_request: (msg: GuardianActionsPendingRequest, socket, ctx) => {
|
|
@@ -19,7 +19,7 @@ import { GENERATING_TITLE, queueGenerateConversationTitle, UNTITLED_FALLBACK } f
|
|
|
19
19
|
import * as externalConversationStore from '../../memory/external-conversation-store.js';
|
|
20
20
|
import { DAEMON_INTERNAL_ASSISTANT_ID } from '../../runtime/assistant-scope.js';
|
|
21
21
|
import { routeGuardianReply } from '../../runtime/guardian-reply-router.js';
|
|
22
|
-
import { resolveLocalIpcGuardianContext } from '../../runtime/local-actor-identity.js';
|
|
22
|
+
import { resolveLocalIpcAuthContext, resolveLocalIpcGuardianContext } from '../../runtime/local-actor-identity.js';
|
|
23
23
|
import * as pendingInteractions from '../../runtime/pending-interactions.js';
|
|
24
24
|
import { checkIngressForSecrets } from '../../security/secret-ingress.js';
|
|
25
25
|
import { compileCustomPatterns, redactSecrets } from '../../security/secret-scanner.js';
|
|
@@ -119,6 +119,7 @@ function makeIpcEventSender(params: {
|
|
|
119
119
|
allowlistOptions: event.allowlistOptions,
|
|
120
120
|
scopeOptions: event.scopeOptions,
|
|
121
121
|
persistentDecisionsAllowed: event.persistentDecisionsAllowed,
|
|
122
|
+
temporaryOptionsAvailable: event.temporaryOptionsAvailable,
|
|
122
123
|
},
|
|
123
124
|
});
|
|
124
125
|
|
|
@@ -286,6 +287,8 @@ export async function handleUserMessage(
|
|
|
286
287
|
// the guardianPrincipalId, and resolveGuardianContext classifies the
|
|
287
288
|
// local user as 'guardian' via binding match.
|
|
288
289
|
session.setGuardianContext(resolveLocalIpcGuardianContext(ipcChannel));
|
|
290
|
+
// Align IPC sessions with the same AuthContext shape as HTTP sessions.
|
|
291
|
+
session.setAuthContext(resolveLocalIpcAuthContext(msg.sessionId));
|
|
289
292
|
session.setCommandIntent(null);
|
|
290
293
|
// Fire-and-forget: don't block the IPC handler so the connection can
|
|
291
294
|
// continue receiving messages (e.g. cancel, confirmations, or
|
|
@@ -6,6 +6,7 @@ import { v4 as uuid } from 'uuid';
|
|
|
6
6
|
import { getConfig } from '../../config/loader.js';
|
|
7
7
|
import type { HeartbeatService } from '../../heartbeat/heartbeat-service.js';
|
|
8
8
|
import type { SecretPromptResult } from '../../permissions/secret-prompter.js';
|
|
9
|
+
import type { AuthContext } from '../../runtime/auth/types.js';
|
|
9
10
|
import type { DebouncerMap } from '../../util/debounce.js';
|
|
10
11
|
import { getLogger } from '../../util/logger.js';
|
|
11
12
|
import { estimateBase64Bytes } from '../assistant-attachments.js';
|
|
@@ -105,6 +106,8 @@ export interface SessionCreateOptions {
|
|
|
105
106
|
transport?: SessionTransportMetadata;
|
|
106
107
|
assistantId?: string;
|
|
107
108
|
guardianContext?: GuardianRuntimeContext;
|
|
109
|
+
/** Normalized auth context for the session (IPC or HTTP-derived). */
|
|
110
|
+
authContext?: AuthContext;
|
|
108
111
|
/** Whether this turn can block on interactive approval prompts. */
|
|
109
112
|
isInteractive?: boolean;
|
|
110
113
|
memoryScopeId?: string;
|
|
@@ -299,11 +299,43 @@ export async function handleSkillsInstall(
|
|
|
299
299
|
(s) => s.id === msg.slug && s.source === "bundled",
|
|
300
300
|
);
|
|
301
301
|
if (bundled) {
|
|
302
|
+
// Auto-enable the bundled skill so it's immediately usable
|
|
303
|
+
try {
|
|
304
|
+
const raw = loadRawConfig();
|
|
305
|
+
ensureSkillEntry(raw, msg.slug).enabled = true;
|
|
306
|
+
ctx.setSuppressConfigReload(true);
|
|
307
|
+
try {
|
|
308
|
+
saveRawConfig(raw);
|
|
309
|
+
} catch (err) {
|
|
310
|
+
ctx.setSuppressConfigReload(false);
|
|
311
|
+
throw err;
|
|
312
|
+
}
|
|
313
|
+
invalidateConfigCache();
|
|
314
|
+
ctx.debounceTimers.schedule(
|
|
315
|
+
"__suppress_reset__",
|
|
316
|
+
() => {
|
|
317
|
+
ctx.setSuppressConfigReload(false);
|
|
318
|
+
},
|
|
319
|
+
CONFIG_RELOAD_DEBOUNCE_MS,
|
|
320
|
+
);
|
|
321
|
+
ctx.updateConfigFingerprint();
|
|
322
|
+
} catch (err) {
|
|
323
|
+
log.warn(
|
|
324
|
+
{ err, skillId: msg.slug },
|
|
325
|
+
"Failed to auto-enable bundled skill",
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
|
|
302
329
|
ctx.send(socket, {
|
|
303
330
|
type: "skills_operation_response",
|
|
304
331
|
operation: "install",
|
|
305
332
|
success: true,
|
|
306
333
|
});
|
|
334
|
+
ctx.broadcast({
|
|
335
|
+
type: "skills_state_changed",
|
|
336
|
+
name: msg.slug,
|
|
337
|
+
state: "enabled",
|
|
338
|
+
});
|
|
307
339
|
return;
|
|
308
340
|
}
|
|
309
341
|
|
|
@@ -30,7 +30,7 @@ export interface UserMessage {
|
|
|
30
30
|
export interface ConfirmationResponse {
|
|
31
31
|
type: 'confirmation_response';
|
|
32
32
|
requestId: string;
|
|
33
|
-
decision: 'allow' | 'always_allow' | 'always_allow_high_risk' | 'deny' | 'always_deny';
|
|
33
|
+
decision: 'allow' | 'allow_10m' | 'allow_thread' | 'always_allow' | 'always_allow_high_risk' | 'deny' | 'always_deny';
|
|
34
34
|
selectedPattern?: string;
|
|
35
35
|
selectedScope?: string;
|
|
36
36
|
}
|
|
@@ -119,6 +119,8 @@ export interface ConfirmationRequest {
|
|
|
119
119
|
sessionId?: string;
|
|
120
120
|
/** When false, the client should hide "always allow" / trust-rule persistence affordances. */
|
|
121
121
|
persistentDecisionsAllowed?: boolean;
|
|
122
|
+
/** Which temporary approval options the client should render (e.g. "Allow for 10 minutes", "Allow for this thread"). */
|
|
123
|
+
temporaryOptionsAvailable?: Array<'allow_10m' | 'allow_thread'>;
|
|
122
124
|
}
|
|
123
125
|
|
|
124
126
|
export interface SecretRequest {
|
|
@@ -7,6 +7,10 @@ import * as net from 'node:net';
|
|
|
7
7
|
|
|
8
8
|
import { buildAssistantEvent } from '../runtime/assistant-event.js';
|
|
9
9
|
import { assistantEventHub } from '../runtime/assistant-event-hub.js';
|
|
10
|
+
import { DAEMON_INTERNAL_ASSISTANT_ID } from '../runtime/assistant-scope.js';
|
|
11
|
+
import { CURRENT_POLICY_EPOCH } from '../runtime/auth/policy.js';
|
|
12
|
+
import { resolveScopeProfile } from '../runtime/auth/scopes.js';
|
|
13
|
+
import type { AuthContext } from '../runtime/auth/types.js';
|
|
10
14
|
import { getLogger } from '../util/logger.js';
|
|
11
15
|
import { serialize, type ServerMessage } from './ipc-protocol.js';
|
|
12
16
|
|
|
@@ -78,6 +82,26 @@ export class IpcSender {
|
|
|
78
82
|
}
|
|
79
83
|
}
|
|
80
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Build a synthetic AuthContext for an IPC session.
|
|
87
|
+
*
|
|
88
|
+
* IPC connections are local-only (Unix domain socket) and pre-authenticated
|
|
89
|
+
* via the daemon's file-system permission model. This produces the same
|
|
90
|
+
* AuthContext shape that HTTP routes receive from JWT verification, keeping
|
|
91
|
+
* downstream code transport-agnostic.
|
|
92
|
+
*/
|
|
93
|
+
export function buildIpcAuthContext(sessionId: string): AuthContext {
|
|
94
|
+
return {
|
|
95
|
+
subject: `ipc:self:${sessionId}`,
|
|
96
|
+
principalType: 'ipc',
|
|
97
|
+
assistantId: DAEMON_INTERNAL_ASSISTANT_ID,
|
|
98
|
+
sessionId,
|
|
99
|
+
scopeProfile: 'ipc_v1',
|
|
100
|
+
scopes: resolveScopeProfile('ipc_v1'),
|
|
101
|
+
policyEpoch: CURRENT_POLICY_EPOCH,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
81
105
|
/** Extract sessionId from a ServerMessage if present. */
|
|
82
106
|
function extractSessionId(msg: ServerMessage): string | undefined {
|
|
83
107
|
const record = msg as unknown as Record<string, unknown>;
|
|
@@ -137,7 +137,7 @@ const HIGH_RISK_VALIDATORS: Record<string, PropertyValidator> = {
|
|
|
137
137
|
if (typeof obj.requestId !== 'string' || obj.requestId === '') {
|
|
138
138
|
return 'confirmation_response requires a non-empty string "requestId"';
|
|
139
139
|
}
|
|
140
|
-
const validDecisions = ['allow', 'always_allow', 'always_allow_high_risk', 'deny', 'always_deny'];
|
|
140
|
+
const validDecisions = ['allow', 'allow_10m', 'allow_thread', 'always_allow', 'always_allow_high_risk', 'deny', 'always_deny'];
|
|
141
141
|
if (typeof obj.decision !== 'string' || !validDecisions.includes(obj.decision)) {
|
|
142
142
|
return `confirmation_response "decision" must be one of: ${validDecisions.join(', ')}`;
|
|
143
143
|
}
|
package/src/daemon/lifecycle.ts
CHANGED
|
@@ -39,11 +39,8 @@ import {
|
|
|
39
39
|
emitNotificationSignal,
|
|
40
40
|
registerBroadcastFn,
|
|
41
41
|
} from "../notifications/emit-signal.js";
|
|
42
|
-
import {
|
|
43
|
-
initSigningKey,
|
|
44
|
-
loadOrCreateSigningKey,
|
|
45
|
-
} from "../runtime/actor-token-service.js";
|
|
46
42
|
import { assistantEventHub } from "../runtime/assistant-event-hub.js";
|
|
43
|
+
import { initAuthSigningKey, loadOrCreateSigningKey } from "../runtime/auth/token-service.js";
|
|
47
44
|
import { ensureVellumGuardianBinding } from "../runtime/guardian-vellum-migration.js";
|
|
48
45
|
import { RuntimeHttpServer } from "../runtime/http-server.js";
|
|
49
46
|
import { startScheduler } from "../schedule/scheduler.js";
|
|
@@ -168,10 +165,11 @@ export async function runDaemon(): Promise<void> {
|
|
|
168
165
|
chmodSync(httpTokenPath, 0o600);
|
|
169
166
|
log.info("Daemon startup: bearer token written");
|
|
170
167
|
|
|
171
|
-
// Load (or generate + persist) the
|
|
172
|
-
//
|
|
173
|
-
//
|
|
174
|
-
|
|
168
|
+
// Load (or generate + persist) the auth signing key so tokens survive
|
|
169
|
+
// daemon restarts. Must happen after ensureDataDir() creates the
|
|
170
|
+
// protected directory.
|
|
171
|
+
const signingKey = loadOrCreateSigningKey();
|
|
172
|
+
initAuthSigningKey(signingKey);
|
|
175
173
|
|
|
176
174
|
log.info("Daemon startup: migrations complete");
|
|
177
175
|
|
package/src/daemon/server.ts
CHANGED
|
@@ -163,6 +163,7 @@ function makePendingInteractionRegistrar(
|
|
|
163
163
|
allowlistOptions: msg.allowlistOptions,
|
|
164
164
|
scopeOptions: msg.scopeOptions,
|
|
165
165
|
persistentDecisionsAllowed: msg.persistentDecisionsAllowed,
|
|
166
|
+
temporaryOptionsAvailable: msg.temporaryOptionsAvailable,
|
|
166
167
|
},
|
|
167
168
|
});
|
|
168
169
|
|
|
@@ -1056,6 +1057,7 @@ export class DaemonServer {
|
|
|
1056
1057
|
options?.assistantId ?? DAEMON_INTERNAL_ASSISTANT_ID,
|
|
1057
1058
|
);
|
|
1058
1059
|
session.setGuardianContext(options?.guardianContext ?? null);
|
|
1060
|
+
session.setAuthContext(options?.authContext ?? null);
|
|
1059
1061
|
await session.ensureActorScopedHistory();
|
|
1060
1062
|
session.setChannelCapabilities(
|
|
1061
1063
|
resolveChannelCapabilities(sourceChannel, sourceInterface),
|
|
@@ -1120,6 +1122,7 @@ export class DaemonServer {
|
|
|
1120
1122
|
session
|
|
1121
1123
|
.runAgentLoop(content, messageId, onEvent, {
|
|
1122
1124
|
isInteractive: options?.isInteractive ?? false,
|
|
1125
|
+
isUserMessage: true,
|
|
1123
1126
|
})
|
|
1124
1127
|
.finally(() => {
|
|
1125
1128
|
// Only reset if no other caller (e.g. a real IPC client) has rebound
|
|
@@ -1259,6 +1262,7 @@ export class DaemonServer {
|
|
|
1259
1262
|
try {
|
|
1260
1263
|
await session.runAgentLoop(resolvedContent, messageId, onEvent, {
|
|
1261
1264
|
isInteractive: options?.isInteractive ?? false,
|
|
1265
|
+
isUserMessage: true,
|
|
1262
1266
|
});
|
|
1263
1267
|
} finally {
|
|
1264
1268
|
// Only reset if no other caller (e.g. a real IPC client) has rebound
|
|
@@ -1284,9 +1288,10 @@ export class DaemonServer {
|
|
|
1284
1288
|
|
|
1285
1289
|
/**
|
|
1286
1290
|
* Look up an active session by ID without creating one.
|
|
1287
|
-
*
|
|
1291
|
+
* Checks both normal sessions and computer-use sessions so the HTTP
|
|
1292
|
+
* surface-action path is consistent with IPC dispatch.
|
|
1288
1293
|
*/
|
|
1289
|
-
findSession(sessionId: string): Session | undefined {
|
|
1290
|
-
return this.sessions.get(sessionId);
|
|
1294
|
+
findSession(sessionId: string): Session | ComputerUseSession | undefined {
|
|
1295
|
+
return this.cuSessions.get(sessionId) ?? this.sessions.get(sessionId);
|
|
1291
1296
|
}
|
|
1292
1297
|
}
|
|
@@ -160,7 +160,7 @@ export async function runAgentLoopImpl(
|
|
|
160
160
|
content: string,
|
|
161
161
|
userMessageId: string,
|
|
162
162
|
onEvent: (msg: ServerMessage) => void,
|
|
163
|
-
options?: { skipPreMessageRollback?: boolean; isInteractive?: boolean; titleText?: string },
|
|
163
|
+
options?: { skipPreMessageRollback?: boolean; isInteractive?: boolean; isUserMessage?: boolean; titleText?: string },
|
|
164
164
|
): Promise<void> {
|
|
165
165
|
if (!ctx.abortController) {
|
|
166
166
|
throw new Error('runAgentLoop called without prior persistUserMessage');
|
|
@@ -213,6 +213,24 @@ export async function runAgentLoopImpl(
|
|
|
213
213
|
let turnStarted = false;
|
|
214
214
|
|
|
215
215
|
try {
|
|
216
|
+
// Auto-complete stale interactive surfaces from previous turns.
|
|
217
|
+
// Only dismiss when the user sends a new message (not a surface action
|
|
218
|
+
// response), so internal turns (subagent notifications, lifecycle
|
|
219
|
+
// instructions) don't accidentally clear active interactive prompts.
|
|
220
|
+
// Placed inside try so the finally block still runs if onEvent throws.
|
|
221
|
+
if (options?.isUserMessage && !ctx.surfaceActionRequestIds.has(reqId)) {
|
|
222
|
+
for (const [surfaceId, entry] of ctx.pendingSurfaceActions) {
|
|
223
|
+
if (entry.surfaceType === 'dynamic_page') continue;
|
|
224
|
+
onEvent({
|
|
225
|
+
type: 'ui_surface_complete',
|
|
226
|
+
sessionId: ctx.conversationId,
|
|
227
|
+
surfaceId,
|
|
228
|
+
summary: 'Dismissed',
|
|
229
|
+
});
|
|
230
|
+
ctx.pendingSurfaceActions.delete(surfaceId);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
216
234
|
const preMessageResult = await getHookManager().trigger('pre-message', {
|
|
217
235
|
sessionId: ctx.conversationId,
|
|
218
236
|
messagePreview: truncate(content, 200, ''),
|
|
@@ -2,6 +2,7 @@ import { AttachmentUploadError,linkAttachmentToMessage, setAttachmentThumbnail,
|
|
|
2
2
|
import { check, classifyRisk, generateAllowlistOptions, generateScopeOptions } from '../permissions/checker.js';
|
|
3
3
|
import type { PermissionPrompter } from '../permissions/prompter.js';
|
|
4
4
|
import { addRule } from '../permissions/trust-store.js';
|
|
5
|
+
import { isAllowDecision } from '../permissions/types.js';
|
|
5
6
|
import type { ContentBlock } from '../providers/types.js';
|
|
6
7
|
import { getLogger } from '../util/logger.js';
|
|
7
8
|
import {
|
|
@@ -67,7 +68,7 @@ export async function approveHostAttachmentRead(
|
|
|
67
68
|
addRule(toolName, response.selectedPattern, response.selectedScope, 'deny');
|
|
68
69
|
}
|
|
69
70
|
|
|
70
|
-
return response.decision
|
|
71
|
+
return isAllowDecision(response.decision);
|
|
71
72
|
}
|
|
72
73
|
|
|
73
74
|
export function formatAttachmentWarnings(warnings: string[]): string | null {
|
|
@@ -264,7 +264,7 @@ export interface HistorySessionContext {
|
|
|
264
264
|
content: string,
|
|
265
265
|
userMessageId: string,
|
|
266
266
|
onEvent: (msg: ServerMessage) => void,
|
|
267
|
-
options?: { skipPreMessageRollback?: boolean; titleText?: string },
|
|
267
|
+
options?: { skipPreMessageRollback?: boolean; isUserMessage?: boolean; titleText?: string },
|
|
268
268
|
): Promise<void>;
|
|
269
269
|
}
|
|
270
270
|
|
|
@@ -435,5 +435,5 @@ export async function regenerate(
|
|
|
435
435
|
session.abortController = new AbortController();
|
|
436
436
|
session.currentRequestId = requestId ?? uuid();
|
|
437
437
|
|
|
438
|
-
await session.runAgentLoop(content, existingUserMessageId, onEvent, { skipPreMessageRollback: true });
|
|
438
|
+
await session.runAgentLoop(content, existingUserMessageId, onEvent, { skipPreMessageRollback: true, isUserMessage: true });
|
|
439
439
|
}
|
|
@@ -81,7 +81,7 @@ export interface ProcessSessionContext {
|
|
|
81
81
|
content: string,
|
|
82
82
|
userMessageId: string,
|
|
83
83
|
onEvent: (msg: ServerMessage) => void,
|
|
84
|
-
options?: { skipPreMessageRollback?: boolean; isInteractive?: boolean; titleText?: string },
|
|
84
|
+
options?: { skipPreMessageRollback?: boolean; isInteractive?: boolean; isUserMessage?: boolean; titleText?: string },
|
|
85
85
|
): Promise<void>;
|
|
86
86
|
getTurnChannelContext(): TurnChannelContext | null;
|
|
87
87
|
setTurnChannelContext(ctx: TurnChannelContext): void;
|
|
@@ -329,13 +329,11 @@ export async function drainQueue(session: ProcessSessionContext, reason: QueueDr
|
|
|
329
329
|
// Fire-and-forget: persistUserMessage set session.processing = true
|
|
330
330
|
// so subsequent messages will still be enqueued.
|
|
331
331
|
// runAgentLoop's finally block will call drainQueue when this run completes.
|
|
332
|
-
const drainLoopOptions: { isInteractive?: boolean; titleText?: string } = {};
|
|
332
|
+
const drainLoopOptions: { isInteractive?: boolean; isUserMessage?: boolean; titleText?: string } = { isUserMessage: true };
|
|
333
333
|
if (next.isInteractive !== undefined) drainLoopOptions.isInteractive = next.isInteractive;
|
|
334
334
|
if (agentLoopContent !== resolvedContent) drainLoopOptions.titleText = resolvedContent;
|
|
335
335
|
|
|
336
|
-
session.runAgentLoop(agentLoopContent, userMessageId, next.onEvent,
|
|
337
|
-
Object.keys(drainLoopOptions).length > 0 ? drainLoopOptions : undefined,
|
|
338
|
-
).catch((err) => {
|
|
336
|
+
session.runAgentLoop(agentLoopContent, userMessageId, next.onEvent, drainLoopOptions).catch((err) => {
|
|
339
337
|
const message = err instanceof Error ? err.message : String(err);
|
|
340
338
|
log.error({ err, conversationId: session.conversationId, requestId: next.requestId }, 'Error processing queued message');
|
|
341
339
|
next.onEvent({ type: 'error', message: `Failed to process queued message: ${message}` });
|
|
@@ -559,12 +557,10 @@ export async function processMessage(
|
|
|
559
557
|
});
|
|
560
558
|
}
|
|
561
559
|
|
|
562
|
-
const loopOptions: { isInteractive?: boolean; titleText?: string } = {};
|
|
560
|
+
const loopOptions: { isInteractive?: boolean; isUserMessage?: boolean; titleText?: string } = { isUserMessage: true };
|
|
563
561
|
if (options?.isInteractive !== undefined) loopOptions.isInteractive = options.isInteractive;
|
|
564
562
|
if (agentLoopContent !== resolvedContent) loopOptions.titleText = resolvedContent;
|
|
565
563
|
|
|
566
|
-
await session.runAgentLoop(agentLoopContent, userMessageId, onEvent,
|
|
567
|
-
Object.keys(loopOptions).length > 0 ? loopOptions : undefined,
|
|
568
|
-
);
|
|
564
|
+
await session.runAgentLoop(agentLoopContent, userMessageId, onEvent, loopOptions);
|
|
569
565
|
return userMessageId;
|
|
570
566
|
}
|
|
@@ -150,6 +150,7 @@ export interface SurfaceSessionContext {
|
|
|
150
150
|
attachments: never[],
|
|
151
151
|
onEvent: (msg: ServerMessage) => void,
|
|
152
152
|
requestId: string,
|
|
153
|
+
activeSurfaceId?: string,
|
|
153
154
|
): { queued: boolean; rejected?: boolean; requestId: string };
|
|
154
155
|
getQueueDepth(): number;
|
|
155
156
|
processMessage(
|
|
@@ -425,7 +426,7 @@ export function handleSurfaceAction(ctx: SurfaceSessionContext, surfaceId: strin
|
|
|
425
426
|
attributes: { source: 'surface_action', surfaceId, actionId },
|
|
426
427
|
});
|
|
427
428
|
|
|
428
|
-
const result = ctx.enqueueMessage(content, [], onEvent, requestId);
|
|
429
|
+
const result = ctx.enqueueMessage(content, [], onEvent, requestId, surfaceId);
|
|
429
430
|
if (result.queued) {
|
|
430
431
|
const position = ctx.getQueueDepth();
|
|
431
432
|
if (!retainPending) {
|
|
@@ -631,6 +632,21 @@ export async function surfaceProxyResolver(
|
|
|
631
632
|
: INTERACTIVE_SURFACE_TYPES.includes(surfaceType);
|
|
632
633
|
const awaitAction = (input.await_action as boolean) ?? isInteractive;
|
|
633
634
|
|
|
635
|
+
// Only one non-persistent interactive surface at a time. If another
|
|
636
|
+
// surface is already awaiting user input, reject this one so the LLM
|
|
637
|
+
// presents surfaces sequentially.
|
|
638
|
+
if (awaitAction) {
|
|
639
|
+
const hasExistingPending = [...ctx.pendingSurfaceActions.values()].some(
|
|
640
|
+
entry => entry.surfaceType !== 'dynamic_page'
|
|
641
|
+
);
|
|
642
|
+
if (hasExistingPending) {
|
|
643
|
+
return {
|
|
644
|
+
content: 'Another interactive surface is already awaiting user input. Present one at a time — wait for the user to respond to the current surface before showing the next.',
|
|
645
|
+
isError: true,
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
634
650
|
// Track surface state for ui_update merging
|
|
635
651
|
ctx.surfaceState.set(surfaceId, { surfaceType, data, title });
|
|
636
652
|
|