@vellumai/assistant 0.4.2 → 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 +124 -10
- package/README.md +43 -35
- package/docs/trusted-contact-access.md +20 -0
- 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__/access-request-decision.test.ts +0 -1
- 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 +415 -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__/call-routes-http.test.ts +0 -25
- 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 -86
- 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 +6 -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__/deterministic-verification-control-plane.test.ts +0 -1
- package/src/__tests__/guardian-actions-endpoint.test.ts +39 -13
- package/src/__tests__/guardian-dispatch.test.ts +8 -0
- package/src/__tests__/guardian-outbound-http.test.ts +4 -5
- 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__/inbound-invite-redemption.test.ts +0 -1
- package/src/__tests__/ingress-routes-http.test.ts +55 -0
- package/src/__tests__/ipc-snapshot.test.ts +18 -51
- package/src/__tests__/non-member-access-request.test.ts +159 -9
- package/src/__tests__/notification-decision-fallback.test.ts +129 -4
- package/src/__tests__/notification-decision-strategy.test.ts +106 -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 +1475 -33
- 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 -2
- package/src/__tests__/session-runtime-assembly.test.ts +4 -1
- package/src/__tests__/session-surfaces-task-progress.test.ts +44 -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__/trusted-contact-lifecycle-notifications.test.ts +11 -1
- package/src/__tests__/trusted-contact-multichannel.test.ts +0 -1
- package/src/__tests__/trusted-contact-verification.test.ts +0 -1
- package/src/__tests__/twilio-config.test.ts +2 -13
- package/src/__tests__/twilio-routes.test.ts +4 -3
- package/src/__tests__/update-bulletin.test.ts +0 -1
- package/src/agent/loop.ts +1 -1
- package/src/approvals/guardian-decision-primitive.ts +12 -3
- package/src/approvals/guardian-request-resolvers.ts +169 -11
- package/src/calls/call-constants.ts +29 -0
- package/src/calls/call-controller.ts +11 -3
- package/src/calls/call-domain.ts +33 -11
- 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 +921 -112
- package/src/calls/twilio-config.ts +4 -11
- package/src/calls/twilio-routes.ts +4 -6
- package/src/calls/types.ts +3 -1
- package/src/calls/voice-session-bridge.ts +4 -3
- package/src/cli/core-commands.ts +7 -4
- 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 +309 -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 +215 -0
- package/src/config/calls-schema.ts +36 -0
- package/src/config/env.ts +22 -0
- package/src/config/feature-flag-registry.json +8 -8
- 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 +8 -1
- 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 +9 -61
- package/src/daemon/handlers/config-inbox.ts +11 -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/index.ts +2 -1
- package/src/daemon/handlers/pairing.ts +2 -0
- package/src/daemon/handlers/publish.ts +11 -46
- package/src/daemon/handlers/sessions.ts +59 -5
- package/src/daemon/handlers/shared.ts +17 -2
- package/src/daemon/ipc-contract/apps.ts +1 -0
- package/src/daemon/ipc-contract/inbox.ts +4 -0
- package/src/daemon/ipc-contract/integrations.ts +1 -97
- 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 +16 -2
- package/src/daemon/session-agent-loop-handlers.ts +20 -0
- package/src/daemon/session-agent-loop.ts +24 -12
- package/src/daemon/session-lifecycle.ts +1 -1
- package/src/daemon/session-process.ts +11 -1
- package/src/daemon/session-runtime-assembly.ts +6 -1
- package/src/daemon/session-surfaces.ts +32 -3
- 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/conversation-crud.ts +2 -1
- package/src/memory/conversation-title-service.ts +16 -2
- package/src/memory/db-init.ts +8 -0
- package/src/memory/delivery-crud.ts +2 -1
- package/src/memory/guardian-action-store.ts +2 -1
- package/src/memory/guardian-approvals.ts +3 -2
- package/src/memory/ingress-invite-store.ts +12 -2
- package/src/memory/ingress-member-store.ts +4 -3
- package/src/memory/migrations/038-actor-token-records.ts +39 -0
- package/src/memory/migrations/124-voice-invite-display-metadata.ts +14 -0
- package/src/memory/migrations/index.ts +2 -0
- package/src/memory/schema.ts +26 -5
- 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 +50 -2
- package/src/notifications/decision-engine.ts +22 -9
- package/src/notifications/destination-resolver.ts +16 -2
- package/src/notifications/emit-signal.ts +18 -9
- 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 +82 -4
- package/src/runtime/actor-token-service.ts +234 -0
- package/src/runtime/actor-token-store.ts +236 -0
- package/src/runtime/actor-trust-resolver.ts +2 -2
- package/src/runtime/assistant-scope.ts +10 -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 +5 -7
- package/src/runtime/guardian-reply-router.ts +67 -30
- package/src/runtime/guardian-vellum-migration.ts +57 -0
- package/src/runtime/http-server.ts +75 -31
- package/src/runtime/http-types.ts +13 -0
- package/src/runtime/ingress-service.ts +14 -0
- package/src/runtime/invite-redemption-service.ts +10 -1
- package/src/runtime/local-actor-identity.ts +76 -0
- package/src/runtime/middleware/actor-token.ts +271 -0
- package/src/runtime/middleware/twilio-validation.ts +2 -4
- package/src/runtime/routes/approval-routes.ts +82 -7
- package/src/runtime/routes/brain-graph-routes.ts +222 -0
- package/src/runtime/routes/call-routes.ts +2 -1
- package/src/runtime/routes/channel-readiness-routes.ts +71 -0
- package/src/runtime/routes/channel-route-shared.ts +3 -3
- package/src/runtime/routes/conversation-attention-routes.ts +2 -1
- package/src/runtime/routes/conversation-routes.ts +142 -53
- package/src/runtime/routes/events-routes.ts +22 -8
- 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-conversation.ts +4 -3
- package/src/runtime/routes/inbound-message-handler.ts +147 -5
- package/src/runtime/routes/ingress-routes.ts +2 -0
- 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/calls/call-start.ts +2 -1
- package/src/tools/permission-checker.ts +15 -4
- package/src/tools/terminal/parser.ts +12 -0
- package/src/tools/tool-approval-handler.ts +244 -19
- package/src/workspace/git-service.ts +19 -0
- package/src/__tests__/handlers-twilio-config.test.ts +0 -1928
- package/src/daemon/handlers/config-twilio.ts +0 -1082
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Route handlers for the brain graph visualization endpoint.
|
|
3
|
+
*
|
|
4
|
+
* Queries the memory database to return a knowledge graph shaped for brain-lobe
|
|
5
|
+
* visualization, with entities mapped to brain regions based on their type.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readFileSync } from 'node:fs';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
|
|
11
|
+
import { count } from 'drizzle-orm';
|
|
12
|
+
|
|
13
|
+
import { getDb } from '../../memory/db.js';
|
|
14
|
+
import { memoryEntities, memoryEntityRelations, memoryItems } from '../../memory/schema.js';
|
|
15
|
+
import { resolveBundledDir } from '../../util/bundled-asset.js';
|
|
16
|
+
|
|
17
|
+
function getLobeRegion(entityType: string): string {
|
|
18
|
+
switch (entityType) {
|
|
19
|
+
case 'person':
|
|
20
|
+
case 'organization':
|
|
21
|
+
return 'right-social';
|
|
22
|
+
case 'project':
|
|
23
|
+
case 'company':
|
|
24
|
+
return 'left-planning';
|
|
25
|
+
case 'tool':
|
|
26
|
+
return 'left-technical';
|
|
27
|
+
case 'concept':
|
|
28
|
+
return 'right-creative';
|
|
29
|
+
case 'location':
|
|
30
|
+
return 'right-spatial';
|
|
31
|
+
default:
|
|
32
|
+
return 'center';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function getEntityColor(entityType: string): string {
|
|
37
|
+
switch (entityType) {
|
|
38
|
+
case 'person':
|
|
39
|
+
return '#22c55e';
|
|
40
|
+
case 'project':
|
|
41
|
+
return '#f97316';
|
|
42
|
+
case 'tool':
|
|
43
|
+
return '#06b6d4';
|
|
44
|
+
case 'company':
|
|
45
|
+
return '#a855f7';
|
|
46
|
+
case 'organization':
|
|
47
|
+
return '#a855f7';
|
|
48
|
+
case 'concept':
|
|
49
|
+
return '#eab308';
|
|
50
|
+
case 'location':
|
|
51
|
+
return '#14b8a6';
|
|
52
|
+
default:
|
|
53
|
+
return '#94a3b8';
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function getMemoryKindColor(kind: string): string {
|
|
58
|
+
switch (kind) {
|
|
59
|
+
case 'profile':
|
|
60
|
+
return '#8b5cf6';
|
|
61
|
+
case 'preference':
|
|
62
|
+
return '#3b82f6';
|
|
63
|
+
case 'constraint':
|
|
64
|
+
return '#ef4444';
|
|
65
|
+
case 'instruction':
|
|
66
|
+
return '#f59e0b';
|
|
67
|
+
case 'style':
|
|
68
|
+
return '#ec4899';
|
|
69
|
+
default:
|
|
70
|
+
return '#94a3b8';
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function handleGetBrainGraph(): Response {
|
|
75
|
+
try {
|
|
76
|
+
const db = getDb();
|
|
77
|
+
|
|
78
|
+
const entityRows = db
|
|
79
|
+
.select({
|
|
80
|
+
id: memoryEntities.id,
|
|
81
|
+
name: memoryEntities.name,
|
|
82
|
+
type: memoryEntities.type,
|
|
83
|
+
mentionCount: memoryEntities.mentionCount,
|
|
84
|
+
firstSeenAt: memoryEntities.firstSeenAt,
|
|
85
|
+
lastSeenAt: memoryEntities.lastSeenAt,
|
|
86
|
+
})
|
|
87
|
+
.from(memoryEntities)
|
|
88
|
+
.all();
|
|
89
|
+
|
|
90
|
+
const relationRows = db
|
|
91
|
+
.select({
|
|
92
|
+
sourceEntityId: memoryEntityRelations.sourceEntityId,
|
|
93
|
+
targetEntityId: memoryEntityRelations.targetEntityId,
|
|
94
|
+
relation: memoryEntityRelations.relation,
|
|
95
|
+
})
|
|
96
|
+
.from(memoryEntityRelations)
|
|
97
|
+
.all();
|
|
98
|
+
|
|
99
|
+
const kindCountRows = db
|
|
100
|
+
.select({
|
|
101
|
+
kind: memoryItems.kind,
|
|
102
|
+
count: count(),
|
|
103
|
+
})
|
|
104
|
+
.from(memoryItems)
|
|
105
|
+
.groupBy(memoryItems.kind)
|
|
106
|
+
.all();
|
|
107
|
+
|
|
108
|
+
const entities = entityRows.map((entity) => ({
|
|
109
|
+
id: entity.id,
|
|
110
|
+
name: entity.name,
|
|
111
|
+
type: entity.type,
|
|
112
|
+
lobeRegion: getLobeRegion(entity.type),
|
|
113
|
+
color: getEntityColor(entity.type),
|
|
114
|
+
mentionCount: entity.mentionCount,
|
|
115
|
+
firstSeenAt: entity.firstSeenAt,
|
|
116
|
+
lastSeenAt: entity.lastSeenAt,
|
|
117
|
+
}));
|
|
118
|
+
|
|
119
|
+
const relations = relationRows.map((rel) => ({
|
|
120
|
+
sourceId: rel.sourceEntityId,
|
|
121
|
+
targetId: rel.targetEntityId,
|
|
122
|
+
relation: rel.relation,
|
|
123
|
+
}));
|
|
124
|
+
|
|
125
|
+
const memorySummary = kindCountRows.map((row) => ({
|
|
126
|
+
kind: row.kind,
|
|
127
|
+
count: row.count,
|
|
128
|
+
color: getMemoryKindColor(row.kind),
|
|
129
|
+
}));
|
|
130
|
+
|
|
131
|
+
const totalKnowledgeCount = memorySummary.reduce((sum, entry) => sum + entry.count, 0);
|
|
132
|
+
|
|
133
|
+
return Response.json({
|
|
134
|
+
entities,
|
|
135
|
+
relations,
|
|
136
|
+
memorySummary,
|
|
137
|
+
totalKnowledgeCount,
|
|
138
|
+
generatedAt: new Date().toISOString(),
|
|
139
|
+
});
|
|
140
|
+
} catch (err) {
|
|
141
|
+
return Response.json(
|
|
142
|
+
{ error: 'Failed to generate brain graph', detail: err instanceof Error ? err.message : String(err) },
|
|
143
|
+
{ status: 500 },
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function handleServeHomeBaseUI(bearerToken?: string): Response {
|
|
149
|
+
try {
|
|
150
|
+
const prebuiltDir = resolveBundledDir(
|
|
151
|
+
import.meta.dirname ?? __dirname,
|
|
152
|
+
'../../home-base/prebuilt',
|
|
153
|
+
'prebuilt',
|
|
154
|
+
);
|
|
155
|
+
let html = readFileSync(join(prebuiltDir, 'index.html'), 'utf-8');
|
|
156
|
+
if (bearerToken) {
|
|
157
|
+
const escapedToken = bearerToken
|
|
158
|
+
.replace(/&/g, '&')
|
|
159
|
+
.replace(/"/g, '"')
|
|
160
|
+
.replace(/</g, '<')
|
|
161
|
+
.replace(/>/g, '>');
|
|
162
|
+
html = html.replace(
|
|
163
|
+
'</head>',
|
|
164
|
+
` <meta name="api-token" content="${escapedToken}">\n</head>`,
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
return new Response(html, {
|
|
168
|
+
headers: { 'Content-Type': 'text/html; charset=utf-8' },
|
|
169
|
+
});
|
|
170
|
+
} catch (err) {
|
|
171
|
+
return Response.json(
|
|
172
|
+
{ error: 'Home Base UI not available', detail: err instanceof Error ? err.message : String(err) },
|
|
173
|
+
{ status: 500 },
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function handleServeBrainGraphUI(bearerToken?: string): Response {
|
|
179
|
+
try {
|
|
180
|
+
const prebuiltDir = resolveBundledDir(
|
|
181
|
+
import.meta.dirname ?? __dirname,
|
|
182
|
+
'../../home-base/prebuilt',
|
|
183
|
+
'prebuilt',
|
|
184
|
+
);
|
|
185
|
+
let html = readFileSync(join(prebuiltDir, 'brain-graph.html'), 'utf-8');
|
|
186
|
+
if (bearerToken) {
|
|
187
|
+
// Inject token as a meta tag for client-side fetch authentication.
|
|
188
|
+
// HTML-escape the token value to guard against injection if the token
|
|
189
|
+
// comes from an environment variable with special characters.
|
|
190
|
+
const escapedToken = bearerToken
|
|
191
|
+
.replace(/&/g, '&')
|
|
192
|
+
.replace(/"/g, '"')
|
|
193
|
+
.replace(/</g, '<')
|
|
194
|
+
.replace(/>/g, '>');
|
|
195
|
+
html = html.replace(
|
|
196
|
+
'</head>',
|
|
197
|
+
` <meta name="api-token" content="${escapedToken}">\n</head>`,
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
// CSP permits the CDN sources required by D3.js and Three.js.
|
|
201
|
+
// 'unsafe-eval' is needed by Three.js's shader compilation path.
|
|
202
|
+
const csp = [
|
|
203
|
+
"default-src 'self'",
|
|
204
|
+
"script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.jsdelivr.net https://d3js.org",
|
|
205
|
+
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
|
|
206
|
+
"font-src 'self' https://fonts.gstatic.com",
|
|
207
|
+
"connect-src 'self'",
|
|
208
|
+
"img-src 'self' data:",
|
|
209
|
+
].join('; ');
|
|
210
|
+
return new Response(html, {
|
|
211
|
+
headers: {
|
|
212
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
213
|
+
'Content-Security-Policy': csp,
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
} catch (err) {
|
|
217
|
+
return Response.json(
|
|
218
|
+
{ error: 'Brain graph UI not available', detail: err instanceof Error ? err.message : String(err) },
|
|
219
|
+
{ status: 500 },
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import { answerCall, cancelCall, getCallStatus, relayInstruction,startCall } from '../../calls/call-domain.js';
|
|
12
12
|
import { getConfig } from '../../config/loader.js';
|
|
13
13
|
import { VALID_CALLER_IDENTITY_MODES } from '../../config/schema.js';
|
|
14
|
+
import { DAEMON_INTERNAL_ASSISTANT_ID } from '../assistant-scope.js';
|
|
14
15
|
import { httpError, httpErrorCodeFromStatus } from '../http-errors.js';
|
|
15
16
|
|
|
16
17
|
// ── Idempotency cache ─────────────────────────────────────────────────────────
|
|
@@ -41,7 +42,7 @@ function pruneIdempotencyCache(): void {
|
|
|
41
42
|
* Optional `idempotencyKey`: if supplied, duplicate requests with the same key
|
|
42
43
|
* within 5 minutes return the cached 201 response without starting a second call.
|
|
43
44
|
*/
|
|
44
|
-
export async function handleStartCall(req: Request, assistantId: string =
|
|
45
|
+
export async function handleStartCall(req: Request, assistantId: string = DAEMON_INTERNAL_ASSISTANT_ID): Promise<Response> {
|
|
45
46
|
if (!getConfig().calls.enabled) {
|
|
46
47
|
return httpError('FORBIDDEN', 'Calls feature is disabled via configuration. Set calls.enabled to true to use this feature.', 403);
|
|
47
48
|
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Route handlers for channel readiness endpoints.
|
|
3
|
+
*
|
|
4
|
+
* GET /v1/channels/readiness — get channel readiness snapshots
|
|
5
|
+
* POST /v1/channels/readiness/refresh — invalidate cache and refresh readiness
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ChannelId } from '../../channels/types.js';
|
|
9
|
+
import { getReadinessService } from '../../daemon/handlers/config-channels.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* GET /v1/channels/readiness
|
|
13
|
+
*
|
|
14
|
+
* Query params: channel? (optional ChannelId), includeRemote? (optional boolean)
|
|
15
|
+
*/
|
|
16
|
+
export async function handleGetChannelReadiness(url: URL): Promise<Response> {
|
|
17
|
+
const channel = (url.searchParams.get('channel') as ChannelId | null) ?? undefined;
|
|
18
|
+
const includeRemote = url.searchParams.get('includeRemote') === 'true';
|
|
19
|
+
|
|
20
|
+
const service = getReadinessService();
|
|
21
|
+
const snapshots = await service.getReadiness(channel, includeRemote);
|
|
22
|
+
|
|
23
|
+
return Response.json({
|
|
24
|
+
success: true,
|
|
25
|
+
snapshots: snapshots.map((s) => ({
|
|
26
|
+
channel: s.channel,
|
|
27
|
+
ready: s.ready,
|
|
28
|
+
checkedAt: s.checkedAt,
|
|
29
|
+
stale: s.stale,
|
|
30
|
+
reasons: s.reasons,
|
|
31
|
+
localChecks: s.localChecks,
|
|
32
|
+
remoteChecks: s.remoteChecks,
|
|
33
|
+
})),
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* POST /v1/channels/readiness/refresh
|
|
39
|
+
*
|
|
40
|
+
* Body: { channel?: ChannelId, includeRemote?: boolean }
|
|
41
|
+
*/
|
|
42
|
+
export async function handleRefreshChannelReadiness(req: Request): Promise<Response> {
|
|
43
|
+
const body = (await req.json().catch(() => ({}))) as {
|
|
44
|
+
channel?: ChannelId;
|
|
45
|
+
includeRemote?: boolean;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const service = getReadinessService();
|
|
49
|
+
|
|
50
|
+
// Invalidate cache before fetching
|
|
51
|
+
if (body.channel) {
|
|
52
|
+
service.invalidateChannel(body.channel);
|
|
53
|
+
} else {
|
|
54
|
+
service.invalidateAll();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const snapshots = await service.getReadiness(body.channel, body.includeRemote);
|
|
58
|
+
|
|
59
|
+
return Response.json({
|
|
60
|
+
success: true,
|
|
61
|
+
snapshots: snapshots.map((s) => ({
|
|
62
|
+
channel: s.channel,
|
|
63
|
+
ready: s.ready,
|
|
64
|
+
checkedAt: s.checkedAt,
|
|
65
|
+
stale: s.stale,
|
|
66
|
+
reasons: s.reasons,
|
|
67
|
+
localChecks: s.localChecks,
|
|
68
|
+
remoteChecks: s.remoteChecks,
|
|
69
|
+
})),
|
|
70
|
+
});
|
|
71
|
+
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { timingSafeEqual } from 'node:crypto';
|
|
5
5
|
|
|
6
6
|
import type { ChannelId } from '../../channels/types.js';
|
|
7
|
-
import {
|
|
7
|
+
import { DAEMON_INTERNAL_ASSISTANT_ID } from '../assistant-scope.js';
|
|
8
8
|
import type {
|
|
9
9
|
ApprovalAction,
|
|
10
10
|
ApprovalDecisionResult,
|
|
@@ -15,8 +15,8 @@ export type { ActorTrustClass, DenialReason, GuardianContext } from '../guardian
|
|
|
15
15
|
export { toGuardianRuntimeContext } from '../guardian-context-resolver.js';
|
|
16
16
|
|
|
17
17
|
/** Canonicalize assistantId for channel ingress paths. */
|
|
18
|
-
export function canonicalChannelAssistantId(
|
|
19
|
-
return
|
|
18
|
+
export function canonicalChannelAssistantId(_assistantId: string): string {
|
|
19
|
+
return DAEMON_INTERNAL_ASSISTANT_ID;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
// ---------------------------------------------------------------------------
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
} from '../../memory/conversation-attention-store.js';
|
|
11
11
|
import * as conversationStore from '../../memory/conversation-store.js';
|
|
12
12
|
import { truncate } from '../../util/truncate.js';
|
|
13
|
+
import { DAEMON_INTERNAL_ASSISTANT_ID } from '../assistant-scope.js';
|
|
13
14
|
import { httpError } from '../http-errors.js';
|
|
14
15
|
|
|
15
16
|
export function handleListConversationAttention(url: URL): Response {
|
|
@@ -27,7 +28,7 @@ export function handleListConversationAttention(url: URL): Response {
|
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
const attentionStates = listConversationAttention({
|
|
30
|
-
assistantId:
|
|
31
|
+
assistantId: DAEMON_INTERNAL_ASSISTANT_ID,
|
|
31
32
|
state: stateParam as AttentionFilterState,
|
|
32
33
|
sourceChannel: channel,
|
|
33
34
|
source: sourceParam !== 'all' ? sourceParam : undefined,
|
|
@@ -24,6 +24,8 @@ import { getConfiguredProvider } from '../../providers/provider-send-message.js'
|
|
|
24
24
|
import type { Provider } from '../../providers/types.js';
|
|
25
25
|
import { getLogger } from '../../util/logger.js';
|
|
26
26
|
import { buildAssistantEvent } from '../assistant-event.js';
|
|
27
|
+
import { DAEMON_INTERNAL_ASSISTANT_ID } from '../assistant-scope.js';
|
|
28
|
+
import { bridgeConfirmationRequestToGuardian } from '../confirmation-request-guardian-bridge.js';
|
|
27
29
|
import { routeGuardianReply } from '../guardian-reply-router.js';
|
|
28
30
|
import { httpError } from '../http-errors.js';
|
|
29
31
|
import type {
|
|
@@ -34,48 +36,41 @@ import type {
|
|
|
34
36
|
RuntimeMessagePayload,
|
|
35
37
|
SendMessageDeps,
|
|
36
38
|
} from '../http-types.js';
|
|
39
|
+
import { resolveLocalIpcGuardianContext } from '../local-actor-identity.js';
|
|
40
|
+
import { type ServerWithRequestIP, verifyHttpActorTokenWithLocalFallback } from '../middleware/actor-token.js';
|
|
37
41
|
import * as pendingInteractions from '../pending-interactions.js';
|
|
38
42
|
|
|
39
43
|
const log = getLogger('conversation-routes');
|
|
40
44
|
|
|
41
45
|
const SUGGESTION_CACHE_MAX = 100;
|
|
42
46
|
|
|
43
|
-
function
|
|
47
|
+
function collectCanonicalGuardianRequestHintIds(
|
|
44
48
|
conversationId: string,
|
|
45
49
|
sourceChannel: string,
|
|
46
50
|
session: import('../../daemon/session.js').Session,
|
|
47
51
|
): string[] {
|
|
48
|
-
const
|
|
49
|
-
.getByConversation(conversationId)
|
|
50
|
-
.filter(
|
|
51
|
-
(interaction) =>
|
|
52
|
-
interaction.kind === 'confirmation'
|
|
53
|
-
&& interaction.session === session
|
|
54
|
-
&& session.hasPendingConfirmation(interaction.requestId),
|
|
55
|
-
)
|
|
56
|
-
.map((interaction) => interaction.requestId);
|
|
57
|
-
|
|
58
|
-
// Query both by destination conversation (via deliveries table) and by
|
|
59
|
-
// source conversation (direct field). For desktop/HTTP sessions these
|
|
60
|
-
// often overlap, but the Set dedup below handles that.
|
|
61
|
-
const pendingCanonicalRequestIds = [
|
|
52
|
+
const requests = [
|
|
62
53
|
...listPendingCanonicalGuardianRequestsByDestinationConversation(conversationId, sourceChannel)
|
|
63
|
-
.
|
|
64
|
-
.map((request) => request.id),
|
|
54
|
+
.map((request) => ({ id: request.id, kind: request.kind })),
|
|
65
55
|
...listCanonicalGuardianRequests({
|
|
66
56
|
status: 'pending',
|
|
67
57
|
conversationId,
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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);
|
|
76
71
|
}
|
|
77
72
|
|
|
78
|
-
async function
|
|
73
|
+
async function tryConsumeCanonicalGuardianReply(params: {
|
|
79
74
|
conversationId: string;
|
|
80
75
|
sourceChannel: string;
|
|
81
76
|
sourceInterface: string;
|
|
@@ -89,6 +84,8 @@ async function tryConsumeInlineApprovalReply(params: {
|
|
|
89
84
|
session: import('../../daemon/session.js').Session;
|
|
90
85
|
onEvent: (msg: ServerMessage) => void;
|
|
91
86
|
approvalConversationGenerator?: ApprovalConversationGenerator;
|
|
87
|
+
/** Verified actor identity from actor-token middleware. */
|
|
88
|
+
verifiedActorExternalUserId?: string;
|
|
92
89
|
}): Promise<{ consumed: boolean; messageId?: string }> {
|
|
93
90
|
const {
|
|
94
91
|
conversationId,
|
|
@@ -99,42 +96,57 @@ async function tryConsumeInlineApprovalReply(params: {
|
|
|
99
96
|
session,
|
|
100
97
|
onEvent,
|
|
101
98
|
approvalConversationGenerator,
|
|
99
|
+
verifiedActorExternalUserId,
|
|
102
100
|
} = params;
|
|
103
101
|
const trimmedContent = content.trim();
|
|
104
102
|
|
|
105
|
-
|
|
106
|
-
// We intentionally do not block on queue depth: after an auto-deny, users
|
|
107
|
-
// often retry with "approve"/"yes" while the queue is still draining, and
|
|
108
|
-
// requiring an empty queue can create a deny/retry cascade.
|
|
109
|
-
if (
|
|
110
|
-
!session.hasAnyPendingConfirmation()
|
|
111
|
-
|| trimmedContent.length === 0
|
|
112
|
-
) {
|
|
103
|
+
if (trimmedContent.length === 0) {
|
|
113
104
|
return { consumed: false };
|
|
114
105
|
}
|
|
115
106
|
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
return { consumed: false };
|
|
119
|
-
}
|
|
107
|
+
const pendingRequestHintIds = collectCanonicalGuardianRequestHintIds(conversationId, sourceChannel, session);
|
|
108
|
+
const pendingRequestIds = pendingRequestHintIds.length > 0 ? pendingRequestHintIds : undefined;
|
|
120
109
|
|
|
121
110
|
const routerResult = await routeGuardianReply({
|
|
122
111
|
messageText: trimmedContent,
|
|
123
112
|
channel: sourceChannel,
|
|
124
113
|
actor: {
|
|
125
|
-
externalUserId:
|
|
114
|
+
externalUserId: verifiedActorExternalUserId,
|
|
126
115
|
channel: sourceChannel,
|
|
127
|
-
|
|
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,
|
|
128
122
|
},
|
|
129
123
|
conversationId,
|
|
130
124
|
pendingRequestIds,
|
|
131
125
|
approvalConversationGenerator,
|
|
126
|
+
emissionContext: {
|
|
127
|
+
source: 'inline_nl',
|
|
128
|
+
decisionText: trimmedContent,
|
|
129
|
+
},
|
|
132
130
|
});
|
|
133
131
|
|
|
134
132
|
if (!routerResult.consumed || routerResult.type === 'nl_keep_pending') {
|
|
135
133
|
return { consumed: false };
|
|
136
134
|
}
|
|
137
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
|
+
|
|
138
150
|
// Decision has been applied — transcript persistence is best-effort.
|
|
139
151
|
// If DB writes fail, we still return consumed: true so the approval text
|
|
140
152
|
// is not re-processed as a new user turn.
|
|
@@ -335,7 +347,7 @@ function makeHubPublisher(
|
|
|
335
347
|
// via applyCanonicalGuardianDecision.
|
|
336
348
|
const guardianContext = session.guardianContext;
|
|
337
349
|
const sourceChannel = guardianContext?.sourceChannel ?? 'vellum';
|
|
338
|
-
createCanonicalGuardianRequest({
|
|
350
|
+
const canonicalRequest = createCanonicalGuardianRequest({
|
|
339
351
|
id: msg.requestId,
|
|
340
352
|
kind: 'tool_approval',
|
|
341
353
|
sourceType: resolveCanonicalRequestSourceType(sourceChannel),
|
|
@@ -349,6 +361,18 @@ function makeHubPublisher(
|
|
|
349
361
|
requestCode: generateCanonicalRequestCode(),
|
|
350
362
|
expiresAt: new Date(Date.now() + 5 * 60 * 1000).toISOString(),
|
|
351
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
|
+
}
|
|
352
376
|
} else if (msg.type === 'secret_request') {
|
|
353
377
|
pendingInteractions.register(msg.requestId, {
|
|
354
378
|
session,
|
|
@@ -363,7 +387,7 @@ function makeHubPublisher(
|
|
|
363
387
|
? (msg as { sessionId: string }).sessionId
|
|
364
388
|
: undefined;
|
|
365
389
|
const resolvedSessionId = msgSessionId ?? conversationId;
|
|
366
|
-
const event = buildAssistantEvent(
|
|
390
|
+
const event = buildAssistantEvent(DAEMON_INTERNAL_ASSISTANT_ID, msg, resolvedSessionId);
|
|
367
391
|
hubChain = (async () => {
|
|
368
392
|
await hubChain;
|
|
369
393
|
try {
|
|
@@ -383,6 +407,7 @@ export async function handleSendMessage(
|
|
|
383
407
|
sendMessageDeps?: SendMessageDeps;
|
|
384
408
|
approvalConversationGenerator?: ApprovalConversationGenerator;
|
|
385
409
|
},
|
|
410
|
+
server: ServerWithRequestIP,
|
|
386
411
|
): Promise<Response> {
|
|
387
412
|
const body = await req.json() as {
|
|
388
413
|
conversationKey?: string;
|
|
@@ -440,31 +465,68 @@ export async function handleSendMessage(
|
|
|
440
465
|
|
|
441
466
|
// ── Queue-if-busy path (preferred when sendMessageDeps is wired) ────
|
|
442
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
|
+
|
|
443
480
|
const smDeps = deps.sendMessageDeps;
|
|
444
481
|
const session = await smDeps.getOrCreateSession(mapping.conversationId);
|
|
445
|
-
//
|
|
446
|
-
//
|
|
447
|
-
|
|
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
|
+
}
|
|
448
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);
|
|
449
503
|
|
|
450
504
|
const attachments = hasAttachments
|
|
451
505
|
? smDeps.resolveAttachments(attachmentIds)
|
|
452
506
|
: [];
|
|
453
507
|
|
|
454
|
-
//
|
|
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.
|
|
455
516
|
// On failure, degrade to the existing queue/auto-deny path rather than
|
|
456
517
|
// surfacing a 500 — mirrors the IPC handler's catch-and-fallback.
|
|
457
518
|
try {
|
|
458
|
-
const inlineReplyResult = await
|
|
519
|
+
const inlineReplyResult = await tryConsumeCanonicalGuardianReply({
|
|
459
520
|
conversationId: mapping.conversationId,
|
|
460
521
|
sourceChannel,
|
|
461
522
|
sourceInterface,
|
|
462
523
|
content: content ?? '',
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
524
|
+
attachments,
|
|
525
|
+
session,
|
|
526
|
+
onEvent,
|
|
527
|
+
approvalConversationGenerator: deps.approvalConversationGenerator,
|
|
528
|
+
verifiedActorExternalUserId,
|
|
529
|
+
});
|
|
468
530
|
if (inlineReplyResult.consumed) {
|
|
469
531
|
return Response.json(
|
|
470
532
|
{ accepted: true, ...(inlineReplyResult.messageId ? { messageId: inlineReplyResult.messageId } : {}) },
|
|
@@ -479,6 +541,18 @@ export async function handleSendMessage(
|
|
|
479
541
|
// If a tool confirmation is pending, auto-deny it so the agent
|
|
480
542
|
// can finish the current turn and process this queued message.
|
|
481
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
|
+
}
|
|
482
556
|
session.denyAllPendingConfirmations();
|
|
483
557
|
pendingInteractions.removeBySession(session);
|
|
484
558
|
}
|
|
@@ -533,12 +607,27 @@ export async function handleSendMessage(
|
|
|
533
607
|
return httpError('SERVICE_UNAVAILABLE', 'Message processing not configured', 503);
|
|
534
608
|
}
|
|
535
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
|
+
|
|
536
625
|
try {
|
|
537
626
|
const result = await processor(
|
|
538
627
|
mapping.conversationId,
|
|
539
628
|
content ?? '',
|
|
540
629
|
hasAttachments ? attachmentIds : undefined,
|
|
541
|
-
{ guardianContext
|
|
630
|
+
{ guardianContext },
|
|
542
631
|
sourceChannel,
|
|
543
632
|
sourceInterface,
|
|
544
633
|
);
|