@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
|
@@ -4,63 +4,43 @@
|
|
|
4
4
|
* These endpoints resolve pending confirmations, secrets, and trust rules
|
|
5
5
|
* by requestId — orthogonal to message sending.
|
|
6
6
|
*
|
|
7
|
-
* All approval endpoints require a valid
|
|
8
|
-
* header
|
|
9
|
-
*
|
|
7
|
+
* All approval endpoints require a valid JWT via the Authorization: Bearer
|
|
8
|
+
* header. Guardian decisions additionally verify that the actor is the
|
|
9
|
+
* bound guardian.
|
|
10
10
|
*/
|
|
11
|
+
import { isHttpAuthDisabled } from '../../config/env.js';
|
|
11
12
|
import { getConversationByKey } from '../../memory/conversation-key-store.js';
|
|
13
|
+
import { getActiveBinding } from '../../memory/guardian-bindings.js';
|
|
12
14
|
import { addRule } from '../../permissions/trust-store.js';
|
|
15
|
+
import type { UserDecision } from '../../permissions/types.js';
|
|
13
16
|
import { getTool } from '../../tools/registry.js';
|
|
14
17
|
import { getLogger } from '../../util/logger.js';
|
|
18
|
+
import { DAEMON_INTERNAL_ASSISTANT_ID } from '../assistant-scope.js';
|
|
19
|
+
import type { AuthContext } from '../auth/types.js';
|
|
15
20
|
import { httpError } from '../http-errors.js';
|
|
16
|
-
import {
|
|
17
|
-
isActorBoundGuardian,
|
|
18
|
-
isLocalFallbackBoundGuardian,
|
|
19
|
-
type ServerWithRequestIP,
|
|
20
|
-
verifyHttpActorTokenWithLocalFallback,
|
|
21
|
-
} from '../middleware/actor-token.js';
|
|
22
21
|
import * as pendingInteractions from '../pending-interactions.js';
|
|
23
22
|
|
|
24
23
|
const log = getLogger('approval-routes');
|
|
25
24
|
|
|
26
25
|
/**
|
|
27
|
-
* Verify the actor
|
|
28
|
-
* Returns an error Response if
|
|
29
|
-
* the actor is authenticated (via actor token or local identity).
|
|
26
|
+
* Verify the actor from AuthContext is the bound guardian for the vellum channel.
|
|
27
|
+
* Returns an error Response if not, or null if allowed.
|
|
30
28
|
*/
|
|
31
|
-
function
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
result.message,
|
|
37
|
-
result.status,
|
|
38
|
-
);
|
|
29
|
+
function requireBoundGuardian(authContext: AuthContext): Response | null {
|
|
30
|
+
// Dev bypass: when auth is disabled, skip guardian binding check
|
|
31
|
+
// (mirrors enforcePolicy dev bypass in route-policy.ts)
|
|
32
|
+
if (isHttpAuthDisabled()) {
|
|
33
|
+
return null;
|
|
39
34
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
* identity is the bound guardian.
|
|
48
|
-
*/
|
|
49
|
-
function requireBoundGuardian(req: Request, server: ServerWithRequestIP): Response | null {
|
|
50
|
-
const result = verifyHttpActorTokenWithLocalFallback(req, server);
|
|
51
|
-
if (!result.ok) {
|
|
52
|
-
return httpError(
|
|
53
|
-
result.status === 401 ? 'UNAUTHORIZED' : 'FORBIDDEN',
|
|
54
|
-
result.message,
|
|
55
|
-
result.status,
|
|
56
|
-
);
|
|
35
|
+
if (!authContext.actorPrincipalId) {
|
|
36
|
+
return httpError('FORBIDDEN', 'Actor is not the bound guardian for this channel', 403);
|
|
37
|
+
}
|
|
38
|
+
const binding = getActiveBinding(DAEMON_INTERNAL_ASSISTANT_ID, 'vellum');
|
|
39
|
+
if (!binding) {
|
|
40
|
+
// No binding yet — in pre-bootstrap state, allow through
|
|
41
|
+
return null;
|
|
57
42
|
}
|
|
58
|
-
|
|
59
|
-
// For local fallback (bearer-auth only), check the local identity.
|
|
60
|
-
const isBoundGuardian = result.claims
|
|
61
|
-
? isActorBoundGuardian(result.claims)
|
|
62
|
-
: isLocalFallbackBoundGuardian();
|
|
63
|
-
if (!isBoundGuardian) {
|
|
43
|
+
if (binding.guardianExternalUserId !== authContext.actorPrincipalId) {
|
|
64
44
|
return httpError('FORBIDDEN', 'Actor is not the bound guardian for this channel', 403);
|
|
65
45
|
}
|
|
66
46
|
return null;
|
|
@@ -68,10 +48,10 @@ function requireBoundGuardian(req: Request, server: ServerWithRequestIP): Respon
|
|
|
68
48
|
|
|
69
49
|
/**
|
|
70
50
|
* POST /v1/confirm — resolve a pending confirmation by requestId.
|
|
71
|
-
* Requires
|
|
51
|
+
* Requires AuthContext with guardian-bound actor.
|
|
72
52
|
*/
|
|
73
|
-
export async function handleConfirm(req: Request,
|
|
74
|
-
const authError = requireBoundGuardian(
|
|
53
|
+
export async function handleConfirm(req: Request, authContext: AuthContext): Promise<Response> {
|
|
54
|
+
const authError = requireBoundGuardian(authContext);
|
|
75
55
|
if (authError) return authError;
|
|
76
56
|
|
|
77
57
|
const body = await req.json() as {
|
|
@@ -85,8 +65,9 @@ export async function handleConfirm(req: Request, server: ServerWithRequestIP):
|
|
|
85
65
|
return httpError('BAD_REQUEST', 'requestId is required', 400);
|
|
86
66
|
}
|
|
87
67
|
|
|
88
|
-
|
|
89
|
-
|
|
68
|
+
const validConfirmDecisions = ['allow', 'allow_10m', 'allow_thread', 'deny'];
|
|
69
|
+
if (typeof decision !== 'string' || !validConfirmDecisions.includes(decision)) {
|
|
70
|
+
return httpError('BAD_REQUEST', `decision must be one of: ${validConfirmDecisions.join(', ')}`, 400);
|
|
90
71
|
}
|
|
91
72
|
|
|
92
73
|
const interaction = pendingInteractions.resolve(requestId);
|
|
@@ -94,7 +75,7 @@ export async function handleConfirm(req: Request, server: ServerWithRequestIP):
|
|
|
94
75
|
return httpError('NOT_FOUND', 'No pending interaction found for this requestId', 404);
|
|
95
76
|
}
|
|
96
77
|
|
|
97
|
-
interaction.session.handleConfirmationResponse(requestId, decision, undefined, undefined, undefined, {
|
|
78
|
+
interaction.session.handleConfirmationResponse(requestId, decision as UserDecision, undefined, undefined, undefined, {
|
|
98
79
|
source: 'button',
|
|
99
80
|
});
|
|
100
81
|
return Response.json({ accepted: true });
|
|
@@ -102,10 +83,10 @@ export async function handleConfirm(req: Request, server: ServerWithRequestIP):
|
|
|
102
83
|
|
|
103
84
|
/**
|
|
104
85
|
* POST /v1/secret — resolve a pending secret request by requestId.
|
|
105
|
-
* Requires
|
|
86
|
+
* Requires AuthContext with guardian-bound actor.
|
|
106
87
|
*/
|
|
107
|
-
export async function handleSecret(req: Request,
|
|
108
|
-
const authError = requireBoundGuardian(
|
|
88
|
+
export async function handleSecret(req: Request, authContext: AuthContext): Promise<Response> {
|
|
89
|
+
const authError = requireBoundGuardian(authContext);
|
|
109
90
|
if (authError) return authError;
|
|
110
91
|
|
|
111
92
|
const body = await req.json() as {
|
|
@@ -139,15 +120,15 @@ export async function handleSecret(req: Request, server: ServerWithRequestIP): P
|
|
|
139
120
|
|
|
140
121
|
/**
|
|
141
122
|
* POST /v1/trust-rules — add a trust rule for a pending confirmation.
|
|
142
|
-
* Requires
|
|
123
|
+
* Requires AuthContext with guardian-bound actor.
|
|
143
124
|
*
|
|
144
125
|
* Does NOT resolve the confirmation itself (the client still needs to
|
|
145
126
|
* POST /v1/confirm to approve/deny). Validates the pattern and scope
|
|
146
127
|
* against the server-provided allowlist options from the original
|
|
147
128
|
* confirmation_request.
|
|
148
129
|
*/
|
|
149
|
-
export async function handleTrustRule(req: Request,
|
|
150
|
-
const authError = requireBoundGuardian(
|
|
130
|
+
export async function handleTrustRule(req: Request, authContext: AuthContext): Promise<Response> {
|
|
131
|
+
const authError = requireBoundGuardian(authContext);
|
|
151
132
|
if (authError) return authError;
|
|
152
133
|
|
|
153
134
|
const body = await req.json() as {
|
|
@@ -227,15 +208,16 @@ export async function handleTrustRule(req: Request, server: ServerWithRequestIP)
|
|
|
227
208
|
|
|
228
209
|
/**
|
|
229
210
|
* GET /v1/pending-interactions?conversationKey=...
|
|
230
|
-
* Requires
|
|
211
|
+
* Requires AuthContext (already verified upstream by JWT middleware).
|
|
231
212
|
*
|
|
232
213
|
* Returns pending confirmations and secrets for a conversation, allowing
|
|
233
214
|
* polling-based clients (like the CLI) to discover approval requests
|
|
234
215
|
* without SSE.
|
|
235
216
|
*/
|
|
236
|
-
export function handleListPendingInteractions(url: URL,
|
|
237
|
-
|
|
238
|
-
|
|
217
|
+
export function handleListPendingInteractions(url: URL, _authContext: AuthContext): Response {
|
|
218
|
+
// Auth is already verified by JWT middleware upstream — no additional
|
|
219
|
+
// verification needed here. The _authContext parameter is accepted for
|
|
220
|
+
// type consistency and potential future use.
|
|
239
221
|
const conversationKey = url.searchParams.get('conversationKey');
|
|
240
222
|
const conversationId = url.searchParams.get('conversationId');
|
|
241
223
|
|
|
@@ -273,6 +255,7 @@ export function handleListPendingInteractions(url: URL, req: Request, server: Se
|
|
|
273
255
|
})),
|
|
274
256
|
scopeOptions: confirmation.confirmationDetails?.scopeOptions,
|
|
275
257
|
persistentDecisionsAllowed: confirmation.confirmationDetails?.persistentDecisionsAllowed,
|
|
258
|
+
temporaryOptionsAvailable: confirmation.confirmationDetails?.temporaryOptionsAvailable,
|
|
276
259
|
}
|
|
277
260
|
: null,
|
|
278
261
|
pendingSecret: secret
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared types, constants, and utilities used across channel route modules.
|
|
3
3
|
*/
|
|
4
|
-
import { timingSafeEqual } from 'node:crypto';
|
|
5
|
-
|
|
6
4
|
import type { ChannelId } from '../../channels/types.js';
|
|
7
5
|
import { DAEMON_INTERNAL_ASSISTANT_ID } from '../assistant-scope.js';
|
|
8
6
|
import type {
|
|
@@ -18,44 +16,6 @@ export function canonicalChannelAssistantId(_assistantId: string): string {
|
|
|
18
16
|
return DAEMON_INTERNAL_ASSISTANT_ID;
|
|
19
17
|
}
|
|
20
18
|
|
|
21
|
-
// ---------------------------------------------------------------------------
|
|
22
|
-
// Gateway-origin proof
|
|
23
|
-
// ---------------------------------------------------------------------------
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Header name used by the gateway to prove a request originated from it.
|
|
27
|
-
* The gateway sends a dedicated gateway-origin secret (or the bearer token
|
|
28
|
-
* as fallback). The runtime validates it using constant-time comparison.
|
|
29
|
-
* Requests to `/channels/inbound` that lack a valid proof are rejected with 403.
|
|
30
|
-
*/
|
|
31
|
-
export const GATEWAY_ORIGIN_HEADER = 'X-Gateway-Origin';
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Validate that the request carries a valid gateway-origin proof.
|
|
35
|
-
* Uses constant-time comparison to prevent timing attacks.
|
|
36
|
-
*
|
|
37
|
-
* The `gatewayOriginSecret` parameter is the dedicated secret configured
|
|
38
|
-
* via `RUNTIME_GATEWAY_ORIGIN_SECRET`. When set, only this value is
|
|
39
|
-
* accepted. When not set, the function falls back to `bearerToken` for
|
|
40
|
-
* backward compatibility. When neither is configured (local dev), validation
|
|
41
|
-
* is skipped entirely.
|
|
42
|
-
*/
|
|
43
|
-
export function verifyGatewayOrigin(
|
|
44
|
-
req: Request,
|
|
45
|
-
bearerToken?: string,
|
|
46
|
-
gatewayOriginSecret?: string,
|
|
47
|
-
): boolean {
|
|
48
|
-
// Determine the expected secret: prefer dedicated secret, fall back to bearer token
|
|
49
|
-
const expectedSecret = gatewayOriginSecret ?? bearerToken;
|
|
50
|
-
if (!expectedSecret) return true; // No shared secret configured — skip validation
|
|
51
|
-
const provided = req.headers.get(GATEWAY_ORIGIN_HEADER);
|
|
52
|
-
if (!provided) return false;
|
|
53
|
-
const a = Buffer.from(provided);
|
|
54
|
-
const b = Buffer.from(expectedSecret);
|
|
55
|
-
if (a.length !== b.length) return false;
|
|
56
|
-
return timingSafeEqual(a, b);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
19
|
// ---------------------------------------------------------------------------
|
|
60
20
|
// Actor role
|
|
61
21
|
// ---------------------------------------------------------------------------
|
|
@@ -69,7 +29,13 @@ export const GUARDIAN_APPROVAL_TTL_MS = 30 * 60 * 1000;
|
|
|
69
29
|
*/
|
|
70
30
|
export function requiredDecisionKeywords(actions: ApprovalUIMetadata['actions']): string[] {
|
|
71
31
|
const hasAlways = actions.some((action) => action.id === 'approve_always');
|
|
72
|
-
|
|
32
|
+
const has10m = actions.some((action) => action.id === 'approve_10m');
|
|
33
|
+
const hasThread = actions.some((action) => action.id === 'approve_thread');
|
|
34
|
+
const keywords = ['yes', 'no'];
|
|
35
|
+
if (has10m) keywords.push('approve for 10 minutes');
|
|
36
|
+
if (hasThread) keywords.push('approve for thread');
|
|
37
|
+
if (hasAlways) keywords.push('always');
|
|
38
|
+
return keywords;
|
|
73
39
|
}
|
|
74
40
|
|
|
75
41
|
// ---------------------------------------------------------------------------
|
|
@@ -78,6 +44,8 @@ export function requiredDecisionKeywords(actions: ApprovalUIMetadata['actions'])
|
|
|
78
44
|
|
|
79
45
|
const VALID_ACTIONS: ReadonlySet<string> = new Set<string>([
|
|
80
46
|
'approve_once',
|
|
47
|
+
'approve_10m',
|
|
48
|
+
'approve_thread',
|
|
81
49
|
'approve_always',
|
|
82
50
|
'reject',
|
|
83
51
|
]);
|
|
@@ -25,7 +25,12 @@ import type { Provider } from '../../providers/types.js';
|
|
|
25
25
|
import { getLogger } from '../../util/logger.js';
|
|
26
26
|
import { buildAssistantEvent } from '../assistant-event.js';
|
|
27
27
|
import { DAEMON_INTERNAL_ASSISTANT_ID } from '../assistant-scope.js';
|
|
28
|
+
import type { AuthContext } from '../auth/types.js';
|
|
28
29
|
import { bridgeConfirmationRequestToGuardian } from '../confirmation-request-guardian-bridge.js';
|
|
30
|
+
import {
|
|
31
|
+
resolveGuardianContext,
|
|
32
|
+
toGuardianRuntimeContext,
|
|
33
|
+
} from '../guardian-context-resolver.js';
|
|
29
34
|
import { routeGuardianReply } from '../guardian-reply-router.js';
|
|
30
35
|
import { httpError } from '../http-errors.js';
|
|
31
36
|
import type {
|
|
@@ -36,8 +41,6 @@ import type {
|
|
|
36
41
|
RuntimeMessagePayload,
|
|
37
42
|
SendMessageDeps,
|
|
38
43
|
} from '../http-types.js';
|
|
39
|
-
import { resolveLocalIpcGuardianContext } from '../local-actor-identity.js';
|
|
40
|
-
import { type ServerWithRequestIP, verifyHttpActorTokenWithLocalFallback } from '../middleware/actor-token.js';
|
|
41
44
|
import * as pendingInteractions from '../pending-interactions.js';
|
|
42
45
|
|
|
43
46
|
const log = getLogger('conversation-routes');
|
|
@@ -338,6 +341,7 @@ function makeHubPublisher(
|
|
|
338
341
|
allowlistOptions: msg.allowlistOptions,
|
|
339
342
|
scopeOptions: msg.scopeOptions,
|
|
340
343
|
persistentDecisionsAllowed: msg.persistentDecisionsAllowed,
|
|
344
|
+
temporaryOptionsAvailable: msg.temporaryOptionsAvailable,
|
|
341
345
|
},
|
|
342
346
|
});
|
|
343
347
|
|
|
@@ -413,7 +417,7 @@ export async function handleSendMessage(
|
|
|
413
417
|
sendMessageDeps?: SendMessageDeps;
|
|
414
418
|
approvalConversationGenerator?: ApprovalConversationGenerator;
|
|
415
419
|
},
|
|
416
|
-
|
|
420
|
+
authContext: AuthContext,
|
|
417
421
|
): Promise<Response> {
|
|
418
422
|
const body = await req.json() as {
|
|
419
423
|
conversationKey?: string;
|
|
@@ -471,35 +475,27 @@ export async function handleSendMessage(
|
|
|
471
475
|
|
|
472
476
|
// ── Queue-if-busy path (preferred when sendMessageDeps is wired) ────
|
|
473
477
|
if (deps.sendMessageDeps) {
|
|
474
|
-
// Vellum HTTP requests prefer actor-token identity. When absent (e.g. CLI
|
|
475
|
-
// bearer-auth only), fall back to local IPC identity resolution so
|
|
476
|
-
// bearer-authenticated local clients are not rejected.
|
|
477
|
-
const actorVerification = sourceChannel === 'vellum' ? verifyHttpActorTokenWithLocalFallback(req, server) : null;
|
|
478
|
-
if (actorVerification && !actorVerification.ok) {
|
|
479
|
-
return httpError(
|
|
480
|
-
actorVerification.status === 401 ? 'UNAUTHORIZED' : 'FORBIDDEN',
|
|
481
|
-
actorVerification.message,
|
|
482
|
-
actorVerification.status,
|
|
483
|
-
);
|
|
484
|
-
}
|
|
485
|
-
|
|
486
478
|
const smDeps = deps.sendMessageDeps;
|
|
487
479
|
const session = await smDeps.getOrCreateSession(mapping.conversationId);
|
|
488
|
-
|
|
489
|
-
//
|
|
490
|
-
//
|
|
491
|
-
|
|
492
|
-
|
|
480
|
+
|
|
481
|
+
// Resolve guardian context from the AuthContext's actorPrincipalId.
|
|
482
|
+
// The JWT-verified principal is used as the sender identity through
|
|
483
|
+
// the same trust resolution pipeline that channel ingress uses.
|
|
484
|
+
if (authContext.actorPrincipalId) {
|
|
485
|
+
const assistantId = DAEMON_INTERNAL_ASSISTANT_ID;
|
|
486
|
+
const guardianCtx = resolveGuardianContext({
|
|
487
|
+
assistantId,
|
|
488
|
+
sourceChannel: 'vellum',
|
|
489
|
+
conversationExternalId: 'local',
|
|
490
|
+
actorExternalId: authContext.actorPrincipalId,
|
|
491
|
+
});
|
|
492
|
+
session.setGuardianContext(toGuardianRuntimeContext(sourceChannel, guardianCtx));
|
|
493
493
|
} else {
|
|
494
|
-
//
|
|
495
|
-
//
|
|
496
|
-
|
|
497
|
-
// trust. Falls back to a minimal guardian context if no binding
|
|
498
|
-
// exists (pre-bootstrap).
|
|
499
|
-
session.setGuardianContext(
|
|
500
|
-
resolveLocalIpcGuardianContext(sourceChannel) ?? { trustClass: 'guardian', sourceChannel },
|
|
501
|
-
);
|
|
494
|
+
// Service principals (svc_gateway) or tokens without an actor ID
|
|
495
|
+
// get a minimal guardian context so downstream code has something.
|
|
496
|
+
session.setGuardianContext({ trustClass: 'guardian', sourceChannel });
|
|
502
497
|
}
|
|
498
|
+
|
|
503
499
|
const onEvent = makeHubPublisher(smDeps, mapping.conversationId, session);
|
|
504
500
|
// Route server-authoritative state signals (confirmation_state_changed,
|
|
505
501
|
// assistant_activity_state) to the SSE hub. Without this, these signals
|
|
@@ -512,14 +508,9 @@ export async function handleSendMessage(
|
|
|
512
508
|
: [];
|
|
513
509
|
|
|
514
510
|
// Resolve the verified actor's external user ID and principal for inline
|
|
515
|
-
// approval routing
|
|
516
|
-
|
|
517
|
-
const
|
|
518
|
-
? actorVerification.guardianContext.guardianExternalUserId
|
|
519
|
-
: session.guardianContext?.guardianExternalUserId;
|
|
520
|
-
const verifiedActorPrincipalId = actorVerification?.ok
|
|
521
|
-
? actorVerification.guardianContext.guardianPrincipalId ?? undefined
|
|
522
|
-
: session.guardianContext?.guardianPrincipalId ?? undefined;
|
|
511
|
+
// approval routing from the session's guardian context.
|
|
512
|
+
const verifiedActorExternalUserId = session.guardianContext?.guardianExternalUserId;
|
|
513
|
+
const verifiedActorPrincipalId = session.guardianContext?.guardianPrincipalId ?? undefined;
|
|
523
514
|
|
|
524
515
|
// Try to consume the message as a canonical guardian approval/rejection reply.
|
|
525
516
|
// On failure, degrade to the existing queue/auto-deny path rather than
|
|
@@ -617,21 +608,20 @@ export async function handleSendMessage(
|
|
|
617
608
|
return httpError('SERVICE_UNAVAILABLE', 'Message processing not configured', 503);
|
|
618
609
|
}
|
|
619
610
|
|
|
620
|
-
//
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
);
|
|
611
|
+
// Resolve guardian context from AuthContext for the legacy path too.
|
|
612
|
+
let guardianContext: import('../../daemon/session-runtime-assembly.js').GuardianRuntimeContext;
|
|
613
|
+
if (authContext.actorPrincipalId) {
|
|
614
|
+
const legacyGuardianCtx = resolveGuardianContext({
|
|
615
|
+
assistantId: DAEMON_INTERNAL_ASSISTANT_ID,
|
|
616
|
+
sourceChannel: 'vellum',
|
|
617
|
+
conversationExternalId: 'local',
|
|
618
|
+
actorExternalId: authContext.actorPrincipalId,
|
|
619
|
+
});
|
|
620
|
+
guardianContext = toGuardianRuntimeContext(sourceChannel, legacyGuardianCtx);
|
|
621
|
+
} else {
|
|
622
|
+
guardianContext = { trustClass: 'guardian' as const, sourceChannel };
|
|
629
623
|
}
|
|
630
624
|
|
|
631
|
-
const guardianContext = legacyActorVerification?.ok
|
|
632
|
-
? legacyActorVerification.guardianContext
|
|
633
|
-
: resolveLocalIpcGuardianContext(sourceChannel) ?? { trustClass: 'guardian' as const, sourceChannel };
|
|
634
|
-
|
|
635
625
|
try {
|
|
636
626
|
const result = await processor(
|
|
637
627
|
mapping.conversationId,
|
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
*
|
|
4
4
|
* GET /v1/events?conversationKey=...
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* JWT bearer auth is enforced by RuntimeHttpServer before this handler
|
|
7
|
+
* is called. The AuthContext is threaded through from the HTTP server
|
|
8
|
+
* layer, so no additional actor-token verification is needed here.
|
|
9
|
+
*
|
|
9
10
|
* Subscribers receive all assistant events scoped to the given conversation.
|
|
10
11
|
*/
|
|
11
12
|
|
|
@@ -14,8 +15,8 @@ import { formatSseFrame, formatSseHeartbeat } from '../assistant-event.js';
|
|
|
14
15
|
import type { AssistantEventSubscription } from '../assistant-event-hub.js';
|
|
15
16
|
import { AssistantEventHub,assistantEventHub } from '../assistant-event-hub.js';
|
|
16
17
|
import { DAEMON_INTERNAL_ASSISTANT_ID } from '../assistant-scope.js';
|
|
18
|
+
import type { AuthContext } from '../auth/types.js';
|
|
17
19
|
import { httpError } from '../http-errors.js';
|
|
18
|
-
import { type ServerWithRequestIP, verifyHttpActorTokenWithLocalFallback } from '../middleware/actor-token.js';
|
|
19
20
|
|
|
20
21
|
/** Keep-alive comment sent to idle clients every 30 s by default. */
|
|
21
22
|
const DEFAULT_HEARTBEAT_INTERVAL_MS = 30_000;
|
|
@@ -24,31 +25,23 @@ const DEFAULT_HEARTBEAT_INTERVAL_MS = 30_000;
|
|
|
24
25
|
* Stream assistant events as Server-Sent Events for a specific conversation.
|
|
25
26
|
*
|
|
26
27
|
* Query params:
|
|
27
|
-
* conversationKey
|
|
28
|
+
* conversationKey -- required; scopes the stream to one conversation.
|
|
28
29
|
*
|
|
29
30
|
* Options (for testing):
|
|
30
|
-
* hub
|
|
31
|
-
* heartbeatIntervalMs
|
|
31
|
+
* hub -- override the event hub (defaults to process singleton).
|
|
32
|
+
* heartbeatIntervalMs -- how often to emit keep-alive comments (default 30 s).
|
|
32
33
|
*/
|
|
33
34
|
export function handleSubscribeAssistantEvents(
|
|
34
35
|
req: Request,
|
|
35
36
|
url: URL,
|
|
36
37
|
options?:
|
|
37
|
-
| { hub?: AssistantEventHub; heartbeatIntervalMs?: number;
|
|
38
|
+
| { hub?: AssistantEventHub; heartbeatIntervalMs?: number; authContext: AuthContext }
|
|
38
39
|
| { hub?: AssistantEventHub; heartbeatIntervalMs?: number; skipActorVerification: true },
|
|
39
40
|
): Response {
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
if (!actorVerification.ok) {
|
|
45
|
-
return httpError(
|
|
46
|
-
actorVerification.status === 401 ? 'UNAUTHORIZED' : 'FORBIDDEN',
|
|
47
|
-
actorVerification.message,
|
|
48
|
-
actorVerification.status,
|
|
49
|
-
);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
41
|
+
// Auth is already verified upstream by JWT middleware. The AuthContext
|
|
42
|
+
// is available via options.authContext but we don't need to check it
|
|
43
|
+
// further here -- the route policy in http-server.ts already enforced
|
|
44
|
+
// scope and principal type requirements.
|
|
52
45
|
|
|
53
46
|
const conversationKey = url.searchParams.get('conversationKey');
|
|
54
47
|
if (!conversationKey) {
|
|
@@ -61,7 +54,7 @@ export function handleSubscribeAssistantEvents(
|
|
|
61
54
|
const mapping = getOrCreateConversation(conversationKey);
|
|
62
55
|
const encoder = new TextEncoder();
|
|
63
56
|
|
|
64
|
-
//
|
|
57
|
+
// -- Eager subscribe --------------------------------------------------------
|
|
65
58
|
// Subscribe before creating the ReadableStream so the callback and onEvict
|
|
66
59
|
// closures are in place before events can arrive. `controllerRef` is set
|
|
67
60
|
// synchronously inside ReadableStream's start(), so it is non-null by the
|
|
@@ -116,7 +109,7 @@ export function handleSubscribeAssistantEvents(
|
|
|
116
109
|
controllerRef = controller;
|
|
117
110
|
|
|
118
111
|
// If the client already disconnected before start() ran, clean up
|
|
119
|
-
// immediately
|
|
112
|
+
// immediately -- the abort event fires once and won't be re-dispatched.
|
|
120
113
|
if (req.signal.aborted) {
|
|
121
114
|
sub.dispose();
|
|
122
115
|
cleanup();
|