@vellumai/assistant 0.4.3 → 0.4.4
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/.env.example +3 -0
- package/ARCHITECTURE.md +40 -3
- package/README.md +43 -35
- package/package.json +1 -1
- package/scripts/ipc/generate-swift.ts +1 -0
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +58 -120
- package/src/__tests__/actor-token-service.test.ts +1099 -0
- package/src/__tests__/agent-loop.test.ts +51 -0
- package/src/__tests__/approval-routes-http.test.ts +2 -0
- package/src/__tests__/assistant-events-sse-hardening.test.ts +7 -5
- package/src/__tests__/assistant-id-boundary-guard.test.ts +125 -0
- package/src/__tests__/call-controller.test.ts +49 -0
- package/src/__tests__/call-pointer-message-composer.test.ts +171 -0
- package/src/__tests__/call-pointer-messages.test.ts +93 -3
- package/src/__tests__/call-pointer-no-hardcoded-copy.guard.test.ts +42 -0
- package/src/__tests__/callback-handoff-copy.test.ts +186 -0
- package/src/__tests__/channel-approval-routes.test.ts +133 -12
- package/src/__tests__/channel-guardian.test.ts +0 -87
- package/src/__tests__/channel-readiness-service.test.ts +10 -16
- package/src/__tests__/checker.test.ts +33 -12
- package/src/__tests__/config-schema.test.ts +4 -0
- package/src/__tests__/confirmation-request-guardian-bridge.test.ts +410 -0
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +256 -0
- package/src/__tests__/conversation-routes.test.ts +12 -3
- package/src/__tests__/credential-security-invariants.test.ts +1 -1
- package/src/__tests__/daemon-server-session-init.test.ts +4 -0
- package/src/__tests__/guardian-actions-endpoint.test.ts +19 -14
- package/src/__tests__/guardian-dispatch.test.ts +8 -0
- package/src/__tests__/guardian-outbound-http.test.ts +4 -4
- package/src/__tests__/guardian-question-mode.test.ts +200 -0
- package/src/__tests__/guardian-routing-invariants.test.ts +178 -0
- package/src/__tests__/guardian-routing-state.test.ts +525 -0
- package/src/__tests__/handle-user-message-secret-resume.test.ts +2 -0
- package/src/__tests__/handlers-telegram-config.test.ts +0 -83
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +55 -0
- package/src/__tests__/headless-browser-navigate.test.ts +2 -0
- package/src/__tests__/ipc-snapshot.test.ts +18 -51
- package/src/__tests__/non-member-access-request.test.ts +131 -8
- package/src/__tests__/notification-decision-fallback.test.ts +129 -4
- package/src/__tests__/notification-decision-strategy.test.ts +62 -2
- package/src/__tests__/notification-guardian-path.test.ts +3 -0
- package/src/__tests__/recording-intent-handler.test.ts +1 -0
- package/src/__tests__/relay-server.test.ts +841 -39
- package/src/__tests__/send-endpoint-busy.test.ts +5 -0
- package/src/__tests__/session-agent-loop.test.ts +1 -0
- package/src/__tests__/session-confirmation-signals.test.ts +523 -0
- package/src/__tests__/session-init.benchmark.test.ts +0 -1
- package/src/__tests__/session-surfaces-task-progress.test.ts +1 -1
- package/src/__tests__/session-tool-setup-app-refresh.test.ts +81 -2
- package/src/__tests__/session-tool-setup-memory-scope.test.ts +1 -1
- package/src/__tests__/session-tool-setup-side-effect-flag.test.ts +1 -1
- package/src/__tests__/tool-executor.test.ts +21 -2
- package/src/__tests__/tool-grant-request-escalation.test.ts +333 -27
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +678 -0
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +1064 -0
- package/src/__tests__/twilio-config.test.ts +2 -13
- package/src/agent/loop.ts +1 -1
- package/src/approvals/guardian-decision-primitive.ts +10 -2
- package/src/approvals/guardian-request-resolvers.ts +128 -9
- package/src/calls/call-constants.ts +21 -0
- package/src/calls/call-controller.ts +9 -2
- package/src/calls/call-domain.ts +28 -7
- package/src/calls/call-pointer-message-composer.ts +154 -0
- package/src/calls/call-pointer-messages.ts +106 -27
- package/src/calls/guardian-dispatch.ts +4 -2
- package/src/calls/relay-server.ts +424 -12
- package/src/calls/twilio-config.ts +4 -11
- package/src/calls/twilio-routes.ts +1 -1
- package/src/calls/types.ts +3 -1
- package/src/cli.ts +5 -4
- package/src/config/bundled-skills/agentmail/SKILL.md +4 -0
- package/src/config/bundled-skills/app-builder/SKILL.md +146 -10
- package/src/config/bundled-skills/app-builder/TOOLS.json +1 -1
- package/src/config/bundled-skills/email-setup/SKILL.md +1 -1
- package/src/config/bundled-skills/google-oauth-setup/SKILL.md +105 -81
- package/src/config/bundled-skills/messaging/SKILL.md +61 -12
- package/src/config/bundled-skills/messaging/TOOLS.json +58 -0
- package/src/config/bundled-skills/messaging/tools/gmail-sender-digest.ts +6 -1
- package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +35 -0
- package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +52 -0
- package/src/config/bundled-skills/phone-calls/SKILL.md +30 -39
- package/src/config/bundled-skills/twitter/SKILL.md +3 -3
- package/src/config/bundled-skills/vercel-token-setup/SKILL.md +1 -0
- package/src/config/calls-schema.ts +24 -0
- package/src/config/env.ts +22 -0
- package/src/config/feature-flag-registry.json +8 -0
- package/src/config/schema.ts +2 -2
- package/src/config/skills.ts +11 -0
- package/src/config/system-prompt.ts +11 -1
- package/src/config/templates/SOUL.md +2 -0
- package/src/config/vellum-skills/sms-setup/SKILL.md +71 -82
- package/src/config/vellum-skills/trusted-contacts/SKILL.md +10 -9
- package/src/config/vellum-skills/twilio-setup/SKILL.md +88 -73
- package/src/daemon/call-pointer-generators.ts +59 -0
- package/src/daemon/computer-use-session.ts +2 -5
- package/src/daemon/handlers/apps.ts +76 -20
- package/src/daemon/handlers/config-channels.ts +5 -55
- package/src/daemon/handlers/config-inbox.ts +9 -3
- package/src/daemon/handlers/config-ingress.ts +28 -3
- package/src/daemon/handlers/config-telegram.ts +12 -0
- package/src/daemon/handlers/config.ts +2 -6
- package/src/daemon/handlers/pairing.ts +2 -0
- package/src/daemon/handlers/sessions.ts +48 -3
- package/src/daemon/handlers/shared.ts +17 -2
- package/src/daemon/ipc-contract/integrations.ts +1 -99
- package/src/daemon/ipc-contract/messages.ts +47 -1
- package/src/daemon/ipc-contract/notifications.ts +11 -0
- package/src/daemon/ipc-contract-inventory.json +2 -4
- package/src/daemon/lifecycle.ts +17 -0
- package/src/daemon/server.ts +14 -1
- package/src/daemon/session-agent-loop-handlers.ts +20 -0
- package/src/daemon/session-agent-loop.ts +22 -11
- package/src/daemon/session-lifecycle.ts +1 -1
- package/src/daemon/session-process.ts +11 -1
- package/src/daemon/session-runtime-assembly.ts +3 -0
- package/src/daemon/session-surfaces.ts +3 -2
- package/src/daemon/session.ts +88 -1
- package/src/daemon/tool-side-effects.ts +22 -0
- package/src/home-base/prebuilt/brain-graph.html +1483 -0
- package/src/home-base/prebuilt/index.html +40 -0
- package/src/inbound/platform-callback-registration.ts +157 -0
- package/src/memory/canonical-guardian-store.ts +1 -1
- package/src/memory/db-init.ts +4 -0
- package/src/memory/migrations/038-actor-token-records.ts +39 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/schema.ts +16 -0
- package/src/messaging/provider-types.ts +24 -0
- package/src/messaging/provider.ts +7 -0
- package/src/messaging/providers/gmail/adapter.ts +127 -0
- package/src/messaging/providers/sms/adapter.ts +40 -37
- package/src/notifications/adapters/macos.ts +45 -2
- package/src/notifications/broadcaster.ts +16 -0
- package/src/notifications/copy-composer.ts +39 -1
- package/src/notifications/decision-engine.ts +22 -9
- package/src/notifications/destination-resolver.ts +16 -2
- package/src/notifications/emit-signal.ts +16 -8
- package/src/notifications/guardian-question-mode.ts +419 -0
- package/src/notifications/signal.ts +14 -3
- package/src/permissions/checker.ts +13 -1
- package/src/permissions/prompter.ts +14 -0
- package/src/providers/anthropic/client.ts +20 -0
- package/src/providers/provider-send-message.ts +15 -3
- package/src/runtime/access-request-helper.ts +71 -1
- package/src/runtime/actor-token-service.ts +234 -0
- package/src/runtime/actor-token-store.ts +236 -0
- package/src/runtime/channel-approvals.ts +5 -3
- package/src/runtime/channel-readiness-service.ts +23 -64
- package/src/runtime/channel-readiness-types.ts +3 -4
- package/src/runtime/channel-retry-sweep.ts +4 -1
- package/src/runtime/confirmation-request-guardian-bridge.ts +197 -0
- package/src/runtime/guardian-action-followup-executor.ts +1 -1
- package/src/runtime/guardian-context-resolver.ts +82 -0
- package/src/runtime/guardian-outbound-actions.ts +0 -3
- package/src/runtime/guardian-reply-router.ts +67 -30
- package/src/runtime/guardian-vellum-migration.ts +57 -0
- package/src/runtime/http-server.ts +65 -12
- package/src/runtime/http-types.ts +13 -0
- package/src/runtime/invite-redemption-service.ts +8 -0
- package/src/runtime/local-actor-identity.ts +76 -0
- package/src/runtime/middleware/actor-token.ts +271 -0
- package/src/runtime/routes/approval-routes.ts +82 -7
- package/src/runtime/routes/brain-graph-routes.ts +222 -0
- package/src/runtime/routes/channel-readiness-routes.ts +71 -0
- package/src/runtime/routes/conversation-routes.ts +140 -52
- package/src/runtime/routes/events-routes.ts +20 -5
- package/src/runtime/routes/guardian-action-routes.ts +45 -3
- package/src/runtime/routes/guardian-approval-interception.ts +29 -0
- package/src/runtime/routes/guardian-bootstrap-routes.ts +145 -0
- package/src/runtime/routes/inbound-message-handler.ts +143 -2
- package/src/runtime/routes/integration-routes.ts +7 -15
- package/src/runtime/routes/pairing-routes.ts +163 -0
- package/src/runtime/routes/twilio-routes.ts +934 -0
- package/src/runtime/tool-grant-request-helper.ts +3 -1
- package/src/security/oauth2.ts +27 -2
- package/src/security/token-manager.ts +46 -10
- package/src/tools/browser/browser-execution.ts +4 -3
- package/src/tools/browser/browser-handoff.ts +10 -18
- package/src/tools/browser/browser-manager.ts +80 -25
- package/src/tools/browser/browser-screencast.ts +35 -119
- package/src/tools/permission-checker.ts +15 -4
- package/src/tools/tool-approval-handler.ts +242 -18
- package/src/__tests__/handlers-twilio-config.test.ts +0 -1928
- package/src/daemon/handlers/config-twilio.ts +0 -1082
|
@@ -25,6 +25,7 @@ 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 { bridgeConfirmationRequestToGuardian } from '../confirmation-request-guardian-bridge.js';
|
|
28
29
|
import { routeGuardianReply } from '../guardian-reply-router.js';
|
|
29
30
|
import { httpError } from '../http-errors.js';
|
|
30
31
|
import type {
|
|
@@ -35,48 +36,41 @@ import type {
|
|
|
35
36
|
RuntimeMessagePayload,
|
|
36
37
|
SendMessageDeps,
|
|
37
38
|
} from '../http-types.js';
|
|
39
|
+
import { resolveLocalIpcGuardianContext } from '../local-actor-identity.js';
|
|
40
|
+
import { type ServerWithRequestIP, verifyHttpActorTokenWithLocalFallback } from '../middleware/actor-token.js';
|
|
38
41
|
import * as pendingInteractions from '../pending-interactions.js';
|
|
39
42
|
|
|
40
43
|
const log = getLogger('conversation-routes');
|
|
41
44
|
|
|
42
45
|
const SUGGESTION_CACHE_MAX = 100;
|
|
43
46
|
|
|
44
|
-
function
|
|
47
|
+
function collectCanonicalGuardianRequestHintIds(
|
|
45
48
|
conversationId: string,
|
|
46
49
|
sourceChannel: string,
|
|
47
50
|
session: import('../../daemon/session.js').Session,
|
|
48
51
|
): string[] {
|
|
49
|
-
const
|
|
50
|
-
.getByConversation(conversationId)
|
|
51
|
-
.filter(
|
|
52
|
-
(interaction) =>
|
|
53
|
-
interaction.kind === 'confirmation'
|
|
54
|
-
&& interaction.session === session
|
|
55
|
-
&& session.hasPendingConfirmation(interaction.requestId),
|
|
56
|
-
)
|
|
57
|
-
.map((interaction) => interaction.requestId);
|
|
58
|
-
|
|
59
|
-
// Query both by destination conversation (via deliveries table) and by
|
|
60
|
-
// source conversation (direct field). For desktop/HTTP sessions these
|
|
61
|
-
// often overlap, but the Set dedup below handles that.
|
|
62
|
-
const pendingCanonicalRequestIds = [
|
|
52
|
+
const requests = [
|
|
63
53
|
...listPendingCanonicalGuardianRequestsByDestinationConversation(conversationId, sourceChannel)
|
|
64
|
-
.
|
|
65
|
-
.map((request) => request.id),
|
|
54
|
+
.map((request) => ({ id: request.id, kind: request.kind })),
|
|
66
55
|
...listCanonicalGuardianRequests({
|
|
67
56
|
status: 'pending',
|
|
68
57
|
conversationId,
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
58
|
+
}).map((request) => ({ id: request.id, kind: request.kind })),
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
const deduped = new Map<string, string>();
|
|
62
|
+
for (const request of requests) {
|
|
63
|
+
if (!deduped.has(request.id)) {
|
|
64
|
+
deduped.set(request.id, request.kind ?? '');
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return Array.from(deduped.entries())
|
|
69
|
+
.filter(([requestId, kind]) => kind !== 'tool_approval' || session.hasPendingConfirmation(requestId))
|
|
70
|
+
.map(([requestId]) => requestId);
|
|
77
71
|
}
|
|
78
72
|
|
|
79
|
-
async function
|
|
73
|
+
async function tryConsumeCanonicalGuardianReply(params: {
|
|
80
74
|
conversationId: string;
|
|
81
75
|
sourceChannel: string;
|
|
82
76
|
sourceInterface: string;
|
|
@@ -90,6 +84,8 @@ async function tryConsumeInlineApprovalReply(params: {
|
|
|
90
84
|
session: import('../../daemon/session.js').Session;
|
|
91
85
|
onEvent: (msg: ServerMessage) => void;
|
|
92
86
|
approvalConversationGenerator?: ApprovalConversationGenerator;
|
|
87
|
+
/** Verified actor identity from actor-token middleware. */
|
|
88
|
+
verifiedActorExternalUserId?: string;
|
|
93
89
|
}): Promise<{ consumed: boolean; messageId?: string }> {
|
|
94
90
|
const {
|
|
95
91
|
conversationId,
|
|
@@ -100,42 +96,57 @@ async function tryConsumeInlineApprovalReply(params: {
|
|
|
100
96
|
session,
|
|
101
97
|
onEvent,
|
|
102
98
|
approvalConversationGenerator,
|
|
99
|
+
verifiedActorExternalUserId,
|
|
103
100
|
} = params;
|
|
104
101
|
const trimmedContent = content.trim();
|
|
105
102
|
|
|
106
|
-
|
|
107
|
-
// We intentionally do not block on queue depth: after an auto-deny, users
|
|
108
|
-
// often retry with "approve"/"yes" while the queue is still draining, and
|
|
109
|
-
// requiring an empty queue can create a deny/retry cascade.
|
|
110
|
-
if (
|
|
111
|
-
!session.hasAnyPendingConfirmation()
|
|
112
|
-
|| trimmedContent.length === 0
|
|
113
|
-
) {
|
|
103
|
+
if (trimmedContent.length === 0) {
|
|
114
104
|
return { consumed: false };
|
|
115
105
|
}
|
|
116
106
|
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
return { consumed: false };
|
|
120
|
-
}
|
|
107
|
+
const pendingRequestHintIds = collectCanonicalGuardianRequestHintIds(conversationId, sourceChannel, session);
|
|
108
|
+
const pendingRequestIds = pendingRequestHintIds.length > 0 ? pendingRequestHintIds : undefined;
|
|
121
109
|
|
|
122
110
|
const routerResult = await routeGuardianReply({
|
|
123
111
|
messageText: trimmedContent,
|
|
124
112
|
channel: sourceChannel,
|
|
125
113
|
actor: {
|
|
126
|
-
externalUserId:
|
|
114
|
+
externalUserId: verifiedActorExternalUserId,
|
|
127
115
|
channel: sourceChannel,
|
|
128
|
-
|
|
116
|
+
// When a verified identity is available, disable the trusted bypass so
|
|
117
|
+
// that the identity-match checks in applyCanonicalGuardianDecision
|
|
118
|
+
// actually run. Only fall back to isTrusted when no verified identity
|
|
119
|
+
// was resolved (defensive — shouldn't happen for vellum since
|
|
120
|
+
// verification runs upstream).
|
|
121
|
+
isTrusted: !verifiedActorExternalUserId,
|
|
129
122
|
},
|
|
130
123
|
conversationId,
|
|
131
124
|
pendingRequestIds,
|
|
132
125
|
approvalConversationGenerator,
|
|
126
|
+
emissionContext: {
|
|
127
|
+
source: 'inline_nl',
|
|
128
|
+
decisionText: trimmedContent,
|
|
129
|
+
},
|
|
133
130
|
});
|
|
134
131
|
|
|
135
132
|
if (!routerResult.consumed || routerResult.type === 'nl_keep_pending') {
|
|
136
133
|
return { consumed: false };
|
|
137
134
|
}
|
|
138
135
|
|
|
136
|
+
// Success-path emissions (approved/denied) are handled centrally
|
|
137
|
+
// by handleConfirmationResponse (called via the resolver chain).
|
|
138
|
+
// However, stale/failed paths never reach handleConfirmationResponse,
|
|
139
|
+
// so we emit resolved_stale here for those cases.
|
|
140
|
+
if (routerResult.requestId && !routerResult.decisionApplied) {
|
|
141
|
+
session.emitConfirmationStateChanged({
|
|
142
|
+
sessionId: conversationId,
|
|
143
|
+
requestId: routerResult.requestId,
|
|
144
|
+
state: 'resolved_stale',
|
|
145
|
+
source: 'inline_nl',
|
|
146
|
+
decisionText: trimmedContent,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
139
150
|
// Decision has been applied — transcript persistence is best-effort.
|
|
140
151
|
// If DB writes fail, we still return consumed: true so the approval text
|
|
141
152
|
// is not re-processed as a new user turn.
|
|
@@ -336,7 +347,7 @@ function makeHubPublisher(
|
|
|
336
347
|
// via applyCanonicalGuardianDecision.
|
|
337
348
|
const guardianContext = session.guardianContext;
|
|
338
349
|
const sourceChannel = guardianContext?.sourceChannel ?? 'vellum';
|
|
339
|
-
createCanonicalGuardianRequest({
|
|
350
|
+
const canonicalRequest = createCanonicalGuardianRequest({
|
|
340
351
|
id: msg.requestId,
|
|
341
352
|
kind: 'tool_approval',
|
|
342
353
|
sourceType: resolveCanonicalRequestSourceType(sourceChannel),
|
|
@@ -350,6 +361,18 @@ function makeHubPublisher(
|
|
|
350
361
|
requestCode: generateCanonicalRequestCode(),
|
|
351
362
|
expiresAt: new Date(Date.now() + 5 * 60 * 1000).toISOString(),
|
|
352
363
|
});
|
|
364
|
+
|
|
365
|
+
// For trusted-contact sessions, bridge to guardian.question so the
|
|
366
|
+
// guardian gets notified and can approve via callback/request-code.
|
|
367
|
+
if (guardianContext) {
|
|
368
|
+
bridgeConfirmationRequestToGuardian({
|
|
369
|
+
canonicalRequest,
|
|
370
|
+
guardianContext,
|
|
371
|
+
conversationId,
|
|
372
|
+
toolName: msg.toolName,
|
|
373
|
+
assistantId: session.assistantId ?? DAEMON_INTERNAL_ASSISTANT_ID,
|
|
374
|
+
});
|
|
375
|
+
}
|
|
353
376
|
} else if (msg.type === 'secret_request') {
|
|
354
377
|
pendingInteractions.register(msg.requestId, {
|
|
355
378
|
session,
|
|
@@ -384,6 +407,7 @@ export async function handleSendMessage(
|
|
|
384
407
|
sendMessageDeps?: SendMessageDeps;
|
|
385
408
|
approvalConversationGenerator?: ApprovalConversationGenerator;
|
|
386
409
|
},
|
|
410
|
+
server: ServerWithRequestIP,
|
|
387
411
|
): Promise<Response> {
|
|
388
412
|
const body = await req.json() as {
|
|
389
413
|
conversationKey?: string;
|
|
@@ -441,31 +465,68 @@ export async function handleSendMessage(
|
|
|
441
465
|
|
|
442
466
|
// ── Queue-if-busy path (preferred when sendMessageDeps is wired) ────
|
|
443
467
|
if (deps.sendMessageDeps) {
|
|
468
|
+
// Vellum HTTP requests prefer actor-token identity. When absent (e.g. CLI
|
|
469
|
+
// bearer-auth only), fall back to local IPC identity resolution so
|
|
470
|
+
// bearer-authenticated local clients are not rejected.
|
|
471
|
+
const actorVerification = sourceChannel === 'vellum' ? verifyHttpActorTokenWithLocalFallback(req, server) : null;
|
|
472
|
+
if (actorVerification && !actorVerification.ok) {
|
|
473
|
+
return httpError(
|
|
474
|
+
actorVerification.status === 401 ? 'UNAUTHORIZED' : 'FORBIDDEN',
|
|
475
|
+
actorVerification.message,
|
|
476
|
+
actorVerification.status,
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
|
|
444
480
|
const smDeps = deps.sendMessageDeps;
|
|
445
481
|
const session = await smDeps.getOrCreateSession(mapping.conversationId);
|
|
446
|
-
//
|
|
447
|
-
//
|
|
448
|
-
|
|
482
|
+
// Resolve actor identity from the verified actor token. The token's
|
|
483
|
+
// guardianPrincipalId is matched against the vellum guardian binding
|
|
484
|
+
// through the standard trust pipeline.
|
|
485
|
+
if (actorVerification?.ok) {
|
|
486
|
+
session.setGuardianContext(actorVerification.guardianContext);
|
|
487
|
+
} else {
|
|
488
|
+
// Non-vellum channels through the HTTP API are still local
|
|
489
|
+
// authenticated requests. Resolve guardian context via the local
|
|
490
|
+
// identity pathway (vellum binding lookup) to preserve guardian
|
|
491
|
+
// trust. Falls back to a minimal guardian context if no binding
|
|
492
|
+
// exists (pre-bootstrap).
|
|
493
|
+
session.setGuardianContext(
|
|
494
|
+
resolveLocalIpcGuardianContext(sourceChannel) ?? { trustClass: 'guardian', sourceChannel },
|
|
495
|
+
);
|
|
496
|
+
}
|
|
449
497
|
const onEvent = makeHubPublisher(smDeps, mapping.conversationId, session);
|
|
498
|
+
// Route server-authoritative state signals (confirmation_state_changed,
|
|
499
|
+
// assistant_activity_state) to the SSE hub. Without this, these signals
|
|
500
|
+
// only travel through session.sendToClient, which is a no-op for
|
|
501
|
+
// socketless HTTP sessions.
|
|
502
|
+
session.setStateSignalListener(onEvent);
|
|
450
503
|
|
|
451
504
|
const attachments = hasAttachments
|
|
452
505
|
? smDeps.resolveAttachments(attachmentIds)
|
|
453
506
|
: [];
|
|
454
507
|
|
|
455
|
-
//
|
|
508
|
+
// Resolve the verified actor's external user ID for inline approval
|
|
509
|
+
// routing. Uses the guardianExternalUserId from the verified context
|
|
510
|
+
// (actor-token or local-fallback) rather than hardcoding undefined.
|
|
511
|
+
const verifiedActorExternalUserId = actorVerification?.ok
|
|
512
|
+
? actorVerification.guardianContext.guardianExternalUserId
|
|
513
|
+
: undefined;
|
|
514
|
+
|
|
515
|
+
// Try to consume the message as a canonical guardian approval/rejection reply.
|
|
456
516
|
// On failure, degrade to the existing queue/auto-deny path rather than
|
|
457
517
|
// surfacing a 500 — mirrors the IPC handler's catch-and-fallback.
|
|
458
518
|
try {
|
|
459
|
-
const inlineReplyResult = await
|
|
519
|
+
const inlineReplyResult = await tryConsumeCanonicalGuardianReply({
|
|
460
520
|
conversationId: mapping.conversationId,
|
|
461
521
|
sourceChannel,
|
|
462
522
|
sourceInterface,
|
|
463
523
|
content: content ?? '',
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
524
|
+
attachments,
|
|
525
|
+
session,
|
|
526
|
+
onEvent,
|
|
527
|
+
approvalConversationGenerator: deps.approvalConversationGenerator,
|
|
528
|
+
verifiedActorExternalUserId,
|
|
529
|
+
});
|
|
469
530
|
if (inlineReplyResult.consumed) {
|
|
470
531
|
return Response.json(
|
|
471
532
|
{ accepted: true, ...(inlineReplyResult.messageId ? { messageId: inlineReplyResult.messageId } : {}) },
|
|
@@ -480,6 +541,18 @@ export async function handleSendMessage(
|
|
|
480
541
|
// If a tool confirmation is pending, auto-deny it so the agent
|
|
481
542
|
// can finish the current turn and process this queued message.
|
|
482
543
|
if (session.hasAnyPendingConfirmation()) {
|
|
544
|
+
// Emit authoritative denial state for each pending request.
|
|
545
|
+
// The onStateSignal listener routes these to the SSE hub automatically.
|
|
546
|
+
for (const interaction of pendingInteractions.getByConversation(mapping.conversationId)) {
|
|
547
|
+
if (interaction.session === session && interaction.kind === 'confirmation') {
|
|
548
|
+
session.emitConfirmationStateChanged({
|
|
549
|
+
sessionId: mapping.conversationId,
|
|
550
|
+
requestId: interaction.requestId,
|
|
551
|
+
state: 'denied' as const,
|
|
552
|
+
source: 'auto_deny' as const,
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
}
|
|
483
556
|
session.denyAllPendingConfirmations();
|
|
484
557
|
pendingInteractions.removeBySession(session);
|
|
485
558
|
}
|
|
@@ -534,12 +607,27 @@ export async function handleSendMessage(
|
|
|
534
607
|
return httpError('SERVICE_UNAVAILABLE', 'Message processing not configured', 503);
|
|
535
608
|
}
|
|
536
609
|
|
|
610
|
+
// Require actor token for vellum channel requests on the legacy path too,
|
|
611
|
+
// with local IPC fallback for bearer-authenticated CLI clients.
|
|
612
|
+
const legacyActorVerification = sourceChannel === 'vellum' ? verifyHttpActorTokenWithLocalFallback(req, server) : null;
|
|
613
|
+
if (legacyActorVerification && !legacyActorVerification.ok) {
|
|
614
|
+
return httpError(
|
|
615
|
+
legacyActorVerification.status === 401 ? 'UNAUTHORIZED' : 'FORBIDDEN',
|
|
616
|
+
legacyActorVerification.message,
|
|
617
|
+
legacyActorVerification.status,
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
const guardianContext = legacyActorVerification?.ok
|
|
622
|
+
? legacyActorVerification.guardianContext
|
|
623
|
+
: resolveLocalIpcGuardianContext(sourceChannel) ?? { trustClass: 'guardian' as const, sourceChannel };
|
|
624
|
+
|
|
537
625
|
try {
|
|
538
626
|
const result = await processor(
|
|
539
627
|
mapping.conversationId,
|
|
540
628
|
content ?? '',
|
|
541
629
|
hasAttachments ? attachmentIds : undefined,
|
|
542
|
-
{ guardianContext
|
|
630
|
+
{ guardianContext },
|
|
543
631
|
sourceChannel,
|
|
544
632
|
sourceInterface,
|
|
545
633
|
);
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* GET /v1/events?conversationKey=...
|
|
5
5
|
*
|
|
6
|
-
*
|
|
6
|
+
* Bearer auth is enforced by RuntimeHttpServer before this handler is called.
|
|
7
|
+
* Actor-token identity verification (with local CLI fallback) is performed
|
|
8
|
+
* within this handler to bind the SSE stream to a verified actor identity.
|
|
7
9
|
* Subscribers receive all assistant events scoped to the given conversation.
|
|
8
10
|
*/
|
|
9
11
|
|
|
@@ -13,6 +15,7 @@ import type { AssistantEventSubscription } from '../assistant-event-hub.js';
|
|
|
13
15
|
import { AssistantEventHub,assistantEventHub } from '../assistant-event-hub.js';
|
|
14
16
|
import { DAEMON_INTERNAL_ASSISTANT_ID } from '../assistant-scope.js';
|
|
15
17
|
import { httpError } from '../http-errors.js';
|
|
18
|
+
import { type ServerWithRequestIP, verifyHttpActorTokenWithLocalFallback } from '../middleware/actor-token.js';
|
|
16
19
|
|
|
17
20
|
/** Keep-alive comment sent to idle clients every 30 s by default. */
|
|
18
21
|
const DEFAULT_HEARTBEAT_INTERVAL_MS = 30_000;
|
|
@@ -30,11 +33,23 @@ const DEFAULT_HEARTBEAT_INTERVAL_MS = 30_000;
|
|
|
30
33
|
export function handleSubscribeAssistantEvents(
|
|
31
34
|
req: Request,
|
|
32
35
|
url: URL,
|
|
33
|
-
options?:
|
|
34
|
-
hub?: AssistantEventHub;
|
|
35
|
-
heartbeatIntervalMs?: number;
|
|
36
|
-
},
|
|
36
|
+
options?:
|
|
37
|
+
| { hub?: AssistantEventHub; heartbeatIntervalMs?: number; skipActorVerification?: false; server: ServerWithRequestIP }
|
|
38
|
+
| { hub?: AssistantEventHub; heartbeatIntervalMs?: number; skipActorVerification: true },
|
|
37
39
|
): Response {
|
|
40
|
+
// Verify actor-token identity for vellum channel requests, with local
|
|
41
|
+
// CLI fallback for bearer-authenticated clients without X-Actor-Token.
|
|
42
|
+
if (options && !options.skipActorVerification) {
|
|
43
|
+
const actorVerification = verifyHttpActorTokenWithLocalFallback(req, options.server);
|
|
44
|
+
if (!actorVerification.ok) {
|
|
45
|
+
return httpError(
|
|
46
|
+
actorVerification.status === 401 ? 'UNAUTHORIZED' : 'FORBIDDEN',
|
|
47
|
+
actorVerification.message,
|
|
48
|
+
actorVerification.status,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
38
53
|
const conversationKey = url.searchParams.get('conversationKey');
|
|
39
54
|
if (!conversationKey) {
|
|
40
55
|
return httpError('BAD_REQUEST', 'conversationKey is required', 400);
|
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
*
|
|
4
4
|
* These endpoints let desktop clients fetch pending guardian prompts and
|
|
5
5
|
* submit button decisions without relying on text parsing.
|
|
6
|
+
*
|
|
7
|
+
* All guardian action endpoints require a valid actor token via the
|
|
8
|
+
* X-Actor-Token header (with local CLI fallback). Guardian decisions
|
|
9
|
+
* additionally verify the actor is the bound guardian.
|
|
6
10
|
*/
|
|
7
11
|
import {
|
|
8
12
|
applyCanonicalGuardianDecision,
|
|
@@ -16,6 +20,12 @@ import type { ApprovalAction } from '../channel-approval-types.js';
|
|
|
16
20
|
import type { GuardianDecisionPrompt } from '../guardian-decision-types.js';
|
|
17
21
|
import { buildDecisionActions } from '../guardian-decision-types.js';
|
|
18
22
|
import { httpError } from '../http-errors.js';
|
|
23
|
+
import {
|
|
24
|
+
isActorBoundGuardian,
|
|
25
|
+
isLocalFallbackBoundGuardian,
|
|
26
|
+
type ServerWithRequestIP,
|
|
27
|
+
verifyHttpActorTokenWithLocalFallback,
|
|
28
|
+
} from '../middleware/actor-token.js';
|
|
19
29
|
|
|
20
30
|
// ---------------------------------------------------------------------------
|
|
21
31
|
// GET /v1/guardian-actions/pending?conversationId=...
|
|
@@ -23,12 +33,22 @@ import { httpError } from '../http-errors.js';
|
|
|
23
33
|
|
|
24
34
|
/**
|
|
25
35
|
* List pending guardian decision prompts for a conversation.
|
|
36
|
+
* Requires a valid actor token.
|
|
26
37
|
*
|
|
27
38
|
* Returns guardian approval requests (from the channel guardian store) that
|
|
28
39
|
* are still pending, mapped to the GuardianDecisionPrompt shape so clients
|
|
29
40
|
* can render structured button UIs.
|
|
30
41
|
*/
|
|
31
|
-
export function handleGuardianActionsPending(req: Request): Response {
|
|
42
|
+
export function handleGuardianActionsPending(req: Request, server: ServerWithRequestIP): Response {
|
|
43
|
+
const tokenResult = verifyHttpActorTokenWithLocalFallback(req, server);
|
|
44
|
+
if (!tokenResult.ok) {
|
|
45
|
+
return httpError(
|
|
46
|
+
tokenResult.status === 401 ? 'UNAUTHORIZED' : 'FORBIDDEN',
|
|
47
|
+
tokenResult.message,
|
|
48
|
+
tokenResult.status,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
32
52
|
const url = new URL(req.url);
|
|
33
53
|
const conversationId = url.searchParams.get('conversationId');
|
|
34
54
|
|
|
@@ -46,12 +66,28 @@ export function handleGuardianActionsPending(req: Request): Response {
|
|
|
46
66
|
|
|
47
67
|
/**
|
|
48
68
|
* Submit a guardian action decision.
|
|
69
|
+
* Requires a valid actor token for a bound guardian.
|
|
49
70
|
*
|
|
50
71
|
* Routes all decisions through the unified canonical guardian decision
|
|
51
72
|
* primitive which handles CAS resolution, resolver dispatch, and grant
|
|
52
73
|
* minting.
|
|
53
74
|
*/
|
|
54
|
-
export async function handleGuardianActionDecision(req: Request): Promise<Response> {
|
|
75
|
+
export async function handleGuardianActionDecision(req: Request, server: ServerWithRequestIP): Promise<Response> {
|
|
76
|
+
const tokenResult = verifyHttpActorTokenWithLocalFallback(req, server);
|
|
77
|
+
if (!tokenResult.ok) {
|
|
78
|
+
return httpError(
|
|
79
|
+
tokenResult.status === 401 ? 'UNAUTHORIZED' : 'FORBIDDEN',
|
|
80
|
+
tokenResult.message,
|
|
81
|
+
tokenResult.status,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
const isBoundGuardian = tokenResult.claims
|
|
85
|
+
? isActorBoundGuardian(tokenResult.claims)
|
|
86
|
+
: isLocalFallbackBoundGuardian();
|
|
87
|
+
if (!isBoundGuardian) {
|
|
88
|
+
return httpError('FORBIDDEN', 'Actor is not the bound guardian for this channel', 403);
|
|
89
|
+
}
|
|
90
|
+
|
|
55
91
|
const body = await req.json() as {
|
|
56
92
|
requestId?: string;
|
|
57
93
|
action?: string;
|
|
@@ -82,11 +118,17 @@ export async function handleGuardianActionDecision(req: Request): Promise<Respon
|
|
|
82
118
|
}
|
|
83
119
|
}
|
|
84
120
|
|
|
121
|
+
// Resolve the actor's external user ID: from the token claims if present,
|
|
122
|
+
// otherwise from the vellum guardian binding (local fallback).
|
|
123
|
+
const actorExternalUserId = tokenResult.claims
|
|
124
|
+
? tokenResult.claims.guardianPrincipalId
|
|
125
|
+
: tokenResult.guardianContext.guardianExternalUserId;
|
|
126
|
+
|
|
85
127
|
const canonicalResult = await applyCanonicalGuardianDecision({
|
|
86
128
|
requestId,
|
|
87
129
|
action: action as ApprovalAction,
|
|
88
130
|
actorContext: {
|
|
89
|
-
externalUserId:
|
|
131
|
+
externalUserId: actorExternalUserId,
|
|
90
132
|
channel: 'vellum',
|
|
91
133
|
isTrusted: true,
|
|
92
134
|
},
|
|
@@ -741,6 +741,35 @@ export async function handleApprovalInterception(
|
|
|
741
741
|
}
|
|
742
742
|
return { handled: true, type: 'decision_applied' };
|
|
743
743
|
}
|
|
744
|
+
|
|
745
|
+
// Guard: non-guardian actors with a guardian binding must not self-approve
|
|
746
|
+
// even when no guardian approval row exists yet. The guardian approval
|
|
747
|
+
// row is created asynchronously when the approval prompt is delivered
|
|
748
|
+
// to the guardian. In the window between the pending confirmation being
|
|
749
|
+
// created (isInteractive=true) and the guardian approval row being
|
|
750
|
+
// persisted, any non-guardian actor could otherwise fall through to the
|
|
751
|
+
// standard conversational engine / legacy parser and resolve their own
|
|
752
|
+
// pending request via handleChannelDecision.
|
|
753
|
+
if (guardianCtx.trustClass !== 'guardian' && guardianCtx.guardianExternalUserId) {
|
|
754
|
+
log.info(
|
|
755
|
+
{ conversationId, externalChatId, guardianExternalUserId: guardianCtx.guardianExternalUserId },
|
|
756
|
+
'Blocking non-guardian self-approval: pending confirmation exists but guardian approval row not yet created',
|
|
757
|
+
);
|
|
758
|
+
try {
|
|
759
|
+
const pendingText = await composeApprovalMessageGenerative({
|
|
760
|
+
scenario: 'request_pending_guardian',
|
|
761
|
+
channel: sourceChannel,
|
|
762
|
+
}, {}, approvalCopyGenerator);
|
|
763
|
+
await deliverChannelReply(replyCallbackUrl, {
|
|
764
|
+
chatId: externalChatId,
|
|
765
|
+
text: pendingText,
|
|
766
|
+
assistantId,
|
|
767
|
+
}, bearerToken);
|
|
768
|
+
} catch (err) {
|
|
769
|
+
log.error({ err, conversationId }, 'Failed to deliver guardian-pending notice to non-guardian actor (pre-row guard)');
|
|
770
|
+
}
|
|
771
|
+
return { handled: true, type: 'assistant_turn' };
|
|
772
|
+
}
|
|
744
773
|
}
|
|
745
774
|
}
|
|
746
775
|
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /v1/integrations/guardian/vellum/bootstrap
|
|
3
|
+
*
|
|
4
|
+
* Idempotent bootstrap endpoint for the vellum guardian channel.
|
|
5
|
+
* Creates or confirms a guardianPrincipalId and channel='vellum'
|
|
6
|
+
* guardian binding, then mints and returns an actor token bound
|
|
7
|
+
* to (assistantId, guardianPrincipalId, deviceId).
|
|
8
|
+
*
|
|
9
|
+
* Only the hashed token is persisted.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { createHash } from 'node:crypto';
|
|
13
|
+
|
|
14
|
+
import { v4 as uuid } from 'uuid';
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
createBinding,
|
|
18
|
+
getActiveBinding,
|
|
19
|
+
} from '../../memory/guardian-bindings.js';
|
|
20
|
+
import { getLogger } from '../../util/logger.js';
|
|
21
|
+
import { mintActorToken } from '../actor-token-service.js';
|
|
22
|
+
import {
|
|
23
|
+
createActorTokenRecord,
|
|
24
|
+
revokeByDeviceBinding,
|
|
25
|
+
} from '../actor-token-store.js';
|
|
26
|
+
import { DAEMON_INTERNAL_ASSISTANT_ID } from '../assistant-scope.js';
|
|
27
|
+
import { httpError } from '../http-errors.js';
|
|
28
|
+
import type { ServerWithRequestIP } from '../middleware/actor-token.js';
|
|
29
|
+
|
|
30
|
+
const log = getLogger('guardian-bootstrap');
|
|
31
|
+
|
|
32
|
+
/** Hash a device ID for storage (same pattern as approved-devices-store). */
|
|
33
|
+
function hashDeviceId(deviceId: string): string {
|
|
34
|
+
return createHash('sha256').update(deviceId).digest('hex');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Ensure a guardianPrincipalId exists for the vellum channel.
|
|
39
|
+
* If a binding already exists, returns the existing guardianExternalUserId
|
|
40
|
+
* as the principal. Otherwise creates a new binding with a fresh principal.
|
|
41
|
+
*/
|
|
42
|
+
function ensureGuardianPrincipal(assistantId: string): {
|
|
43
|
+
guardianPrincipalId: string;
|
|
44
|
+
isNew: boolean;
|
|
45
|
+
} {
|
|
46
|
+
const existing = getActiveBinding(assistantId, 'vellum');
|
|
47
|
+
if (existing) {
|
|
48
|
+
return { guardianPrincipalId: existing.guardianExternalUserId, isNew: false };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Mint a new principal ID for the vellum channel
|
|
52
|
+
const guardianPrincipalId = `vellum-principal-${uuid()}`;
|
|
53
|
+
|
|
54
|
+
createBinding({
|
|
55
|
+
assistantId,
|
|
56
|
+
channel: 'vellum',
|
|
57
|
+
guardianExternalUserId: guardianPrincipalId,
|
|
58
|
+
guardianDeliveryChatId: 'local',
|
|
59
|
+
verifiedVia: 'bootstrap',
|
|
60
|
+
metadataJson: JSON.stringify({ bootstrappedAt: Date.now() }),
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
log.info({ assistantId, guardianPrincipalId }, 'Created vellum guardian principal via bootstrap');
|
|
64
|
+
return { guardianPrincipalId, isNew: true };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Loopback addresses — used to gate the bootstrap endpoint to local-only. */
|
|
68
|
+
const LOOPBACK_ADDRESSES = new Set(['127.0.0.1', '::1', '::ffff:127.0.0.1']);
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Handle POST /v1/integrations/guardian/vellum/bootstrap
|
|
72
|
+
*
|
|
73
|
+
* Body: { platform: 'macos', deviceId: string }
|
|
74
|
+
* Returns: { guardianPrincipalId, actorToken, isNew }
|
|
75
|
+
*
|
|
76
|
+
* This endpoint is loopback-only (macOS local use only). iOS devices
|
|
77
|
+
* obtain actor tokens exclusively through the QR pairing flow.
|
|
78
|
+
*/
|
|
79
|
+
export async function handleGuardianBootstrap(req: Request, server: ServerWithRequestIP): Promise<Response> {
|
|
80
|
+
// Reject proxied requests — bootstrap is local-only
|
|
81
|
+
if (req.headers.get('x-forwarded-for')) {
|
|
82
|
+
return httpError('FORBIDDEN', 'Bootstrap endpoint is local-only', 403);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Reject non-loopback peers
|
|
86
|
+
const peerIp = server.requestIP(req)?.address;
|
|
87
|
+
if (!peerIp || !LOOPBACK_ADDRESSES.has(peerIp)) {
|
|
88
|
+
return httpError('FORBIDDEN', 'Bootstrap endpoint is local-only', 403);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
const body = await req.json() as Record<string, unknown>;
|
|
93
|
+
const platform = typeof body.platform === 'string' ? body.platform.trim() : '';
|
|
94
|
+
const deviceId = typeof body.deviceId === 'string' ? body.deviceId.trim() : '';
|
|
95
|
+
|
|
96
|
+
if (!platform || !deviceId) {
|
|
97
|
+
return httpError('BAD_REQUEST', 'Missing required fields: platform, deviceId', 400);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (platform !== 'macos') {
|
|
101
|
+
return httpError('BAD_REQUEST', 'Invalid platform. Bootstrap is macOS-only; iOS uses QR pairing.', 400);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const assistantId = DAEMON_INTERNAL_ASSISTANT_ID;
|
|
105
|
+
const { guardianPrincipalId, isNew } = ensureGuardianPrincipal(assistantId);
|
|
106
|
+
const hashedDeviceId = hashDeviceId(deviceId);
|
|
107
|
+
|
|
108
|
+
// Revoke any existing active tokens for this device binding
|
|
109
|
+
// so we maintain one-active-token-per-device
|
|
110
|
+
revokeByDeviceBinding(assistantId, guardianPrincipalId, hashedDeviceId);
|
|
111
|
+
|
|
112
|
+
// Mint a new actor token
|
|
113
|
+
const { token, tokenHash, claims } = mintActorToken({
|
|
114
|
+
assistantId,
|
|
115
|
+
platform,
|
|
116
|
+
deviceId,
|
|
117
|
+
guardianPrincipalId,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Store only the hash
|
|
121
|
+
createActorTokenRecord({
|
|
122
|
+
tokenHash,
|
|
123
|
+
assistantId,
|
|
124
|
+
guardianPrincipalId,
|
|
125
|
+
hashedDeviceId,
|
|
126
|
+
platform,
|
|
127
|
+
issuedAt: claims.iat,
|
|
128
|
+
expiresAt: claims.exp,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
log.info(
|
|
132
|
+
{ assistantId, platform, guardianPrincipalId, isNew },
|
|
133
|
+
'Guardian bootstrap completed',
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
return Response.json({
|
|
137
|
+
guardianPrincipalId,
|
|
138
|
+
actorToken: token,
|
|
139
|
+
isNew,
|
|
140
|
+
});
|
|
141
|
+
} catch (err) {
|
|
142
|
+
log.error({ err }, 'Guardian bootstrap failed');
|
|
143
|
+
return httpError('INTERNAL_ERROR', 'Internal server error', 500);
|
|
144
|
+
}
|
|
145
|
+
}
|