@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
|
@@ -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,9 +52,11 @@ 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
|
|
|
@@ -59,6 +65,9 @@ const TEMPLATES: Record<string, CopyTemplate> = {
|
|
|
59
65
|
const requestCode = nonEmpty(typeof payload.requestCode === 'string' ? payload.requestCode : undefined);
|
|
60
66
|
const sourceChannel = typeof payload.sourceChannel === 'string' ? payload.sourceChannel : undefined;
|
|
61
67
|
const callerName = nonEmpty(typeof payload.senderName === 'string' ? payload.senderName : undefined);
|
|
68
|
+
const previousMemberStatus = typeof payload.previousMemberStatus === 'string'
|
|
69
|
+
? payload.previousMemberStatus
|
|
70
|
+
: undefined;
|
|
62
71
|
const lines: string[] = [];
|
|
63
72
|
|
|
64
73
|
// Voice-originated access requests include caller name context
|
|
@@ -67,6 +76,9 @@ const TEMPLATES: Record<string, CopyTemplate> = {
|
|
|
67
76
|
} else {
|
|
68
77
|
lines.push(`${requester} is requesting access to the assistant.`);
|
|
69
78
|
}
|
|
79
|
+
if (previousMemberStatus === 'revoked') {
|
|
80
|
+
lines.push('Note: this user was previously revoked.');
|
|
81
|
+
}
|
|
70
82
|
|
|
71
83
|
if (requestCode) {
|
|
72
84
|
const code = requestCode.toUpperCase();
|
|
@@ -79,6 +91,32 @@ const TEMPLATES: Record<string, CopyTemplate> = {
|
|
|
79
91
|
};
|
|
80
92
|
},
|
|
81
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
|
+
|
|
82
120
|
'ingress.escalation': (payload) => ({
|
|
83
121
|
title: 'Escalation',
|
|
84
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':
|
|
@@ -24,7 +24,12 @@ import { updateDecision } from './decisions-store.js';
|
|
|
24
24
|
import { type DeterministicCheckContext, runDeterministicChecks } from './deterministic-checks.js';
|
|
25
25
|
import { createEvent, updateEventDedupeKey } from './events-store.js';
|
|
26
26
|
import { dispatchDecision } from './runtime-dispatch.js';
|
|
27
|
-
import type {
|
|
27
|
+
import type {
|
|
28
|
+
AttentionHints,
|
|
29
|
+
NotificationContextPayload,
|
|
30
|
+
NotificationSignal,
|
|
31
|
+
RoutingIntent,
|
|
32
|
+
} from './signal.js';
|
|
28
33
|
import type { NotificationChannel, NotificationDeliveryResult } from './types.js';
|
|
29
34
|
|
|
30
35
|
const log = getLogger('emit-signal');
|
|
@@ -67,9 +72,10 @@ function getBroadcaster(): NotificationBroadcaster {
|
|
|
67
72
|
conversationId: info.conversationId,
|
|
68
73
|
title: info.title,
|
|
69
74
|
sourceEventName: info.sourceEventName,
|
|
75
|
+
targetGuardianPrincipalId: info.targetGuardianPrincipalId,
|
|
70
76
|
});
|
|
71
77
|
log.info(
|
|
72
|
-
{ conversationId: info.conversationId },
|
|
78
|
+
{ conversationId: info.conversationId, guardianScoped: info.targetGuardianPrincipalId != null },
|
|
73
79
|
'Emitted notification_thread_created push event',
|
|
74
80
|
);
|
|
75
81
|
});
|
|
@@ -117,9 +123,9 @@ function getConnectedChannels(assistantId: string): NotificationChannel[] {
|
|
|
117
123
|
|
|
118
124
|
// ── Public API ─────────────────────────────────────────────────────────
|
|
119
125
|
|
|
120
|
-
export interface EmitSignalParams {
|
|
126
|
+
export interface EmitSignalParams<TEventName extends string = string> {
|
|
121
127
|
/** Free-form event name, e.g. 'reminder.fired', 'schedule.complete'. */
|
|
122
|
-
sourceEventName:
|
|
128
|
+
sourceEventName: TEventName;
|
|
123
129
|
/** Source channel that produced the event. */
|
|
124
130
|
sourceChannel: string;
|
|
125
131
|
/** Session or conversation ID from the source context. */
|
|
@@ -129,7 +135,7 @@ export interface EmitSignalParams {
|
|
|
129
135
|
/** Attention hints for the decision engine. */
|
|
130
136
|
attentionHints: AttentionHints;
|
|
131
137
|
/** Arbitrary context payload passed to the decision engine. */
|
|
132
|
-
contextPayload?:
|
|
138
|
+
contextPayload?: NotificationContextPayload<TEventName>;
|
|
133
139
|
/** Routing intent from the source (e.g. reminder). Controls post-decision channel enforcement. */
|
|
134
140
|
routingIntent?: RoutingIntent;
|
|
135
141
|
/** Free-form hints from the source for the decision engine. */
|
|
@@ -169,18 +175,20 @@ export interface EmitSignalResult {
|
|
|
169
175
|
* Fire-and-forget safe by default: errors are caught and logged unless
|
|
170
176
|
* `throwOnError` is enabled by the caller.
|
|
171
177
|
*/
|
|
172
|
-
export async function emitNotificationSignal
|
|
178
|
+
export async function emitNotificationSignal<TEventName extends string>(
|
|
179
|
+
params: EmitSignalParams<TEventName>,
|
|
180
|
+
): Promise<EmitSignalResult> {
|
|
173
181
|
const signalId = uuid();
|
|
174
182
|
const assistantId = params.assistantId ?? DAEMON_INTERNAL_ASSISTANT_ID;
|
|
175
183
|
|
|
176
|
-
const signal: NotificationSignal = {
|
|
184
|
+
const signal: NotificationSignal<TEventName> = {
|
|
177
185
|
signalId,
|
|
178
186
|
assistantId,
|
|
179
187
|
createdAt: Date.now(),
|
|
180
188
|
sourceChannel: params.sourceChannel,
|
|
181
189
|
sourceSessionId: params.sourceSessionId,
|
|
182
190
|
sourceEventName: params.sourceEventName,
|
|
183
|
-
contextPayload: params.contextPayload ?? {}
|
|
191
|
+
contextPayload: (params.contextPayload ?? {}) as NotificationContextPayload<TEventName>,
|
|
184
192
|
attentionHints: params.attentionHints,
|
|
185
193
|
routingIntent: params.routingIntent,
|
|
186
194
|
routingHints: params.routingHints,
|