@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
|
@@ -30,39 +30,34 @@ import {
|
|
|
30
30
|
import { parseChannelId } from "../channels/types.js";
|
|
31
31
|
import {
|
|
32
32
|
getGatewayInternalBaseUrl,
|
|
33
|
-
getRuntimeGatewayOriginSecret,
|
|
34
33
|
hasUngatedHttpAuthDisabled,
|
|
35
34
|
isHttpAuthDisabled,
|
|
36
|
-
} from
|
|
37
|
-
import type { ServerMessage } from
|
|
38
|
-
import { PairingStore } from
|
|
39
|
-
import {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
} from
|
|
45
|
-
import
|
|
46
|
-
import
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
} from
|
|
51
|
-
import {
|
|
52
|
-
import {
|
|
53
|
-
import {
|
|
54
|
-
import { DAEMON_INTERNAL_ASSISTANT_ID } from "./assistant-scope.js";
|
|
55
|
-
import { sweepFailedEvents } from "./channel-retry-sweep.js";
|
|
56
|
-
import { httpError } from "./http-errors.js";
|
|
35
|
+
} from '../config/env.js';
|
|
36
|
+
import type { ServerMessage } from '../daemon/ipc-contract.js';
|
|
37
|
+
import { PairingStore } from '../daemon/pairing-store.js';
|
|
38
|
+
import { type Confidence, getAttentionStateByConversationIds, recordConversationSeenSignal, type SignalType } from '../memory/conversation-attention-store.js';
|
|
39
|
+
import * as conversationStore from '../memory/conversation-store.js';
|
|
40
|
+
import * as externalConversationStore from '../memory/external-conversation-store.js';
|
|
41
|
+
import { consumeCallback, consumeCallbackError } from '../security/oauth-callback-registry.js';
|
|
42
|
+
import { getLogger } from '../util/logger.js';
|
|
43
|
+
import { buildAssistantEvent } from './assistant-event.js';
|
|
44
|
+
import { assistantEventHub } from './assistant-event-hub.js';
|
|
45
|
+
import { DAEMON_INTERNAL_ASSISTANT_ID } from './assistant-scope.js';
|
|
46
|
+
// Auth
|
|
47
|
+
import { authenticateRequest } from './auth/middleware.js';
|
|
48
|
+
import { enforcePolicy, getPolicy } from './auth/route-policy.js';
|
|
49
|
+
import { verifyToken } from './auth/token-service.js';
|
|
50
|
+
import type { AuthContext } from './auth/types.js';
|
|
51
|
+
import { sweepFailedEvents } from './channel-retry-sweep.js';
|
|
52
|
+
import { httpError } from './http-errors.js';
|
|
57
53
|
// Middleware
|
|
58
54
|
import {
|
|
59
55
|
extractBearerToken,
|
|
60
56
|
isLoopbackHost,
|
|
61
57
|
isPrivateNetworkOrigin,
|
|
62
58
|
isPrivateNetworkPeer,
|
|
63
|
-
|
|
64
|
-
} from
|
|
65
|
-
import { withErrorHandling } from "./middleware/error-handler.js";
|
|
59
|
+
} from './middleware/auth.js';
|
|
60
|
+
import { withErrorHandling } from './middleware/error-handler.js';
|
|
66
61
|
import {
|
|
67
62
|
apiRateLimiter,
|
|
68
63
|
extractClientIp,
|
|
@@ -241,6 +236,68 @@ const DEFAULT_HOSTNAME = "127.0.0.1";
|
|
|
241
236
|
/** Global hard cap on request body size (50 MB). */
|
|
242
237
|
const MAX_REQUEST_BODY_BYTES = 50 * 1024 * 1024;
|
|
243
238
|
|
|
239
|
+
// ---------------------------------------------------------------------------
|
|
240
|
+
// Parameterized endpoint normalization for policy lookup
|
|
241
|
+
// ---------------------------------------------------------------------------
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Patterns that map parameterized URLs back to their policy registry keys.
|
|
245
|
+
* Order matters: more specific patterns must come before general ones so
|
|
246
|
+
* that e.g. `attachments/{id}/content` matches before `attachments/{id}`.
|
|
247
|
+
*/
|
|
248
|
+
const PARAMETERIZED_ROUTE_PATTERNS: Array<{ re: RegExp; policyBase: string }> = [
|
|
249
|
+
// calls/{id}/cancel, calls/{id}/answer, calls/{id}/instruction
|
|
250
|
+
{ re: /^calls\/[^/]+\/(cancel|answer|instruction)$/, policyBase: 'calls/$1' },
|
|
251
|
+
// calls/{id} (GET status)
|
|
252
|
+
{ re: /^calls\/[^/]+$/, policyBase: 'calls' },
|
|
253
|
+
// contacts/{id}
|
|
254
|
+
{ re: /^contacts\/[^/]+$/, policyBase: 'contacts' },
|
|
255
|
+
// ingress/members/{id}/block
|
|
256
|
+
{ re: /^ingress\/members\/[^/]+\/block$/, policyBase: 'ingress/members/block' },
|
|
257
|
+
// ingress/members/{id}
|
|
258
|
+
{ re: /^ingress\/members\/[^/]+$/, policyBase: 'ingress/members' },
|
|
259
|
+
// ingress/invites/{id}
|
|
260
|
+
{ re: /^ingress\/invites\/[^/]+$/, policyBase: 'ingress/invites' },
|
|
261
|
+
// integrations/twilio/sms/compliance/tollfree/{sid}
|
|
262
|
+
{ re: /^integrations\/twilio\/sms\/compliance\/tollfree\/[^/]+$/, policyBase: 'integrations/twilio/sms/compliance/tollfree' },
|
|
263
|
+
// attachments/{id}/content
|
|
264
|
+
{ re: /^attachments\/[^/]+\/content$/, policyBase: 'attachments/content' },
|
|
265
|
+
// attachments/{id}
|
|
266
|
+
{ re: /^attachments\/[^/]+$/, policyBase: 'attachments' },
|
|
267
|
+
// trust-rules/manage/{id}
|
|
268
|
+
{ re: /^trust-rules\/manage\/[^/]+$/, policyBase: 'trust-rules/manage' },
|
|
269
|
+
// interfaces/{path}
|
|
270
|
+
{ re: /^interfaces\/.+$/, policyBase: 'interfaces' },
|
|
271
|
+
];
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Strip parameterized segments from an endpoint string so it matches
|
|
275
|
+
* the base route name used in the policy registry.
|
|
276
|
+
*
|
|
277
|
+
* For example, `calls/abc123` becomes `calls`, and
|
|
278
|
+
* `attachments/abc123/content` becomes `attachments/content`.
|
|
279
|
+
*
|
|
280
|
+
* If the raw endpoint already has a direct policy registered (either
|
|
281
|
+
* bare or with a method qualifier), normalization is skipped. This
|
|
282
|
+
* prevents literal sub-routes like `calls/start` from being
|
|
283
|
+
* incorrectly collapsed to `calls`.
|
|
284
|
+
*/
|
|
285
|
+
function normalizeEndpointForPolicy(endpoint: string, method: string): string {
|
|
286
|
+
// If the exact endpoint (with or without method) has a direct policy, don't normalize
|
|
287
|
+
if (getPolicy(`${endpoint}:${method}`) || getPolicy(endpoint)) {
|
|
288
|
+
return endpoint;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
for (const { re, policyBase } of PARAMETERIZED_ROUTE_PATTERNS) {
|
|
292
|
+
const match = endpoint.match(re);
|
|
293
|
+
if (match) {
|
|
294
|
+
// Support capture-group substitution (e.g. calls/$1 -> calls/cancel)
|
|
295
|
+
return policyBase.replace(/\$(\d+)/g, (_, idx) => match[Number(idx)] ?? '');
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return endpoint;
|
|
299
|
+
}
|
|
300
|
+
|
|
244
301
|
export class RuntimeHttpServer {
|
|
245
302
|
private server: ReturnType<typeof Bun.serve> | null = null;
|
|
246
303
|
private port: number;
|
|
@@ -260,9 +317,15 @@ export class RuntimeHttpServer {
|
|
|
260
317
|
private pairingStore = new PairingStore();
|
|
261
318
|
private pairingBroadcast?: (msg: ServerMessage) => void;
|
|
262
319
|
private sendMessageDeps?: SendMessageDeps;
|
|
263
|
-
private findSession?: (
|
|
264
|
-
|
|
265
|
-
|
|
320
|
+
private findSession?: (sessionId: string) =>
|
|
321
|
+
| {
|
|
322
|
+
handleSurfaceAction(
|
|
323
|
+
surfaceId: string,
|
|
324
|
+
actionId: string,
|
|
325
|
+
data?: Record<string, unknown>,
|
|
326
|
+
): void;
|
|
327
|
+
}
|
|
328
|
+
| undefined;
|
|
266
329
|
|
|
267
330
|
constructor(options: RuntimeHttpServerOptions = {}) {
|
|
268
331
|
this.port = options.port ?? DEFAULT_PORT;
|
|
@@ -528,13 +591,25 @@ export class RuntimeHttpServer {
|
|
|
528
591
|
return handlePairingStatus(url, this.pairingContext);
|
|
529
592
|
}
|
|
530
593
|
|
|
531
|
-
//
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
594
|
+
// Guardian bootstrap and refresh endpoints — before JWT auth because
|
|
595
|
+
// bootstrap is the first endpoint called to obtain a JWT, and refresh
|
|
596
|
+
// needs to work when the access token is expired. Bootstrap has its
|
|
597
|
+
// own loopback IP validation; refresh is secured by the refresh token
|
|
598
|
+
// in the request body (32 random bytes, hash-only storage).
|
|
599
|
+
if (path === '/v1/integrations/guardian/vellum/bootstrap' && req.method === 'POST') {
|
|
600
|
+
return await handleGuardianBootstrap(req, server);
|
|
601
|
+
}
|
|
602
|
+
if (path === '/v1/integrations/guardian/vellum/refresh' && req.method === 'POST') {
|
|
603
|
+
return await handleGuardianRefresh(req);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// JWT bearer authentication — replaces the old shared-secret comparison.
|
|
607
|
+
// authenticateRequest handles dev bypass (DISABLE_HTTP_AUTH) internally.
|
|
608
|
+
const authResult = authenticateRequest(req);
|
|
609
|
+
if (!authResult.ok) {
|
|
610
|
+
return authResult.response;
|
|
537
611
|
}
|
|
612
|
+
const authContext = authResult.context;
|
|
538
613
|
|
|
539
614
|
// Per-client-IP rate limiting for /v1/* endpoints. Authenticated requests
|
|
540
615
|
// get a higher limit; unauthenticated requests get a lower limit to reduce
|
|
@@ -551,12 +626,7 @@ export class RuntimeHttpServer {
|
|
|
551
626
|
return rateLimitResponse(result);
|
|
552
627
|
}
|
|
553
628
|
// Attach rate limit headers to the eventual response
|
|
554
|
-
const originalResponse = await this.handleAuthenticatedRequest(
|
|
555
|
-
req,
|
|
556
|
-
url,
|
|
557
|
-
path,
|
|
558
|
-
server,
|
|
559
|
-
);
|
|
629
|
+
const originalResponse = await this.handleAuthenticatedRequest(req, url, path, server, authContext);
|
|
560
630
|
const headers = new Headers(originalResponse.headers);
|
|
561
631
|
for (const [k, v] of Object.entries(rateLimitHeaders(result))) {
|
|
562
632
|
headers.set(k, v);
|
|
@@ -568,20 +638,17 @@ export class RuntimeHttpServer {
|
|
|
568
638
|
});
|
|
569
639
|
}
|
|
570
640
|
|
|
571
|
-
return this.handleAuthenticatedRequest(req, url, path, server);
|
|
641
|
+
return this.handleAuthenticatedRequest(req, url, path, server, authContext);
|
|
572
642
|
}
|
|
573
643
|
|
|
574
644
|
/**
|
|
575
645
|
* Handle requests that have already passed auth and rate limiting.
|
|
576
646
|
*/
|
|
577
|
-
private async handleAuthenticatedRequest(
|
|
578
|
-
req: Request,
|
|
579
|
-
url: URL,
|
|
580
|
-
path: string,
|
|
581
|
-
server: ReturnType<typeof Bun.serve>,
|
|
582
|
-
): Promise<Response> {
|
|
647
|
+
private async handleAuthenticatedRequest(req: Request, url: URL, path: string, server: ReturnType<typeof Bun.serve>, authContext: AuthContext): Promise<Response> {
|
|
583
648
|
// Pairing registration (bearer-authenticated)
|
|
584
|
-
if (path ===
|
|
649
|
+
if (path === '/v1/pairing/register' && req.method === 'POST') {
|
|
650
|
+
const policyDenied = enforcePolicy('pairing/register', authContext);
|
|
651
|
+
if (policyDenied) return policyDenied;
|
|
585
652
|
return await handlePairingRegister(req, this.pairingContext);
|
|
586
653
|
}
|
|
587
654
|
|
|
@@ -600,79 +667,68 @@ export class RuntimeHttpServer {
|
|
|
600
667
|
}
|
|
601
668
|
|
|
602
669
|
// Cloud sharing endpoints
|
|
603
|
-
if (path ===
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
} catch (err) {
|
|
607
|
-
log.error({ err },
|
|
608
|
-
return httpError(
|
|
670
|
+
if (path === '/v1/apps/share' && req.method === 'POST') {
|
|
671
|
+
const policyDenied = enforcePolicy('apps/share', authContext);
|
|
672
|
+
if (policyDenied) return policyDenied;
|
|
673
|
+
try { return await handleShareApp(req); } catch (err) {
|
|
674
|
+
log.error({ err }, 'Runtime HTTP handler error sharing app');
|
|
675
|
+
return httpError('INTERNAL_ERROR', 'Internal server error', 500);
|
|
609
676
|
}
|
|
610
677
|
}
|
|
611
678
|
|
|
612
679
|
const sharedTokenMatch = path.match(/^\/v1\/apps\/shared\/([^/]+)$/);
|
|
613
680
|
if (sharedTokenMatch) {
|
|
614
681
|
const shareToken = sharedTokenMatch[1];
|
|
615
|
-
if (req.method ===
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
} catch (err) {
|
|
619
|
-
log.error(
|
|
620
|
-
|
|
621
|
-
"Runtime HTTP handler error downloading shared app",
|
|
622
|
-
);
|
|
623
|
-
return httpError("INTERNAL_ERROR", "Internal server error", 500);
|
|
682
|
+
if (req.method === 'GET') {
|
|
683
|
+
const policyDenied = enforcePolicy('apps/shared:GET', authContext);
|
|
684
|
+
if (policyDenied) return policyDenied;
|
|
685
|
+
try { return handleDownloadSharedApp(shareToken); } catch (err) {
|
|
686
|
+
log.error({ err, shareToken }, 'Runtime HTTP handler error downloading shared app');
|
|
687
|
+
return httpError('INTERNAL_ERROR', 'Internal server error', 500);
|
|
624
688
|
}
|
|
625
689
|
}
|
|
626
|
-
if (req.method ===
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
} catch (err) {
|
|
630
|
-
log.error(
|
|
631
|
-
|
|
632
|
-
"Runtime HTTP handler error deleting shared app",
|
|
633
|
-
);
|
|
634
|
-
return httpError("INTERNAL_ERROR", "Internal server error", 500);
|
|
690
|
+
if (req.method === 'DELETE') {
|
|
691
|
+
const policyDenied = enforcePolicy('apps/shared:DELETE', authContext);
|
|
692
|
+
if (policyDenied) return policyDenied;
|
|
693
|
+
try { return handleDeleteSharedApp(shareToken); } catch (err) {
|
|
694
|
+
log.error({ err, shareToken }, 'Runtime HTTP handler error deleting shared app');
|
|
695
|
+
return httpError('INTERNAL_ERROR', 'Internal server error', 500);
|
|
635
696
|
}
|
|
636
697
|
}
|
|
637
698
|
}
|
|
638
699
|
|
|
639
|
-
const sharedMetadataMatch = path.match(
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
try {
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
log.error(
|
|
647
|
-
{ err, shareToken: sharedMetadataMatch[1] },
|
|
648
|
-
"Runtime HTTP handler error getting shared app metadata",
|
|
649
|
-
);
|
|
650
|
-
return httpError("INTERNAL_ERROR", "Internal server error", 500);
|
|
700
|
+
const sharedMetadataMatch = path.match(/^\/v1\/apps\/shared\/([^/]+)\/metadata$/);
|
|
701
|
+
if (sharedMetadataMatch && req.method === 'GET') {
|
|
702
|
+
const policyDenied = enforcePolicy('apps/shared/metadata', authContext);
|
|
703
|
+
if (policyDenied) return policyDenied;
|
|
704
|
+
try { return handleGetSharedAppMetadata(sharedMetadataMatch[1]); } catch (err) {
|
|
705
|
+
log.error({ err, shareToken: sharedMetadataMatch[1] }, 'Runtime HTTP handler error getting shared app metadata');
|
|
706
|
+
return httpError('INTERNAL_ERROR', 'Internal server error', 500);
|
|
651
707
|
}
|
|
652
708
|
}
|
|
653
709
|
|
|
654
710
|
// Secret management endpoint
|
|
655
|
-
if (path ===
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
} catch (err) {
|
|
659
|
-
log.error({ err },
|
|
660
|
-
return httpError(
|
|
711
|
+
if (path === '/v1/secrets' && req.method === 'POST') {
|
|
712
|
+
const policyDenied = enforcePolicy('secrets', authContext);
|
|
713
|
+
if (policyDenied) return policyDenied;
|
|
714
|
+
try { return await handleAddSecret(req); } catch (err) {
|
|
715
|
+
log.error({ err }, 'Runtime HTTP handler error adding secret');
|
|
716
|
+
return httpError('INTERNAL_ERROR', 'Internal server error', 500);
|
|
661
717
|
}
|
|
662
718
|
}
|
|
663
|
-
if (path ===
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
} catch (err) {
|
|
667
|
-
log.error({ err },
|
|
668
|
-
return httpError(
|
|
719
|
+
if (path === '/v1/secrets' && req.method === 'DELETE') {
|
|
720
|
+
const policyDenied = enforcePolicy('secrets', authContext);
|
|
721
|
+
if (policyDenied) return policyDenied;
|
|
722
|
+
try { return await handleDeleteSecret(req); } catch (err) {
|
|
723
|
+
log.error({ err }, 'Runtime HTTP handler error deleting secret');
|
|
724
|
+
return httpError('INTERNAL_ERROR', 'Internal server error', 500);
|
|
669
725
|
}
|
|
670
726
|
}
|
|
671
727
|
|
|
672
728
|
// Runtime routes: /v1/<endpoint>
|
|
673
729
|
const routeMatch = path.match(/^\/v1\/(.+)$/);
|
|
674
730
|
if (routeMatch) {
|
|
675
|
-
return this.dispatchEndpoint(routeMatch[1], req, url, server);
|
|
731
|
+
return this.dispatchEndpoint(routeMatch[1], req, url, server, authContext);
|
|
676
732
|
}
|
|
677
733
|
|
|
678
734
|
return httpError("NOT_FOUND", "Not found", 404);
|
|
@@ -693,11 +749,15 @@ export class RuntimeHttpServer {
|
|
|
693
749
|
);
|
|
694
750
|
}
|
|
695
751
|
|
|
696
|
-
if (!isHttpAuthDisabled()
|
|
752
|
+
if (!isHttpAuthDisabled()) {
|
|
697
753
|
const wsUrl = new URL(req.url);
|
|
698
|
-
const token = wsUrl.searchParams.get(
|
|
699
|
-
if (!token
|
|
700
|
-
return httpError(
|
|
754
|
+
const token = wsUrl.searchParams.get('token');
|
|
755
|
+
if (!token) {
|
|
756
|
+
return httpError('UNAUTHORIZED', 'Unauthorized', 401);
|
|
757
|
+
}
|
|
758
|
+
const jwtResult = verifyToken(token, 'vellum-daemon');
|
|
759
|
+
if (!jwtResult.ok) {
|
|
760
|
+
return httpError('UNAUTHORIZED', 'Unauthorized', 401);
|
|
701
761
|
}
|
|
702
762
|
}
|
|
703
763
|
|
|
@@ -788,8 +848,23 @@ export class RuntimeHttpServer {
|
|
|
788
848
|
req: Request,
|
|
789
849
|
url: URL,
|
|
790
850
|
server: ReturnType<typeof Bun.serve>,
|
|
851
|
+
authContext: AuthContext,
|
|
791
852
|
): Promise<Response> {
|
|
792
853
|
const assistantId = DAEMON_INTERNAL_ASSISTANT_ID;
|
|
854
|
+
|
|
855
|
+
// Normalize parameterized endpoints to their base policy key so that
|
|
856
|
+
// routes like `calls/abc123` match the registered key `calls` and
|
|
857
|
+
// `attachments/abc123/content` matches `attachments/content`.
|
|
858
|
+
const normalizedEndpoint = normalizeEndpointForPolicy(endpoint, req.method);
|
|
859
|
+
|
|
860
|
+
// Enforce route-level scope/principal policy before invoking any handler.
|
|
861
|
+
// Try method-specific key first (e.g. "messages:POST"); fall back to the
|
|
862
|
+
// plain endpoint key only when no method-specific policy is registered.
|
|
863
|
+
const methodKey = `${normalizedEndpoint}:${req.method}`;
|
|
864
|
+
const policyKey = getPolicy(methodKey) ? methodKey : normalizedEndpoint;
|
|
865
|
+
const policyDenied = enforcePolicy(policyKey, authContext);
|
|
866
|
+
if (policyDenied) return policyDenied;
|
|
867
|
+
|
|
793
868
|
return withErrorHandling(endpoint, async () => {
|
|
794
869
|
if (endpoint === "health" && req.method === "GET") return handleHealth();
|
|
795
870
|
if (endpoint === "debug" && req.method === "GET") return handleDebug();
|
|
@@ -933,28 +1008,20 @@ export class RuntimeHttpServer {
|
|
|
933
1008
|
if (endpoint === "search" && req.method === "GET")
|
|
934
1009
|
return handleSearchConversations(url);
|
|
935
1010
|
|
|
936
|
-
if (endpoint ===
|
|
937
|
-
return await handleSendMessage(
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
approvalConversationGenerator: this.approvalConversationGenerator,
|
|
944
|
-
},
|
|
945
|
-
server,
|
|
946
|
-
);
|
|
1011
|
+
if (endpoint === 'messages' && req.method === 'POST') {
|
|
1012
|
+
return await handleSendMessage(req, {
|
|
1013
|
+
processMessage: this.processMessage,
|
|
1014
|
+
persistAndProcessMessage: this.persistAndProcessMessage,
|
|
1015
|
+
sendMessageDeps: this.sendMessageDeps,
|
|
1016
|
+
approvalConversationGenerator: this.approvalConversationGenerator,
|
|
1017
|
+
}, authContext);
|
|
947
1018
|
}
|
|
948
1019
|
|
|
949
1020
|
// Standalone approval endpoints — keyed by requestId, orthogonal to message sending
|
|
950
|
-
if (endpoint ===
|
|
951
|
-
|
|
952
|
-
if (endpoint ===
|
|
953
|
-
|
|
954
|
-
if (endpoint === "trust-rules" && req.method === "POST")
|
|
955
|
-
return await handleTrustRule(req, server);
|
|
956
|
-
if (endpoint === "pending-interactions" && req.method === "GET")
|
|
957
|
-
return handleListPendingInteractions(url, req, server);
|
|
1021
|
+
if (endpoint === 'confirm' && req.method === 'POST') return await handleConfirm(req, authContext);
|
|
1022
|
+
if (endpoint === 'secret' && req.method === 'POST') return await handleSecret(req, authContext);
|
|
1023
|
+
if (endpoint === 'trust-rules' && req.method === 'POST') return await handleTrustRule(req, authContext);
|
|
1024
|
+
if (endpoint === 'pending-interactions' && req.method === 'GET') return handleListPendingInteractions(url, authContext);
|
|
958
1025
|
|
|
959
1026
|
// Trust rule CRUD — standalone management (not approval-flow)
|
|
960
1027
|
if (endpoint === "trust-rules/manage" && req.method === "GET")
|
|
@@ -982,10 +1049,8 @@ export class RuntimeHttpServer {
|
|
|
982
1049
|
}
|
|
983
1050
|
|
|
984
1051
|
// Guardian action endpoints — deterministic button-based decisions
|
|
985
|
-
if (endpoint ===
|
|
986
|
-
|
|
987
|
-
if (endpoint === "guardian-actions/decision" && req.method === "POST")
|
|
988
|
-
return await handleGuardianActionDecision(req, server);
|
|
1052
|
+
if (endpoint === 'guardian-actions/pending' && req.method === 'GET') return handleGuardianActionsPending(url, authContext);
|
|
1053
|
+
if (endpoint === 'guardian-actions/decision' && req.method === 'POST') return await handleGuardianActionDecision(req, authContext);
|
|
989
1054
|
|
|
990
1055
|
// Contacts
|
|
991
1056
|
if (endpoint === "contacts" && req.method === "GET")
|
|
@@ -1080,17 +1145,8 @@ export class RuntimeHttpServer {
|
|
|
1080
1145
|
)
|
|
1081
1146
|
return await handleCancelOutbound(req);
|
|
1082
1147
|
|
|
1083
|
-
// Guardian vellum channel bootstrap
|
|
1084
|
-
|
|
1085
|
-
endpoint === "integrations/guardian/vellum/bootstrap" &&
|
|
1086
|
-
req.method === "POST"
|
|
1087
|
-
)
|
|
1088
|
-
return await handleGuardianBootstrap(req, server);
|
|
1089
|
-
if (
|
|
1090
|
-
endpoint === "integrations/guardian/vellum/refresh" &&
|
|
1091
|
-
req.method === "POST"
|
|
1092
|
-
)
|
|
1093
|
-
return await handleGuardianRefresh(req);
|
|
1148
|
+
// Guardian vellum channel bootstrap and refresh are handled before
|
|
1149
|
+
// JWT auth in routeRequest() — they are not dispatched here.
|
|
1094
1150
|
|
|
1095
1151
|
// Integrations — Twilio config
|
|
1096
1152
|
if (endpoint === "integrations/twilio/config" && req.method === "GET")
|
|
@@ -1187,19 +1243,9 @@ export class RuntimeHttpServer {
|
|
|
1187
1243
|
if (endpoint === "channels/conversation" && req.method === "DELETE")
|
|
1188
1244
|
return await handleDeleteConversation(req, assistantId);
|
|
1189
1245
|
|
|
1190
|
-
if (endpoint ===
|
|
1191
|
-
|
|
1192
|
-
return await handleChannelInbound(
|
|
1193
|
-
req,
|
|
1194
|
-
this.processMessage,
|
|
1195
|
-
this.bearerToken,
|
|
1196
|
-
assistantId,
|
|
1197
|
-
gatewayOriginSecret,
|
|
1198
|
-
this.approvalCopyGenerator,
|
|
1199
|
-
this.approvalConversationGenerator,
|
|
1200
|
-
this.guardianActionCopyGenerator,
|
|
1201
|
-
this.guardianFollowUpConversationGenerator,
|
|
1202
|
-
);
|
|
1246
|
+
if (endpoint === 'channels/inbound' && req.method === 'POST') {
|
|
1247
|
+
// Route policy enforces svc_gateway principal type.
|
|
1248
|
+
return await handleChannelInbound(req, this.processMessage, assistantId, this.approvalCopyGenerator, this.approvalConversationGenerator, this.guardianActionCopyGenerator, this.guardianFollowUpConversationGenerator);
|
|
1203
1249
|
}
|
|
1204
1250
|
|
|
1205
1251
|
if (endpoint === "channels/delivery-ack" && req.method === "POST")
|
|
@@ -1233,15 +1279,10 @@ export class RuntimeHttpServer {
|
|
|
1233
1279
|
}
|
|
1234
1280
|
}
|
|
1235
1281
|
|
|
1236
|
-
// Internal Twilio forwarding endpoints (gateway -> runtime)
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
req.
|
|
1240
|
-
) {
|
|
1241
|
-
const json = (await req.json()) as {
|
|
1242
|
-
params: Record<string, string>;
|
|
1243
|
-
originalUrl?: string;
|
|
1244
|
-
};
|
|
1282
|
+
// Internal Twilio forwarding endpoints (gateway -> runtime).
|
|
1283
|
+
// Route policy enforces svc_gateway principal type.
|
|
1284
|
+
if (endpoint === 'internal/twilio/voice-webhook' && req.method === 'POST') {
|
|
1285
|
+
const json = await req.json() as { params: Record<string, string>; originalUrl?: string };
|
|
1245
1286
|
const formBody = new URLSearchParams(json.params).toString();
|
|
1246
1287
|
const reconstructedUrl = json.originalUrl ?? req.url;
|
|
1247
1288
|
const fakeReq = new Request(reconstructedUrl, {
|
|
@@ -1252,8 +1293,8 @@ export class RuntimeHttpServer {
|
|
|
1252
1293
|
return await handleVoiceWebhook(fakeReq);
|
|
1253
1294
|
}
|
|
1254
1295
|
|
|
1255
|
-
if (endpoint ===
|
|
1256
|
-
const json =
|
|
1296
|
+
if (endpoint === 'internal/twilio/status' && req.method === 'POST') {
|
|
1297
|
+
const json = await req.json() as { params: Record<string, string> };
|
|
1257
1298
|
const formBody = new URLSearchParams(json.params).toString();
|
|
1258
1299
|
const fakeReq = new Request(req.url, {
|
|
1259
1300
|
method: "POST",
|
|
@@ -1263,11 +1304,8 @@ export class RuntimeHttpServer {
|
|
|
1263
1304
|
return await handleStatusCallback(fakeReq);
|
|
1264
1305
|
}
|
|
1265
1306
|
|
|
1266
|
-
if (
|
|
1267
|
-
|
|
1268
|
-
req.method === "POST"
|
|
1269
|
-
) {
|
|
1270
|
-
const json = (await req.json()) as { params: Record<string, string> };
|
|
1307
|
+
if (endpoint === 'internal/twilio/connect-action' && req.method === 'POST') {
|
|
1308
|
+
const json = await req.json() as { params: Record<string, string> };
|
|
1271
1309
|
const formBody = new URLSearchParams(json.params).toString();
|
|
1272
1310
|
const fakeReq = new Request(req.url, {
|
|
1273
1311
|
method: "POST",
|
|
@@ -1277,26 +1315,16 @@ export class RuntimeHttpServer {
|
|
|
1277
1315
|
return await handleConnectAction(fakeReq);
|
|
1278
1316
|
}
|
|
1279
1317
|
|
|
1280
|
-
if (endpoint ===
|
|
1281
|
-
|
|
1282
|
-
if (endpoint ===
|
|
1283
|
-
|
|
1284
|
-
if (endpoint ===
|
|
1285
|
-
return handleServeBrainGraphUI(this.bearerToken);
|
|
1286
|
-
if (endpoint === "home-base-ui" && req.method === "GET")
|
|
1287
|
-
return handleServeHomeBaseUI(this.bearerToken);
|
|
1288
|
-
if (endpoint === "events" && req.method === "GET")
|
|
1289
|
-
return handleSubscribeAssistantEvents(req, url, { server });
|
|
1318
|
+
if (endpoint === 'identity' && req.method === 'GET') return handleGetIdentity();
|
|
1319
|
+
if (endpoint === 'brain-graph' && req.method === 'GET') return handleGetBrainGraph();
|
|
1320
|
+
if (endpoint === 'brain-graph-ui' && req.method === 'GET') return handleServeBrainGraphUI(this.bearerToken);
|
|
1321
|
+
if (endpoint === 'home-base-ui' && req.method === 'GET') return handleServeHomeBaseUI(this.bearerToken);
|
|
1322
|
+
if (endpoint === 'events' && req.method === 'GET') return handleSubscribeAssistantEvents(req, url, { authContext });
|
|
1290
1323
|
|
|
1291
1324
|
// Internal OAuth callback endpoint (gateway -> runtime)
|
|
1292
|
-
if (endpoint ===
|
|
1293
|
-
const json =
|
|
1294
|
-
|
|
1295
|
-
code?: string;
|
|
1296
|
-
error?: string;
|
|
1297
|
-
};
|
|
1298
|
-
if (!json.state)
|
|
1299
|
-
return httpError("BAD_REQUEST", "Missing state parameter", 400);
|
|
1325
|
+
if (endpoint === 'internal/oauth/callback' && req.method === 'POST') {
|
|
1326
|
+
const json = await req.json() as { state: string; code?: string; error?: string };
|
|
1327
|
+
if (!json.state) return httpError('BAD_REQUEST', 'Missing state parameter', 400);
|
|
1300
1328
|
if (json.error) {
|
|
1301
1329
|
const consumed = consumeCallbackError(json.state, json.error);
|
|
1302
1330
|
return consumed
|
|
@@ -30,6 +30,8 @@ export type ApprovalCopyGenerator = (
|
|
|
30
30
|
export type ApprovalConversationDisposition =
|
|
31
31
|
| "keep_pending"
|
|
32
32
|
| "approve_once"
|
|
33
|
+
| "approve_10m"
|
|
34
|
+
| "approve_thread"
|
|
33
35
|
| "approve_always"
|
|
34
36
|
| "reject";
|
|
35
37
|
|
|
@@ -184,7 +186,15 @@ export interface RuntimeHttpServerOptions {
|
|
|
184
186
|
/** Dependencies for the POST /v1/messages queue-if-busy handler. */
|
|
185
187
|
sendMessageDeps?: SendMessageDeps;
|
|
186
188
|
/** Lookup an active session by ID (for surface actions). Returns undefined if not found. */
|
|
187
|
-
findSession?: (sessionId: string) =>
|
|
189
|
+
findSession?: (sessionId: string) =>
|
|
190
|
+
| {
|
|
191
|
+
handleSurfaceAction(
|
|
192
|
+
surfaceId: string,
|
|
193
|
+
actionId: string,
|
|
194
|
+
data?: Record<string, unknown>,
|
|
195
|
+
): void;
|
|
196
|
+
}
|
|
197
|
+
| undefined;
|
|
188
198
|
}
|
|
189
199
|
|
|
190
200
|
export interface RuntimeAttachmentMetadata {
|
|
@@ -12,10 +12,12 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import type { ChannelId } from '../channels/types.js';
|
|
15
|
+
import { buildIpcAuthContext } from '../daemon/ipc-handler.js';
|
|
15
16
|
import type { GuardianRuntimeContext } from '../daemon/session-runtime-assembly.js';
|
|
16
17
|
import { getActiveBinding } from '../memory/guardian-bindings.js';
|
|
17
18
|
import { getLogger } from '../util/logger.js';
|
|
18
19
|
import { DAEMON_INTERNAL_ASSISTANT_ID } from './assistant-scope.js';
|
|
20
|
+
import type { AuthContext } from './auth/types.js';
|
|
19
21
|
import { resolveGuardianContext } from './guardian-context-resolver.js';
|
|
20
22
|
import { ensureVellumGuardianBinding } from './guardian-vellum-migration.js';
|
|
21
23
|
|
|
@@ -78,3 +80,26 @@ export function resolveLocalIpcGuardianContext(
|
|
|
78
80
|
// so downstream consumers see the correct channel provenance.
|
|
79
81
|
return { ...guardianCtx, sourceChannel };
|
|
80
82
|
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Build an AuthContext for a local IPC connection.
|
|
86
|
+
*
|
|
87
|
+
* Produces the same AuthContext shape that HTTP routes receive from JWT
|
|
88
|
+
* verification, using the `ipc_v1` scope profile. The `actorPrincipalId`
|
|
89
|
+
* is populated from the vellum guardian binding when available, enabling
|
|
90
|
+
* downstream code to resolve guardian context using the same
|
|
91
|
+
* `authContext.actorPrincipalId` path as HTTP sessions.
|
|
92
|
+
*/
|
|
93
|
+
export function resolveLocalIpcAuthContext(sessionId: string): AuthContext {
|
|
94
|
+
const authContext = buildIpcAuthContext(sessionId);
|
|
95
|
+
|
|
96
|
+
// Enrich with the guardian principal ID when a vellum binding exists,
|
|
97
|
+
// so downstream guardian resolution can use authContext.actorPrincipalId.
|
|
98
|
+
const assistantId = DAEMON_INTERNAL_ASSISTANT_ID;
|
|
99
|
+
const binding = getActiveBinding(assistantId, 'vellum');
|
|
100
|
+
if (binding) {
|
|
101
|
+
return { ...authContext, actorPrincipalId: binding.guardianExternalUserId };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return authContext;
|
|
105
|
+
}
|
|
@@ -18,6 +18,7 @@ export interface ConfirmationDetails {
|
|
|
18
18
|
allowlistOptions: Array<{ label: string; description: string; pattern: string }>;
|
|
19
19
|
scopeOptions: Array<{ label: string; scope: string }>;
|
|
20
20
|
persistentDecisionsAllowed?: boolean;
|
|
21
|
+
temporaryOptionsAvailable?: Array<'allow_10m' | 'allow_thread'>;
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
export interface PendingInteraction {
|