@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
|
@@ -488,6 +488,24 @@
|
|
|
488
488
|
</div>
|
|
489
489
|
</section>
|
|
490
490
|
|
|
491
|
+
<!-- Brain Knowledge -->
|
|
492
|
+
<section class="section" id="home-base-brain-lane">
|
|
493
|
+
<div class="section-header anim anim-d6"><h2>Knowledge</h2></div>
|
|
494
|
+
|
|
495
|
+
<div class="card-stack">
|
|
496
|
+
<div class="card feature anim anim-d6">
|
|
497
|
+
<div class="card-icon">🧠</div>
|
|
498
|
+
<div class="card-body">
|
|
499
|
+
<div class="task">Knowledge Brain</div>
|
|
500
|
+
<div class="task-meta" id="home-base-brain-count">Loading...</div>
|
|
501
|
+
<div class="task-controls">
|
|
502
|
+
<a href="/v1/brain-graph-ui" class="task-button primary" id="home-base-brain-view">View →</a>
|
|
503
|
+
</div>
|
|
504
|
+
</div>
|
|
505
|
+
</div>
|
|
506
|
+
</div>
|
|
507
|
+
</section>
|
|
508
|
+
|
|
491
509
|
<!-- Onboarding -->
|
|
492
510
|
<section class="section" id="home-base-onboarding-lane">
|
|
493
511
|
<div class="section-header anim anim-d6"><h2>Set up your assistant</h2></div>
|
|
@@ -656,6 +674,28 @@
|
|
|
656
674
|
});
|
|
657
675
|
});
|
|
658
676
|
}
|
|
677
|
+
|
|
678
|
+
// Fetch brain graph count for the Knowledge Brain card.
|
|
679
|
+
(function () {
|
|
680
|
+
var countEl = byId('home-base-brain-count');
|
|
681
|
+
if (!countEl) return;
|
|
682
|
+
var apiToken = document.querySelector('meta[name="api-token"]');
|
|
683
|
+
var headers = {};
|
|
684
|
+
if (apiToken && apiToken.content) headers['Authorization'] = 'Bearer ' + apiToken.content;
|
|
685
|
+
fetch('/v1/brain-graph', { headers: headers })
|
|
686
|
+
.then(function (res) {
|
|
687
|
+
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
688
|
+
return res.json();
|
|
689
|
+
})
|
|
690
|
+
.then(function (data) {
|
|
691
|
+
var entities = (data.entities || []).length;
|
|
692
|
+
var relations = (data.relations || []).length;
|
|
693
|
+
countEl.textContent = entities + ' nodes \u2022 ' + relations + ' relations';
|
|
694
|
+
})
|
|
695
|
+
.catch(function () {
|
|
696
|
+
countEl.textContent = '\u2014';
|
|
697
|
+
});
|
|
698
|
+
})();
|
|
659
699
|
})();
|
|
660
700
|
</script>
|
|
661
701
|
</body>
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform callback route registration for containerized deployments.
|
|
3
|
+
*
|
|
4
|
+
* When the assistant daemon runs inside a container (IS_CONTAINERIZED=true)
|
|
5
|
+
* with a configured PLATFORM_BASE_URL and PLATFORM_ASSISTANT_ID, external
|
|
6
|
+
* service callbacks (Twilio webhooks, OAuth redirects, Telegram webhooks, etc.)
|
|
7
|
+
* must route through the platform's gateway proxy instead of hitting the
|
|
8
|
+
* assistant directly.
|
|
9
|
+
*
|
|
10
|
+
* This module registers callback routes with the platform's internal
|
|
11
|
+
* gateway endpoint so the platform knows how to forward inbound provider
|
|
12
|
+
* webhooks to the correct containerized assistant instance.
|
|
13
|
+
*
|
|
14
|
+
* The platform endpoint is:
|
|
15
|
+
* POST {PLATFORM_BASE_URL}/v1/internal/gateway/callback-routes/register/
|
|
16
|
+
*
|
|
17
|
+
* It accepts { assistant_id, callback_path, type } and returns a stable
|
|
18
|
+
* callback_url that external services should use.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { getPlatformAssistantId, getPlatformBaseUrl, getPlatformInternalApiKey } from '../config/env.js';
|
|
22
|
+
import { getIsContainerized } from '../config/env-registry.js';
|
|
23
|
+
import { getLogger } from '../util/logger.js';
|
|
24
|
+
|
|
25
|
+
const log = getLogger('platform-callback-registration');
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Whether the daemon should register callback routes with the platform.
|
|
29
|
+
* True when IS_CONTAINERIZED, PLATFORM_BASE_URL, and PLATFORM_ASSISTANT_ID
|
|
30
|
+
* are all set.
|
|
31
|
+
*/
|
|
32
|
+
export function shouldUsePlatformCallbacks(): boolean {
|
|
33
|
+
return getIsContainerized() && !!getPlatformBaseUrl() && !!getPlatformAssistantId();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface RegisterCallbackRouteResponse {
|
|
37
|
+
callback_url: string;
|
|
38
|
+
callback_path: string;
|
|
39
|
+
type: string;
|
|
40
|
+
assistant_id: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Register a callback route with the platform's internal gateway endpoint.
|
|
45
|
+
*
|
|
46
|
+
* @param callbackPath - The path portion after the ingress base URL
|
|
47
|
+
* (e.g. "webhooks/twilio/voice"). Leading/trailing slashes are stripped
|
|
48
|
+
* by the platform.
|
|
49
|
+
* @param type - The route type identifier (e.g. "twilio_voice", "twilio_sms",
|
|
50
|
+
* "twilio_status", "oauth", "telegram").
|
|
51
|
+
* @returns The platform-provided callback URL that external services should use.
|
|
52
|
+
* @throws If the platform request fails.
|
|
53
|
+
*/
|
|
54
|
+
export async function registerCallbackRoute(
|
|
55
|
+
callbackPath: string,
|
|
56
|
+
type: string,
|
|
57
|
+
): Promise<string> {
|
|
58
|
+
const platformBaseUrl = getPlatformBaseUrl().replace(/\/+$/, '');
|
|
59
|
+
const assistantId = getPlatformAssistantId();
|
|
60
|
+
const apiKey = getPlatformInternalApiKey();
|
|
61
|
+
|
|
62
|
+
const url = `${platformBaseUrl}/v1/internal/gateway/callback-routes/register/`;
|
|
63
|
+
|
|
64
|
+
const headers: Record<string, string> = {
|
|
65
|
+
'Content-Type': 'application/json',
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
if (apiKey) {
|
|
69
|
+
headers['Authorization'] = `Bearer ${apiKey}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const body = JSON.stringify({
|
|
73
|
+
assistant_id: assistantId,
|
|
74
|
+
callback_path: callbackPath,
|
|
75
|
+
type,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
log.debug({ callbackPath, type }, 'Registering platform callback route');
|
|
79
|
+
|
|
80
|
+
const response = await fetch(url, {
|
|
81
|
+
method: 'POST',
|
|
82
|
+
headers,
|
|
83
|
+
body,
|
|
84
|
+
signal: AbortSignal.timeout(10_000),
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
if (!response.ok) {
|
|
88
|
+
const detail = await response.text().catch(() => '');
|
|
89
|
+
throw new Error(
|
|
90
|
+
`Platform callback route registration failed (HTTP ${response.status}): ${detail}`,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const data = (await response.json()) as RegisterCallbackRouteResponse;
|
|
95
|
+
|
|
96
|
+
log.info(
|
|
97
|
+
{ callbackPath, type, callbackUrl: data.callback_url },
|
|
98
|
+
'Platform callback route registered',
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
return data.callback_url;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Resolve a callback URL, registering with the platform when containerized.
|
|
106
|
+
*
|
|
107
|
+
* When platform callbacks are enabled, registers the route and returns the
|
|
108
|
+
* platform's stable callback URL (optionally with query parameters appended).
|
|
109
|
+
* Otherwise evaluates the lazy direct URL supplier and returns that value.
|
|
110
|
+
*
|
|
111
|
+
* The `directUrl` parameter is a **lazy supplier** (a function returning a
|
|
112
|
+
* string) rather than an eagerly-evaluated string. This is critical because
|
|
113
|
+
* the direct URL builders (e.g. `getTwilioVoiceWebhookUrl`) call
|
|
114
|
+
* `getPublicBaseUrl()` which throws when no public ingress URL is configured.
|
|
115
|
+
* In containerized environments that rely solely on platform callbacks, the
|
|
116
|
+
* direct URL is never needed — deferring evaluation avoids the throw.
|
|
117
|
+
*
|
|
118
|
+
* @param directUrl - Lazy supplier for the direct callback URL.
|
|
119
|
+
* @param callbackPath - The path to register (e.g. "webhooks/twilio/voice").
|
|
120
|
+
* @param type - The route type identifier.
|
|
121
|
+
* @param queryParams - Optional query parameters to append to the resolved URL.
|
|
122
|
+
* @returns The resolved callback URL.
|
|
123
|
+
*/
|
|
124
|
+
export async function resolveCallbackUrl(
|
|
125
|
+
directUrl: () => string,
|
|
126
|
+
callbackPath: string,
|
|
127
|
+
type: string,
|
|
128
|
+
queryParams?: Record<string, string>,
|
|
129
|
+
): Promise<string> {
|
|
130
|
+
if (!shouldUsePlatformCallbacks()) {
|
|
131
|
+
return directUrl();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
let url = await registerCallbackRoute(callbackPath, type);
|
|
136
|
+
if (queryParams && Object.keys(queryParams).length > 0) {
|
|
137
|
+
const params = new URLSearchParams(queryParams);
|
|
138
|
+
const separator = url.includes('?') ? '&' : '?';
|
|
139
|
+
url = `${url}${separator}${params.toString()}`;
|
|
140
|
+
}
|
|
141
|
+
return url;
|
|
142
|
+
} catch (err) {
|
|
143
|
+
log.warn(
|
|
144
|
+
{ err, callbackPath, type },
|
|
145
|
+
'Failed to register platform callback route, falling back to direct URL',
|
|
146
|
+
);
|
|
147
|
+
try {
|
|
148
|
+
return directUrl();
|
|
149
|
+
} catch (fallbackErr) {
|
|
150
|
+
log.error(
|
|
151
|
+
{ fallbackErr, callbackPath, type },
|
|
152
|
+
'Direct URL fallback also failed after platform registration failure',
|
|
153
|
+
);
|
|
154
|
+
throw err;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -292,7 +292,7 @@ export interface UpdateCanonicalGuardianRequestParams {
|
|
|
292
292
|
status?: CanonicalRequestStatus;
|
|
293
293
|
answerText?: string;
|
|
294
294
|
decidedByExternalUserId?: string;
|
|
295
|
-
followupState?: string;
|
|
295
|
+
followupState?: string | null;
|
|
296
296
|
expiresAt?: string;
|
|
297
297
|
}
|
|
298
298
|
|
package/src/memory/db-init.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { getDb } from './db-connection.js';
|
|
2
2
|
import {
|
|
3
3
|
addCoreColumns,
|
|
4
|
+
createActorTokenRecordsTable,
|
|
4
5
|
createAssistantInboxTables,
|
|
5
6
|
createCallSessionsTables,
|
|
6
7
|
createCanonicalGuardianTables,
|
|
@@ -169,5 +170,8 @@ export function initializeDb(): void {
|
|
|
169
170
|
// 27. Voice invite display metadata (friend_name, guardian_name) for personalized prompts
|
|
170
171
|
migrateVoiceInviteDisplayMetadata(database);
|
|
171
172
|
|
|
173
|
+
// 28. Actor token records (hash-only actor token persistence)
|
|
174
|
+
createActorTokenRecordsTable(database);
|
|
175
|
+
|
|
172
176
|
validateMigrationState(database);
|
|
173
177
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { DrizzleDb } from '../db-connection.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Create the actor_token_records table for hash-only actor token persistence.
|
|
5
|
+
*
|
|
6
|
+
* Stores the SHA-256 hash of each actor token alongside metadata for
|
|
7
|
+
* verification and revocation. The raw token plaintext is never stored.
|
|
8
|
+
*/
|
|
9
|
+
export function createActorTokenRecordsTable(database: DrizzleDb): void {
|
|
10
|
+
database.run(/*sql*/ `
|
|
11
|
+
CREATE TABLE IF NOT EXISTS actor_token_records (
|
|
12
|
+
id TEXT PRIMARY KEY,
|
|
13
|
+
token_hash TEXT NOT NULL,
|
|
14
|
+
assistant_id TEXT NOT NULL,
|
|
15
|
+
guardian_principal_id TEXT NOT NULL,
|
|
16
|
+
hashed_device_id TEXT NOT NULL,
|
|
17
|
+
platform TEXT NOT NULL,
|
|
18
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
19
|
+
issued_at INTEGER NOT NULL,
|
|
20
|
+
expires_at INTEGER,
|
|
21
|
+
created_at INTEGER NOT NULL,
|
|
22
|
+
updated_at INTEGER NOT NULL
|
|
23
|
+
)
|
|
24
|
+
`);
|
|
25
|
+
|
|
26
|
+
// Unique active token per device binding
|
|
27
|
+
database.run(
|
|
28
|
+
/*sql*/ `CREATE UNIQUE INDEX IF NOT EXISTS idx_actor_tokens_active_device
|
|
29
|
+
ON actor_token_records(assistant_id, guardian_principal_id, hashed_device_id)
|
|
30
|
+
WHERE status = 'active'`,
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
// Token hash lookup for verification
|
|
34
|
+
database.run(
|
|
35
|
+
/*sql*/ `CREATE INDEX IF NOT EXISTS idx_actor_tokens_hash
|
|
36
|
+
ON actor_token_records(token_hash)
|
|
37
|
+
WHERE status = 'active'`,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
@@ -39,6 +39,7 @@ export { migrateGuardianActionToolMetadata } from './034-guardian-action-tool-me
|
|
|
39
39
|
export { migrateGuardianActionSupersession } from './035-guardian-action-supersession.js';
|
|
40
40
|
export { migrateNormalizePhoneIdentities } from './036-normalize-phone-identities.js';
|
|
41
41
|
export { migrateVoiceInviteColumns } from './037-voice-invite-columns.js';
|
|
42
|
+
export { createActorTokenRecordsTable } from './038-actor-token-records.js';
|
|
42
43
|
export { createCoreTables } from './100-core-tables.js';
|
|
43
44
|
export { createWatchersAndLogsTables } from './101-watchers-and-logs.js';
|
|
44
45
|
export { addCoreColumns } from './102-alter-table-columns.js';
|
package/src/memory/schema.ts
CHANGED
|
@@ -1143,6 +1143,22 @@ export const conversationAssistantAttentionState = sqliteTable('conversation_ass
|
|
|
1143
1143
|
index('idx_conv_attn_state_assistant_last_seen').on(table.assistantId, table.lastSeenAssistantMessageAt),
|
|
1144
1144
|
]);
|
|
1145
1145
|
|
|
1146
|
+
// ── Actor Token Records ──────────────────────────────────────────────
|
|
1147
|
+
|
|
1148
|
+
export const actorTokenRecords = sqliteTable('actor_token_records', {
|
|
1149
|
+
id: text('id').primaryKey(),
|
|
1150
|
+
tokenHash: text('token_hash').notNull(),
|
|
1151
|
+
assistantId: text('assistant_id').notNull(),
|
|
1152
|
+
guardianPrincipalId: text('guardian_principal_id').notNull(),
|
|
1153
|
+
hashedDeviceId: text('hashed_device_id').notNull(),
|
|
1154
|
+
platform: text('platform').notNull(),
|
|
1155
|
+
status: text('status').notNull().default('active'),
|
|
1156
|
+
issuedAt: integer('issued_at').notNull(),
|
|
1157
|
+
expiresAt: integer('expires_at'),
|
|
1158
|
+
createdAt: integer('created_at').notNull(),
|
|
1159
|
+
updatedAt: integer('updated_at').notNull(),
|
|
1160
|
+
});
|
|
1161
|
+
|
|
1146
1162
|
// ── Scoped Approval Grants ──────────────────────────────────────────
|
|
1147
1163
|
|
|
1148
1164
|
export const scopedApprovalGrants = sqliteTable('scoped_approval_grants', {
|
|
@@ -81,3 +81,27 @@ export interface SendOptions {
|
|
|
81
81
|
/** Optional assistant scope for multi-assistant channels. */
|
|
82
82
|
assistantId?: string;
|
|
83
83
|
}
|
|
84
|
+
|
|
85
|
+
/** Result from a sender digest scan — groups messages by sender for bulk cleanup. */
|
|
86
|
+
export interface SenderDigestEntry {
|
|
87
|
+
id: string;
|
|
88
|
+
displayName: string;
|
|
89
|
+
email: string;
|
|
90
|
+
messageCount: number;
|
|
91
|
+
hasUnsubscribe: boolean;
|
|
92
|
+
newestMessageId: string;
|
|
93
|
+
searchQuery: string;
|
|
94
|
+
messageIds: string[];
|
|
95
|
+
hasMore: boolean;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface SenderDigestResult {
|
|
99
|
+
senders: SenderDigestEntry[];
|
|
100
|
+
totalScanned: number;
|
|
101
|
+
queryUsed: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface ArchiveResult {
|
|
105
|
+
archived: number;
|
|
106
|
+
truncated?: boolean;
|
|
107
|
+
}
|
|
@@ -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,
|