@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
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type {
|
|
9
|
+
ArchiveResult,
|
|
9
10
|
ConnectionInfo,
|
|
10
11
|
Conversation,
|
|
11
12
|
HistoryOptions,
|
|
@@ -13,6 +14,7 @@ import type {
|
|
|
13
14
|
Message,
|
|
14
15
|
SearchOptions,
|
|
15
16
|
SearchResult,
|
|
17
|
+
SenderDigestResult,
|
|
16
18
|
SendOptions,
|
|
17
19
|
SendResult,
|
|
18
20
|
} from './provider-types.js';
|
|
@@ -38,6 +40,11 @@ export interface MessagingProvider {
|
|
|
38
40
|
getThreadReplies?(token: string, conversationId: string, threadId: string, options?: HistoryOptions): Promise<Message[]>;
|
|
39
41
|
markRead?(token: string, conversationId: string, messageId?: string): Promise<void>;
|
|
40
42
|
|
|
43
|
+
/** Scan messages and group by sender for bulk cleanup (e.g. newsletter decluttering). */
|
|
44
|
+
senderDigest?(token: string, query: string, options?: { maxMessages?: number; maxSenders?: number }): Promise<SenderDigestResult>;
|
|
45
|
+
/** Archive messages matching a search query. */
|
|
46
|
+
archiveByQuery?(token: string, query: string): Promise<ArchiveResult>;
|
|
47
|
+
|
|
41
48
|
/**
|
|
42
49
|
* Override the default credential check used by getConnectedProviders().
|
|
43
50
|
* When present, the registry calls this instead of looking for
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import type { MessagingProvider } from '../../provider.js';
|
|
9
9
|
import type {
|
|
10
|
+
ArchiveResult,
|
|
10
11
|
ConnectionInfo,
|
|
11
12
|
Conversation,
|
|
12
13
|
HistoryOptions,
|
|
@@ -14,6 +15,8 @@ import type {
|
|
|
14
15
|
Message,
|
|
15
16
|
SearchOptions,
|
|
16
17
|
SearchResult,
|
|
18
|
+
SenderDigestEntry,
|
|
19
|
+
SenderDigestResult,
|
|
17
20
|
SendOptions,
|
|
18
21
|
SendResult,
|
|
19
22
|
} from '../../provider-types.js';
|
|
@@ -191,4 +194,128 @@ export const gmailMessagingProvider: MessagingProvider = {
|
|
|
191
194
|
if (!messageId) return;
|
|
192
195
|
await gmail.modifyMessage(token, messageId, { removeLabelIds: ['UNREAD'] });
|
|
193
196
|
},
|
|
197
|
+
|
|
198
|
+
async senderDigest(token: string, query: string, options?: { maxMessages?: number; maxSenders?: number }): Promise<SenderDigestResult> {
|
|
199
|
+
const maxMessages = Math.min(options?.maxMessages ?? 500, 2000);
|
|
200
|
+
const maxSenders = options?.maxSenders ?? 30;
|
|
201
|
+
const maxIdsPerSender = 1000;
|
|
202
|
+
|
|
203
|
+
const allMessageIds: string[] = [];
|
|
204
|
+
let pageToken: string | undefined;
|
|
205
|
+
|
|
206
|
+
while (allMessageIds.length < maxMessages) {
|
|
207
|
+
const pageSize = Math.min(100, maxMessages - allMessageIds.length);
|
|
208
|
+
const listResp = await gmail.listMessages(token, query, pageSize, pageToken);
|
|
209
|
+
const ids = (listResp.messages ?? []).map((m) => m.id);
|
|
210
|
+
if (ids.length === 0) break;
|
|
211
|
+
allMessageIds.push(...ids);
|
|
212
|
+
pageToken = listResp.nextPageToken ?? undefined;
|
|
213
|
+
if (!pageToken) break;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (allMessageIds.length === 0) {
|
|
217
|
+
return { senders: [], totalScanned: 0, queryUsed: query };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const messages = await gmail.batchGetMessages(token, allMessageIds, 'metadata', [
|
|
221
|
+
'From', 'List-Unsubscribe',
|
|
222
|
+
]);
|
|
223
|
+
|
|
224
|
+
const senderMap = new Map<string, {
|
|
225
|
+
displayName: string; email: string; messageCount: number;
|
|
226
|
+
hasUnsubscribe: boolean; newestMessageId: string;
|
|
227
|
+
newestUnsubscribableMessageId: string | null; newestUnsubscribableEpoch: number;
|
|
228
|
+
messageIds: string[]; hasMore: boolean;
|
|
229
|
+
}>();
|
|
230
|
+
|
|
231
|
+
for (const msg of messages) {
|
|
232
|
+
const headers = msg.payload?.headers ?? [];
|
|
233
|
+
const fromHeader = headers.find((h) => h.name.toLowerCase() === 'from')?.value ?? '';
|
|
234
|
+
const listUnsub = headers.find((h) => h.name.toLowerCase() === 'list-unsubscribe')?.value;
|
|
235
|
+
|
|
236
|
+
const match = fromHeader.match(/^(.+?)\s*<([^>]+)>$/);
|
|
237
|
+
const email = match ? match[2].toLowerCase() : fromHeader.trim().toLowerCase();
|
|
238
|
+
const displayName = match ? match[1].replace(/^["']|["']$/g, '').trim() : '';
|
|
239
|
+
if (!email) continue;
|
|
240
|
+
|
|
241
|
+
let agg = senderMap.get(email);
|
|
242
|
+
if (!agg) {
|
|
243
|
+
agg = {
|
|
244
|
+
displayName, email, messageCount: 0, hasUnsubscribe: false,
|
|
245
|
+
newestMessageId: msg.id, newestUnsubscribableMessageId: null,
|
|
246
|
+
newestUnsubscribableEpoch: 0, messageIds: [], hasMore: false,
|
|
247
|
+
};
|
|
248
|
+
senderMap.set(email, agg);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
agg.messageCount++;
|
|
252
|
+
if (listUnsub) agg.hasUnsubscribe = true;
|
|
253
|
+
if (!agg.displayName && displayName) agg.displayName = displayName;
|
|
254
|
+
|
|
255
|
+
if (agg.messageIds.length < maxIdsPerSender) {
|
|
256
|
+
agg.messageIds.push(msg.id);
|
|
257
|
+
} else {
|
|
258
|
+
agg.hasMore = true;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const msgEpoch = msg.internalDate ? Number(msg.internalDate) : 0;
|
|
262
|
+
if (listUnsub && msgEpoch >= agg.newestUnsubscribableEpoch) {
|
|
263
|
+
agg.newestUnsubscribableMessageId = msg.id;
|
|
264
|
+
agg.newestUnsubscribableEpoch = msgEpoch;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const sorted = [...senderMap.values()]
|
|
269
|
+
.sort((a, b) => b.messageCount - a.messageCount)
|
|
270
|
+
.slice(0, maxSenders);
|
|
271
|
+
|
|
272
|
+
const senders: SenderDigestEntry[] = sorted.map((s) => ({
|
|
273
|
+
id: Buffer.from(s.email).toString('base64url'),
|
|
274
|
+
displayName: s.displayName || s.email.split('@')[0],
|
|
275
|
+
email: s.email,
|
|
276
|
+
messageCount: s.messageCount,
|
|
277
|
+
hasUnsubscribe: s.hasUnsubscribe,
|
|
278
|
+
newestMessageId: (s.hasUnsubscribe && s.newestUnsubscribableMessageId)
|
|
279
|
+
? s.newestUnsubscribableMessageId
|
|
280
|
+
: s.newestMessageId,
|
|
281
|
+
searchQuery: `from:${s.email} ${query}`,
|
|
282
|
+
messageIds: s.messageIds,
|
|
283
|
+
hasMore: s.hasMore,
|
|
284
|
+
}));
|
|
285
|
+
|
|
286
|
+
return { senders, totalScanned: allMessageIds.length, queryUsed: query };
|
|
287
|
+
},
|
|
288
|
+
|
|
289
|
+
async archiveByQuery(token: string, query: string): Promise<ArchiveResult> {
|
|
290
|
+
const maxMessages = 5000;
|
|
291
|
+
const batchModifyLimit = 1000;
|
|
292
|
+
|
|
293
|
+
const allMessageIds: string[] = [];
|
|
294
|
+
let pageToken: string | undefined;
|
|
295
|
+
let truncated = false;
|
|
296
|
+
|
|
297
|
+
while (allMessageIds.length < maxMessages) {
|
|
298
|
+
const listResp = await gmail.listMessages(token, query, Math.min(500, maxMessages - allMessageIds.length), pageToken);
|
|
299
|
+
const ids = (listResp.messages ?? []).map((m) => m.id);
|
|
300
|
+
if (ids.length === 0) break;
|
|
301
|
+
allMessageIds.push(...ids);
|
|
302
|
+
pageToken = listResp.nextPageToken ?? undefined;
|
|
303
|
+
if (!pageToken) break;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (allMessageIds.length >= maxMessages && pageToken) {
|
|
307
|
+
truncated = true;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (allMessageIds.length === 0) {
|
|
311
|
+
return { archived: 0 };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
for (let i = 0; i < allMessageIds.length; i += batchModifyLimit) {
|
|
315
|
+
const chunk = allMessageIds.slice(i, i + batchModifyLimit);
|
|
316
|
+
await gmail.batchModifyMessages(token, chunk, { removeLabelIds: ['INBOX'] });
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return { archived: allMessageIds.length, truncated };
|
|
320
|
+
},
|
|
194
321
|
};
|
|
@@ -18,6 +18,7 @@ import { getGatewayInternalBaseUrl, getTwilioPhoneNumberEnv } from '../../../con
|
|
|
18
18
|
import { loadConfig } from '../../../config/loader.js';
|
|
19
19
|
import { getOrCreateConversation } from '../../../memory/conversation-key-store.js';
|
|
20
20
|
import * as externalConversationStore from '../../../memory/external-conversation-store.js';
|
|
21
|
+
import { DAEMON_INTERNAL_ASSISTANT_ID } from '../../../runtime/assistant-scope.js';
|
|
21
22
|
import { getSecureKey } from '../../../security/secure-keys.js';
|
|
22
23
|
import { readHttpToken } from '../../../util/platform.js';
|
|
23
24
|
import type { MessagingProvider } from '../../provider.js';
|
|
@@ -56,22 +57,8 @@ function hasTwilioCredentials(): boolean {
|
|
|
56
57
|
);
|
|
57
58
|
}
|
|
58
59
|
|
|
59
|
-
/**
|
|
60
|
-
|
|
61
|
-
* Priority: assistant-scoped phone number > TWILIO_PHONE_NUMBER env > config sms.phoneNumber > secure key fallback.
|
|
62
|
-
*/
|
|
63
|
-
function getPhoneNumber(assistantId?: string): string | undefined {
|
|
64
|
-
// Check assistant-scoped phone number first
|
|
65
|
-
if (assistantId) {
|
|
66
|
-
try {
|
|
67
|
-
const config = loadConfig();
|
|
68
|
-
const assistantPhone = config.sms?.assistantPhoneNumbers?.[assistantId];
|
|
69
|
-
if (assistantPhone) return assistantPhone;
|
|
70
|
-
} catch {
|
|
71
|
-
// Config may not be available yet during early startup
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
60
|
+
/** Resolve the configured SMS phone number. */
|
|
61
|
+
function getPhoneNumber(): string | undefined {
|
|
75
62
|
const fromEnv = getTwilioPhoneNumberEnv();
|
|
76
63
|
if (fromEnv) return fromEnv;
|
|
77
64
|
|
|
@@ -85,15 +72,6 @@ function getPhoneNumber(assistantId?: string): string | undefined {
|
|
|
85
72
|
return getSecureKey('credential:twilio:phone_number') || undefined;
|
|
86
73
|
}
|
|
87
74
|
|
|
88
|
-
function hasAnyAssistantPhoneNumber(): boolean {
|
|
89
|
-
try {
|
|
90
|
-
const config = loadConfig();
|
|
91
|
-
return Object.keys(config.sms?.assistantPhoneNumbers ?? {}).length > 0;
|
|
92
|
-
} catch {
|
|
93
|
-
return false;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
75
|
export const smsMessagingProvider: MessagingProvider = {
|
|
98
76
|
id: 'sms',
|
|
99
77
|
displayName: 'SMS',
|
|
@@ -106,7 +84,16 @@ export const smsMessagingProvider: MessagingProvider = {
|
|
|
106
84
|
* the `from` for outbound messages.
|
|
107
85
|
*/
|
|
108
86
|
isConnected(): boolean {
|
|
109
|
-
|
|
87
|
+
if (!hasTwilioCredentials()) return false;
|
|
88
|
+
if (getPhoneNumber()) return true;
|
|
89
|
+
try {
|
|
90
|
+
const config = loadConfig();
|
|
91
|
+
const mappings = config.sms?.assistantPhoneNumbers as Record<string, string> | undefined;
|
|
92
|
+
if (mappings && Object.keys(mappings).length > 0) return true;
|
|
93
|
+
} catch {
|
|
94
|
+
// Config may not be available yet
|
|
95
|
+
}
|
|
96
|
+
return false;
|
|
110
97
|
},
|
|
111
98
|
|
|
112
99
|
async testConnection(_token: string): Promise<ConnectionInfo> {
|
|
@@ -120,7 +107,26 @@ export const smsMessagingProvider: MessagingProvider = {
|
|
|
120
107
|
}
|
|
121
108
|
|
|
122
109
|
const phoneNumber = getPhoneNumber();
|
|
123
|
-
if (!phoneNumber
|
|
110
|
+
if (!phoneNumber) {
|
|
111
|
+
// Mirror isConnected(): fall back to assistant-scoped phone numbers
|
|
112
|
+
try {
|
|
113
|
+
const config = loadConfig();
|
|
114
|
+
const mappings = config.sms?.assistantPhoneNumbers as Record<string, string> | undefined;
|
|
115
|
+
if (mappings && Object.keys(mappings).length > 0) {
|
|
116
|
+
const accountSid = getSecureKey('credential:twilio:account_sid')!;
|
|
117
|
+
return {
|
|
118
|
+
connected: true,
|
|
119
|
+
user: 'assistant-scoped',
|
|
120
|
+
platform: 'sms',
|
|
121
|
+
metadata: {
|
|
122
|
+
accountSid: accountSid.slice(0, 6) + '...',
|
|
123
|
+
assistantPhoneNumbers: Object.keys(mappings).length,
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
} catch {
|
|
128
|
+
// Config may not be available yet
|
|
129
|
+
}
|
|
124
130
|
return {
|
|
125
131
|
connected: false,
|
|
126
132
|
user: 'unknown',
|
|
@@ -133,12 +139,11 @@ export const smsMessagingProvider: MessagingProvider = {
|
|
|
133
139
|
|
|
134
140
|
return {
|
|
135
141
|
connected: true,
|
|
136
|
-
user: phoneNumber
|
|
142
|
+
user: phoneNumber,
|
|
137
143
|
platform: 'sms',
|
|
138
144
|
metadata: {
|
|
139
145
|
accountSid: accountSid.slice(0, 6) + '...',
|
|
140
|
-
|
|
141
|
-
hasAssistantScopedPhoneNumbers: hasAnyAssistantPhoneNumber(),
|
|
146
|
+
phoneNumber,
|
|
142
147
|
},
|
|
143
148
|
};
|
|
144
149
|
},
|
|
@@ -152,16 +157,14 @@ export const smsMessagingProvider: MessagingProvider = {
|
|
|
152
157
|
|
|
153
158
|
// Upsert external conversation binding so the conversation key mapping
|
|
154
159
|
// exists for the next inbound SMS from this number.
|
|
160
|
+
const isSelfScope = !assistantId || assistantId === DAEMON_INTERNAL_ASSISTANT_ID;
|
|
155
161
|
try {
|
|
156
162
|
const sourceChannel = 'sms';
|
|
157
|
-
const conversationKey =
|
|
158
|
-
?
|
|
159
|
-
:
|
|
163
|
+
const conversationKey = isSelfScope
|
|
164
|
+
? `${sourceChannel}:${conversationId}`
|
|
165
|
+
: `asst:${assistantId}:${sourceChannel}:${conversationId}`;
|
|
160
166
|
const { conversationId: internalId } = getOrCreateConversation(conversationKey);
|
|
161
|
-
|
|
162
|
-
// sourceChannel + externalChatId). Restrict proactive writes to self so
|
|
163
|
-
// multi-assistant sends cannot clobber each other's binding metadata.
|
|
164
|
-
if (!assistantId || assistantId === 'self') {
|
|
167
|
+
if (isSelfScope) {
|
|
165
168
|
externalConversationStore.upsertOutboundBinding({
|
|
166
169
|
conversationId: internalId,
|
|
167
170
|
sourceChannel,
|
|
@@ -5,6 +5,12 @@
|
|
|
5
5
|
* The adapter broadcasts a `notification_intent` message that the Vellum
|
|
6
6
|
* client can use to display a native notification (e.g. NSUserNotification
|
|
7
7
|
* or UNUserNotificationCenter).
|
|
8
|
+
*
|
|
9
|
+
* Guardian-sensitive notifications (approval requests, escalation alerts)
|
|
10
|
+
* are annotated with `targetGuardianPrincipalId` so that only clients
|
|
11
|
+
* bound to the guardian identity display them. Non-guardian clients
|
|
12
|
+
* should ignore notifications with a `targetGuardianPrincipalId` that
|
|
13
|
+
* does not match their own identity.
|
|
8
14
|
*/
|
|
9
15
|
|
|
10
16
|
import type { ServerMessage } from '../../daemon/ipc-contract.js';
|
|
@@ -21,6 +27,24 @@ const log = getLogger('notif-adapter-vellum');
|
|
|
21
27
|
|
|
22
28
|
export type BroadcastFn = (msg: ServerMessage) => void;
|
|
23
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Event name prefixes that carry guardian-sensitive content (approval
|
|
32
|
+
* requests, escalation alerts, access requests). Notifications for
|
|
33
|
+
* these events are scoped to bound guardian devices via
|
|
34
|
+
* `targetGuardianPrincipalId`.
|
|
35
|
+
*/
|
|
36
|
+
const GUARDIAN_SENSITIVE_EVENT_PREFIXES = [
|
|
37
|
+
'guardian.question',
|
|
38
|
+
'ingress.escalation',
|
|
39
|
+
'ingress.access_request',
|
|
40
|
+
] as const;
|
|
41
|
+
|
|
42
|
+
export function isGuardianSensitiveEvent(sourceEventName: string): boolean {
|
|
43
|
+
return GUARDIAN_SENSITIVE_EVENT_PREFIXES.some(
|
|
44
|
+
(prefix) => sourceEventName === prefix || sourceEventName.startsWith(prefix + '.'),
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
24
48
|
export class VellumAdapter implements ChannelAdapter {
|
|
25
49
|
readonly channel: NotificationChannel = 'vellum';
|
|
26
50
|
|
|
@@ -30,8 +54,22 @@ export class VellumAdapter implements ChannelAdapter {
|
|
|
30
54
|
this.broadcast = broadcast;
|
|
31
55
|
}
|
|
32
56
|
|
|
33
|
-
async send(payload: ChannelDeliveryPayload,
|
|
57
|
+
async send(payload: ChannelDeliveryPayload, destination: ChannelDestination): Promise<DeliveryResult> {
|
|
34
58
|
try {
|
|
59
|
+
// For guardian-sensitive events, annotate the outbound message with
|
|
60
|
+
// the target guardian identity so clients can filter. The
|
|
61
|
+
// guardianPrincipalId comes from the vellum binding resolved by
|
|
62
|
+
// the destination resolver.
|
|
63
|
+
const guardianPrincipalId =
|
|
64
|
+
typeof destination.metadata?.guardianPrincipalId === 'string'
|
|
65
|
+
? destination.metadata.guardianPrincipalId
|
|
66
|
+
: undefined;
|
|
67
|
+
|
|
68
|
+
const targetGuardianPrincipalId =
|
|
69
|
+
guardianPrincipalId && isGuardianSensitiveEvent(payload.sourceEventName)
|
|
70
|
+
? guardianPrincipalId
|
|
71
|
+
: undefined;
|
|
72
|
+
|
|
35
73
|
this.broadcast({
|
|
36
74
|
type: 'notification_intent',
|
|
37
75
|
deliveryId: payload.deliveryId,
|
|
@@ -39,10 +77,15 @@ export class VellumAdapter implements ChannelAdapter {
|
|
|
39
77
|
title: payload.copy.title,
|
|
40
78
|
body: payload.copy.body,
|
|
41
79
|
deepLinkMetadata: payload.deepLinkTarget,
|
|
80
|
+
targetGuardianPrincipalId,
|
|
42
81
|
} as ServerMessage);
|
|
43
82
|
|
|
44
83
|
log.info(
|
|
45
|
-
{
|
|
84
|
+
{
|
|
85
|
+
sourceEventName: payload.sourceEventName,
|
|
86
|
+
title: payload.copy.title,
|
|
87
|
+
guardianScoped: targetGuardianPrincipalId != null,
|
|
88
|
+
},
|
|
46
89
|
'Vellum notification intent broadcast',
|
|
47
90
|
);
|
|
48
91
|
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import { v4 as uuid } from 'uuid';
|
|
13
13
|
|
|
14
14
|
import { getLogger } from '../util/logger.js';
|
|
15
|
+
import { isGuardianSensitiveEvent } from './adapters/macos.js';
|
|
15
16
|
import { pairDeliveryWithConversation } from './conversation-pairing.js';
|
|
16
17
|
import { composeFallbackCopy } from './copy-composer.js';
|
|
17
18
|
import { createDelivery, findDeliveryByDecisionAndChannel, updateDeliveryStatus } from './deliveries-store.js';
|
|
@@ -34,6 +35,8 @@ export interface ThreadCreatedInfo {
|
|
|
34
35
|
conversationId: string;
|
|
35
36
|
title: string;
|
|
36
37
|
sourceEventName: string;
|
|
38
|
+
/** Present when the thread is for a guardian-sensitive notification. */
|
|
39
|
+
targetGuardianPrincipalId?: string;
|
|
37
40
|
}
|
|
38
41
|
export type OnThreadCreatedFn = (info: ThreadCreatedInfo) => void;
|
|
39
42
|
export interface BroadcastDecisionOptions {
|
|
@@ -163,6 +166,18 @@ export class NotificationBroadcaster {
|
|
|
163
166
|
if (channel === 'vellum' && pairing.conversationId) {
|
|
164
167
|
deepLinkTarget = { ...deepLinkTarget, conversationId: pairing.conversationId };
|
|
165
168
|
|
|
169
|
+
// Resolve guardian scoping for thread-created events so clients
|
|
170
|
+
// can filter guardian-sensitive threads the same way they filter
|
|
171
|
+
// guardian-sensitive notification intents.
|
|
172
|
+
const guardianPrincipalId =
|
|
173
|
+
typeof destination.metadata?.guardianPrincipalId === 'string'
|
|
174
|
+
? destination.metadata.guardianPrincipalId
|
|
175
|
+
: undefined;
|
|
176
|
+
const targetGuardianPrincipalId =
|
|
177
|
+
guardianPrincipalId && isGuardianSensitiveEvent(signal.sourceEventName)
|
|
178
|
+
? guardianPrincipalId
|
|
179
|
+
: undefined;
|
|
180
|
+
|
|
166
181
|
const threadTitle =
|
|
167
182
|
copy.threadTitle ??
|
|
168
183
|
copy.title ??
|
|
@@ -171,6 +186,7 @@ export class NotificationBroadcaster {
|
|
|
171
186
|
conversationId: pairing.conversationId,
|
|
172
187
|
title: threadTitle,
|
|
173
188
|
sourceEventName: signal.sourceEventName,
|
|
189
|
+
targetGuardianPrincipalId,
|
|
174
190
|
};
|
|
175
191
|
|
|
176
192
|
// The per-dispatch onThreadCreated callback fires whenever a vellum
|
|
@@ -9,6 +9,10 @@
|
|
|
9
9
|
* values from the context payload.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
import {
|
|
13
|
+
buildGuardianRequestCodeInstruction,
|
|
14
|
+
resolveGuardianQuestionInstructionMode,
|
|
15
|
+
} from './guardian-question-mode.js';
|
|
12
16
|
import type { NotificationSignal } from './signal.js';
|
|
13
17
|
import type { NotificationChannel, RenderedChannelCopy } from './types.js';
|
|
14
18
|
|
|
@@ -48,16 +52,34 @@ const TEMPLATES: Record<string, CopyTemplate> = {
|
|
|
48
52
|
}
|
|
49
53
|
|
|
50
54
|
const normalizedCode = requestCode.toUpperCase();
|
|
55
|
+
const modeResolution = resolveGuardianQuestionInstructionMode(payload);
|
|
56
|
+
const instruction = buildGuardianRequestCodeInstruction(normalizedCode, modeResolution.mode);
|
|
51
57
|
return {
|
|
52
58
|
title: 'Guardian Question',
|
|
53
|
-
body: `${question}\n\
|
|
59
|
+
body: `${question}\n\n${instruction}`,
|
|
54
60
|
};
|
|
55
61
|
},
|
|
56
62
|
|
|
57
63
|
'ingress.access_request': (payload) => {
|
|
58
64
|
const requester = str(payload.senderIdentifier, 'Someone');
|
|
59
65
|
const requestCode = nonEmpty(typeof payload.requestCode === 'string' ? payload.requestCode : undefined);
|
|
60
|
-
const
|
|
66
|
+
const sourceChannel = typeof payload.sourceChannel === 'string' ? payload.sourceChannel : undefined;
|
|
67
|
+
const callerName = nonEmpty(typeof payload.senderName === 'string' ? payload.senderName : undefined);
|
|
68
|
+
const previousMemberStatus = typeof payload.previousMemberStatus === 'string'
|
|
69
|
+
? payload.previousMemberStatus
|
|
70
|
+
: undefined;
|
|
71
|
+
const lines: string[] = [];
|
|
72
|
+
|
|
73
|
+
// Voice-originated access requests include caller name context
|
|
74
|
+
if (sourceChannel === 'voice' && callerName) {
|
|
75
|
+
lines.push(`${callerName} (${str(payload.senderExternalUserId, requester)}) is calling and requesting access to the assistant.`);
|
|
76
|
+
} else {
|
|
77
|
+
lines.push(`${requester} is requesting access to the assistant.`);
|
|
78
|
+
}
|
|
79
|
+
if (previousMemberStatus === 'revoked') {
|
|
80
|
+
lines.push('Note: this user was previously revoked.');
|
|
81
|
+
}
|
|
82
|
+
|
|
61
83
|
if (requestCode) {
|
|
62
84
|
const code = requestCode.toUpperCase();
|
|
63
85
|
lines.push(`Reply "${code} approve" to grant access or "${code} reject" to deny.`);
|
|
@@ -69,6 +91,32 @@ const TEMPLATES: Record<string, CopyTemplate> = {
|
|
|
69
91
|
};
|
|
70
92
|
},
|
|
71
93
|
|
|
94
|
+
'ingress.access_request.callback_handoff': (payload) => {
|
|
95
|
+
const callerName = nonEmpty(typeof payload.callerName === 'string' ? payload.callerName : undefined);
|
|
96
|
+
const callerPhone = nonEmpty(typeof payload.callerPhoneNumber === 'string' ? payload.callerPhoneNumber : undefined);
|
|
97
|
+
const requestCode = nonEmpty(typeof payload.requestCode === 'string' ? payload.requestCode : undefined);
|
|
98
|
+
const memberId = nonEmpty(typeof payload.requesterMemberId === 'string' ? payload.requesterMemberId : undefined);
|
|
99
|
+
|
|
100
|
+
const callerIdentity = callerName && callerPhone
|
|
101
|
+
? `${callerName} (${callerPhone})`
|
|
102
|
+
: callerName ?? callerPhone ?? 'An unknown caller';
|
|
103
|
+
|
|
104
|
+
const lines: string[] = [];
|
|
105
|
+
lines.push(`${callerIdentity} called and requested a callback while you were unreachable.`);
|
|
106
|
+
|
|
107
|
+
if (requestCode) {
|
|
108
|
+
lines.push(`Request code: ${requestCode.toUpperCase()}`);
|
|
109
|
+
}
|
|
110
|
+
if (memberId) {
|
|
111
|
+
lines.push(`This caller is a trusted contact (member ID: ${memberId}).`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
title: 'Callback Requested',
|
|
116
|
+
body: lines.join('\n'),
|
|
117
|
+
};
|
|
118
|
+
},
|
|
119
|
+
|
|
72
120
|
'ingress.escalation': (payload) => ({
|
|
73
121
|
title: 'Escalation',
|
|
74
122
|
body: str(payload.senderIdentifier, 'An incoming message') + ' needs attention',
|
|
@@ -18,6 +18,12 @@ import type { ModelIntent } from '../providers/types.js';
|
|
|
18
18
|
import { getLogger } from '../util/logger.js';
|
|
19
19
|
import { composeFallbackCopy } from './copy-composer.js';
|
|
20
20
|
import { createDecision } from './decisions-store.js';
|
|
21
|
+
import {
|
|
22
|
+
buildGuardianRequestCodeInstruction,
|
|
23
|
+
hasGuardianRequestCodeInstruction,
|
|
24
|
+
resolveGuardianQuestionInstructionMode,
|
|
25
|
+
stripConflictingGuardianRequestInstructions,
|
|
26
|
+
} from './guardian-question-mode.js';
|
|
21
27
|
import { getPreferenceSummary } from './preference-summary.js';
|
|
22
28
|
import type { NotificationSignal, RoutingIntent } from './signal.js';
|
|
23
29
|
import { buildThreadCandidates, serializeCandidatesForPrompt,type ThreadCandidateSet } from './thread-candidates.js';
|
|
@@ -409,18 +415,15 @@ export function validateThreadActions(
|
|
|
409
415
|
function ensureGuardianRequestCodeInCopy(
|
|
410
416
|
copy: RenderedChannelCopy,
|
|
411
417
|
requestCode: string,
|
|
418
|
+
mode: 'approval' | 'answer',
|
|
412
419
|
): RenderedChannelCopy {
|
|
413
|
-
const instruction =
|
|
414
|
-
const hasParserCompatibleInstructions = (text: string | undefined): boolean => {
|
|
415
|
-
if (typeof text !== 'string') return false;
|
|
416
|
-
const upper = text.toUpperCase();
|
|
417
|
-
return upper.includes(`${requestCode} APPROVE`) && upper.includes(`${requestCode} REJECT`);
|
|
418
|
-
};
|
|
420
|
+
const instruction = buildGuardianRequestCodeInstruction(requestCode, mode);
|
|
419
421
|
|
|
420
422
|
const ensureText = (text: string | undefined): string => {
|
|
421
423
|
const base = typeof text === 'string' ? text.trim() : '';
|
|
422
|
-
|
|
423
|
-
|
|
424
|
+
const sanitized = stripConflictingGuardianRequestInstructions(base, requestCode, mode);
|
|
425
|
+
if (hasGuardianRequestCodeInstruction(sanitized, requestCode, mode)) return sanitized;
|
|
426
|
+
return sanitized.length > 0 ? `${sanitized}\n\n${instruction}` : instruction;
|
|
424
427
|
};
|
|
425
428
|
|
|
426
429
|
return {
|
|
@@ -445,6 +448,16 @@ function enforceGuardianRequestCode(
|
|
|
445
448
|
if (typeof rawCode !== 'string' || rawCode.trim().length === 0) return decision;
|
|
446
449
|
|
|
447
450
|
const requestCode = rawCode.trim().toUpperCase();
|
|
451
|
+
const modeResolution = resolveGuardianQuestionInstructionMode(signal.contextPayload);
|
|
452
|
+
if (modeResolution.legacyFallbackUsed) {
|
|
453
|
+
log.warn(
|
|
454
|
+
{
|
|
455
|
+
signalId: signal.signalId,
|
|
456
|
+
requestKind: modeResolution.requestKind,
|
|
457
|
+
},
|
|
458
|
+
'guardian.question payload missing/invalid typed fields; using legacy instruction-mode fallback',
|
|
459
|
+
);
|
|
460
|
+
}
|
|
448
461
|
const nextCopy: Partial<Record<NotificationChannel, RenderedChannelCopy>> = {
|
|
449
462
|
...decision.renderedCopy,
|
|
450
463
|
};
|
|
@@ -452,7 +465,7 @@ function enforceGuardianRequestCode(
|
|
|
452
465
|
for (const channel of Object.keys(nextCopy) as NotificationChannel[]) {
|
|
453
466
|
const copy = nextCopy[channel];
|
|
454
467
|
if (!copy) continue;
|
|
455
|
-
nextCopy[channel] = ensureGuardianRequestCodeInCopy(copy, requestCode);
|
|
468
|
+
nextCopy[channel] = ensureGuardianRequestCodeInCopy(copy, requestCode, modeResolution.mode);
|
|
456
469
|
}
|
|
457
470
|
|
|
458
471
|
return {
|
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
* Resolves per-channel destination endpoints for notification delivery.
|
|
3
3
|
*
|
|
4
4
|
* - Vellum: no external endpoint needed — delivery goes through the IPC
|
|
5
|
-
* broadcast mechanism to connected desktop/mobile clients.
|
|
5
|
+
* broadcast mechanism to connected desktop/mobile clients. The
|
|
6
|
+
* guardianPrincipalId from the vellum binding is included in metadata
|
|
7
|
+
* so downstream adapters can scope guardian-sensitive notifications to
|
|
8
|
+
* bound guardian devices only.
|
|
6
9
|
* - Binding-based channels (telegram, sms): require a chat/delivery ID
|
|
7
10
|
* sourced from the guardian binding for the assistant.
|
|
8
11
|
*/
|
|
@@ -35,7 +38,18 @@ export function resolveDestinations(
|
|
|
35
38
|
switch (channel as NotificationChannel) {
|
|
36
39
|
case 'vellum': {
|
|
37
40
|
// Vellum delivery is local IPC — no external endpoint required.
|
|
38
|
-
|
|
41
|
+
// Include the guardianPrincipalId from the vellum binding so the
|
|
42
|
+
// adapter can annotate guardian-sensitive notifications for scoped
|
|
43
|
+
// delivery to bound guardian devices.
|
|
44
|
+
const vellumBinding = getActiveBinding(assistantId, 'vellum');
|
|
45
|
+
const metadata: Record<string, unknown> = {};
|
|
46
|
+
if (vellumBinding) {
|
|
47
|
+
metadata.guardianPrincipalId = vellumBinding.guardianExternalUserId;
|
|
48
|
+
}
|
|
49
|
+
result.set('vellum', {
|
|
50
|
+
channel: 'vellum',
|
|
51
|
+
metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
|
|
52
|
+
});
|
|
39
53
|
break;
|
|
40
54
|
}
|
|
41
55
|
case 'telegram':
|
|
@@ -13,6 +13,7 @@ import { v4 as uuid } from 'uuid';
|
|
|
13
13
|
|
|
14
14
|
import { getDeliverableChannels } from '../channels/config.js';
|
|
15
15
|
import { getActiveBinding } from '../memory/channel-guardian-store.js';
|
|
16
|
+
import { DAEMON_INTERNAL_ASSISTANT_ID } from '../runtime/assistant-scope.js';
|
|
16
17
|
import { getLogger } from '../util/logger.js';
|
|
17
18
|
import { type BroadcastFn, VellumAdapter } from './adapters/macos.js';
|
|
18
19
|
import { SmsAdapter } from './adapters/sms.js';
|
|
@@ -23,7 +24,12 @@ import { updateDecision } from './decisions-store.js';
|
|
|
23
24
|
import { type DeterministicCheckContext, runDeterministicChecks } from './deterministic-checks.js';
|
|
24
25
|
import { createEvent, updateEventDedupeKey } from './events-store.js';
|
|
25
26
|
import { dispatchDecision } from './runtime-dispatch.js';
|
|
26
|
-
import type {
|
|
27
|
+
import type {
|
|
28
|
+
AttentionHints,
|
|
29
|
+
NotificationContextPayload,
|
|
30
|
+
NotificationSignal,
|
|
31
|
+
RoutingIntent,
|
|
32
|
+
} from './signal.js';
|
|
27
33
|
import type { NotificationChannel, NotificationDeliveryResult } from './types.js';
|
|
28
34
|
|
|
29
35
|
const log = getLogger('emit-signal');
|
|
@@ -66,9 +72,10 @@ function getBroadcaster(): NotificationBroadcaster {
|
|
|
66
72
|
conversationId: info.conversationId,
|
|
67
73
|
title: info.title,
|
|
68
74
|
sourceEventName: info.sourceEventName,
|
|
75
|
+
targetGuardianPrincipalId: info.targetGuardianPrincipalId,
|
|
69
76
|
});
|
|
70
77
|
log.info(
|
|
71
|
-
{ conversationId: info.conversationId },
|
|
78
|
+
{ conversationId: info.conversationId, guardianScoped: info.targetGuardianPrincipalId != null },
|
|
72
79
|
'Emitted notification_thread_created push event',
|
|
73
80
|
);
|
|
74
81
|
});
|
|
@@ -116,9 +123,9 @@ function getConnectedChannels(assistantId: string): NotificationChannel[] {
|
|
|
116
123
|
|
|
117
124
|
// ── Public API ─────────────────────────────────────────────────────────
|
|
118
125
|
|
|
119
|
-
export interface EmitSignalParams {
|
|
126
|
+
export interface EmitSignalParams<TEventName extends string = string> {
|
|
120
127
|
/** Free-form event name, e.g. 'reminder.fired', 'schedule.complete'. */
|
|
121
|
-
sourceEventName:
|
|
128
|
+
sourceEventName: TEventName;
|
|
122
129
|
/** Source channel that produced the event. */
|
|
123
130
|
sourceChannel: string;
|
|
124
131
|
/** Session or conversation ID from the source context. */
|
|
@@ -128,7 +135,7 @@ export interface EmitSignalParams {
|
|
|
128
135
|
/** Attention hints for the decision engine. */
|
|
129
136
|
attentionHints: AttentionHints;
|
|
130
137
|
/** Arbitrary context payload passed to the decision engine. */
|
|
131
|
-
contextPayload?:
|
|
138
|
+
contextPayload?: NotificationContextPayload<TEventName>;
|
|
132
139
|
/** Routing intent from the source (e.g. reminder). Controls post-decision channel enforcement. */
|
|
133
140
|
routingIntent?: RoutingIntent;
|
|
134
141
|
/** Free-form hints from the source for the decision engine. */
|
|
@@ -168,18 +175,20 @@ export interface EmitSignalResult {
|
|
|
168
175
|
* Fire-and-forget safe by default: errors are caught and logged unless
|
|
169
176
|
* `throwOnError` is enabled by the caller.
|
|
170
177
|
*/
|
|
171
|
-
export async function emitNotificationSignal
|
|
178
|
+
export async function emitNotificationSignal<TEventName extends string>(
|
|
179
|
+
params: EmitSignalParams<TEventName>,
|
|
180
|
+
): Promise<EmitSignalResult> {
|
|
172
181
|
const signalId = uuid();
|
|
173
|
-
const assistantId = params.assistantId ??
|
|
182
|
+
const assistantId = params.assistantId ?? DAEMON_INTERNAL_ASSISTANT_ID;
|
|
174
183
|
|
|
175
|
-
const signal: NotificationSignal = {
|
|
184
|
+
const signal: NotificationSignal<TEventName> = {
|
|
176
185
|
signalId,
|
|
177
186
|
assistantId,
|
|
178
187
|
createdAt: Date.now(),
|
|
179
188
|
sourceChannel: params.sourceChannel,
|
|
180
189
|
sourceSessionId: params.sourceSessionId,
|
|
181
190
|
sourceEventName: params.sourceEventName,
|
|
182
|
-
contextPayload: params.contextPayload ?? {}
|
|
191
|
+
contextPayload: (params.contextPayload ?? {}) as NotificationContextPayload<TEventName>,
|
|
183
192
|
attentionHints: params.attentionHints,
|
|
184
193
|
routingIntent: params.routingIntent,
|
|
185
194
|
routingHints: params.routingHints,
|