@vellumai/assistant 0.3.28 → 0.4.1
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/ARCHITECTURE.md +33 -3
- package/bun.lock +4 -1
- package/docs/trusted-contact-access.md +9 -2
- package/package.json +6 -3
- package/scripts/ipc/generate-swift.ts +3 -3
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +80 -0
- package/src/__tests__/agent-loop-thinking.test.ts +1 -1
- package/src/__tests__/approval-routes-http.test.ts +13 -5
- package/src/__tests__/asset-materialize-tool.test.ts +2 -0
- package/src/__tests__/asset-search-tool.test.ts +2 -0
- package/src/__tests__/assistant-events-sse-hardening.test.ts +4 -2
- package/src/__tests__/attachments-store.test.ts +2 -0
- package/src/__tests__/browser-skill-endstate.test.ts +3 -3
- package/src/__tests__/call-controller.test.ts +30 -29
- package/src/__tests__/call-routes-http.test.ts +34 -32
- package/src/__tests__/call-start-guardian-guard.test.ts +2 -0
- package/src/__tests__/channel-invite-transport.test.ts +6 -6
- package/src/__tests__/channel-reply-delivery.test.ts +19 -0
- package/src/__tests__/channel-retry-sweep.test.ts +130 -0
- package/src/__tests__/clarification-resolver.test.ts +2 -0
- package/src/__tests__/claude-code-skill-regression.test.ts +2 -0
- package/src/__tests__/claude-code-tool-profiles.test.ts +2 -0
- package/src/__tests__/commit-message-enrichment-service.test.ts +9 -1
- package/src/__tests__/computer-use-session-lifecycle.test.ts +2 -0
- package/src/__tests__/computer-use-session-working-dir.test.ts +1 -0
- package/src/__tests__/computer-use-skill-lifecycle-cleanup.test.ts +2 -0
- package/src/__tests__/config-schema.test.ts +5 -5
- package/src/__tests__/config-watcher.test.ts +3 -1
- package/src/__tests__/connection-policy.test.ts +14 -5
- package/src/__tests__/contacts-tools.test.ts +3 -1
- package/src/__tests__/contradiction-checker.test.ts +2 -0
- package/src/__tests__/conversation-pairing.test.ts +10 -0
- package/src/__tests__/conversation-routes.test.ts +1 -1
- package/src/__tests__/credential-security-invariants.test.ts +16 -6
- package/src/__tests__/credential-vault-unit.test.ts +2 -2
- package/src/__tests__/credential-vault.test.ts +5 -4
- package/src/__tests__/daemon-lifecycle.test.ts +9 -0
- package/src/__tests__/daemon-server-session-init.test.ts +27 -0
- package/src/__tests__/elevenlabs-config.test.ts +2 -0
- package/src/__tests__/encrypted-store.test.ts +10 -5
- package/src/__tests__/followup-tools.test.ts +3 -1
- package/src/__tests__/gateway-only-enforcement.test.ts +21 -21
- package/src/__tests__/gmail-integration.test.ts +0 -1
- package/src/__tests__/guardian-control-plane-policy.test.ts +25 -21
- package/src/__tests__/guardian-dispatch.test.ts +2 -0
- package/src/__tests__/guardian-grant-minting.test.ts +68 -1
- package/src/__tests__/guardian-outbound-http.test.ts +12 -9
- package/src/__tests__/guardian-routing-invariants.test.ts +138 -0
- package/src/__tests__/handle-user-message-secret-resume.test.ts +1 -0
- package/src/__tests__/handlers-slack-config.test.ts +3 -1
- package/src/__tests__/handlers-telegram-config.test.ts +3 -1
- package/src/__tests__/handlers-twilio-config.test.ts +3 -1
- package/src/__tests__/handlers-twitter-config.test.ts +3 -1
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +318 -0
- package/src/__tests__/heartbeat-service.test.ts +20 -0
- package/src/__tests__/inbound-invite-redemption.test.ts +33 -0
- package/src/__tests__/ingress-reconcile.test.ts +3 -1
- package/src/__tests__/ingress-routes-http.test.ts +231 -4
- package/src/__tests__/intent-routing.test.ts +2 -0
- package/src/__tests__/ipc-snapshot.test.ts +13 -0
- package/src/__tests__/media-generate-image.test.ts +21 -0
- package/src/__tests__/media-reuse-story.e2e.test.ts +2 -0
- package/src/__tests__/memory-regressions.test.ts +20 -20
- package/src/__tests__/non-member-access-request.test.ts +183 -9
- package/src/__tests__/notification-decision-fallback.test.ts +2 -0
- package/src/__tests__/notification-decision-strategy.test.ts +61 -0
- package/src/__tests__/notification-guardian-path.test.ts +2 -0
- package/src/__tests__/oauth-connect-handler.test.ts +3 -1
- package/src/__tests__/oauth2-gateway-transport.test.ts +2 -0
- package/src/__tests__/onboarding-starter-tasks.test.ts +4 -4
- package/src/__tests__/pairing-routes.test.ts +171 -0
- package/src/__tests__/playbook-execution.test.ts +3 -1
- package/src/__tests__/playbook-tools.test.ts +3 -1
- package/src/__tests__/provider-error-scenarios.test.ts +59 -8
- package/src/__tests__/proxy-approval-callback.test.ts +2 -0
- package/src/__tests__/recording-handler.test.ts +11 -0
- package/src/__tests__/recording-intent-handler.test.ts +15 -0
- package/src/__tests__/recording-state-machine.test.ts +13 -2
- package/src/__tests__/registry.test.ts +7 -3
- package/src/__tests__/relay-server.test.ts +148 -28
- package/src/__tests__/runtime-attachment-metadata.test.ts +4 -2
- package/src/__tests__/runtime-events-sse-parity.test.ts +21 -0
- package/src/__tests__/runtime-events-sse.test.ts +4 -2
- package/src/__tests__/sandbox-diagnostics.test.ts +2 -0
- package/src/__tests__/schedule-tools.test.ts +3 -1
- package/src/__tests__/send-endpoint-busy.test.ts +288 -0
- package/src/__tests__/session-abort-tool-results.test.ts +23 -0
- package/src/__tests__/session-agent-loop.test.ts +16 -0
- package/src/__tests__/session-conflict-gate.test.ts +21 -0
- package/src/__tests__/session-load-history-repair.test.ts +27 -17
- package/src/__tests__/session-pre-run-repair.test.ts +23 -0
- package/src/__tests__/session-profile-injection.test.ts +21 -0
- package/src/__tests__/session-provider-retry-repair.test.ts +20 -0
- package/src/__tests__/session-queue.test.ts +23 -0
- package/src/__tests__/session-runtime-assembly.test.ts +50 -12
- package/src/__tests__/session-skill-tools.test.ts +27 -5
- package/src/__tests__/session-slash-known.test.ts +23 -0
- package/src/__tests__/session-slash-queue.test.ts +23 -0
- package/src/__tests__/session-slash-unknown.test.ts +23 -0
- package/src/__tests__/session-workspace-cache-state.test.ts +7 -0
- package/src/__tests__/session-workspace-injection.test.ts +21 -0
- package/src/__tests__/session-workspace-tool-tracking.test.ts +21 -0
- package/src/__tests__/shell-credential-ref.test.ts +2 -0
- package/src/__tests__/skill-feature-flags-integration.test.ts +6 -6
- package/src/__tests__/skill-load-feature-flag.test.ts +5 -4
- package/src/__tests__/skill-projection-feature-flag.test.ts +22 -0
- package/src/__tests__/skills.test.ts +8 -4
- package/src/__tests__/slack-channel-config.test.ts +3 -1
- package/src/__tests__/subagent-tools.test.ts +19 -0
- package/src/__tests__/swarm-recursion.test.ts +2 -0
- package/src/__tests__/swarm-session-integration.test.ts +2 -0
- package/src/__tests__/swarm-tool.test.ts +2 -0
- package/src/__tests__/system-prompt.test.ts +3 -1
- package/src/__tests__/task-compiler.test.ts +3 -1
- package/src/__tests__/task-management-tools.test.ts +3 -1
- package/src/__tests__/task-tools.test.ts +3 -1
- package/src/__tests__/terminal-sandbox.test.ts +13 -12
- package/src/__tests__/terminal-tools.test.ts +2 -0
- package/src/__tests__/tool-approval-handler.test.ts +15 -15
- package/src/__tests__/tool-execution-abort-cleanup.test.ts +2 -0
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +2 -0
- package/src/__tests__/tool-grant-request-escalation.test.ts +7 -7
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +48 -0
- package/src/__tests__/trusted-contact-multichannel.test.ts +22 -19
- package/src/__tests__/trusted-contact-verification.test.ts +91 -0
- package/src/__tests__/twilio-routes-elevenlabs.test.ts +2 -0
- package/src/__tests__/twitter-auth-handler.test.ts +3 -1
- package/src/__tests__/twitter-cli-routing.test.ts +3 -1
- package/src/__tests__/view-image-tool.test.ts +3 -1
- package/src/__tests__/voice-invite-redemption.test.ts +329 -0
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +7 -5
- package/src/__tests__/voice-session-bridge.test.ts +10 -10
- package/src/__tests__/work-item-output.test.ts +3 -1
- package/src/__tests__/workspace-lifecycle.test.ts +13 -2
- package/src/calls/call-controller.ts +26 -23
- package/src/calls/guardian-action-sweep.ts +10 -2
- package/src/calls/relay-server.ts +216 -27
- package/src/calls/types.ts +1 -1
- package/src/calls/voice-session-bridge.ts +3 -3
- package/src/cli.ts +12 -0
- package/src/config/agent-schema.ts +14 -3
- package/src/config/calls-schema.ts +6 -6
- package/src/config/core-schema.ts +3 -3
- package/src/config/feature-flag-registry.json +8 -0
- package/src/config/mcp-schema.ts +1 -1
- package/src/config/memory-schema.ts +27 -19
- package/src/config/schema.ts +21 -21
- package/src/config/skills-schema.ts +7 -7
- package/src/config/vellum-skills/trusted-contacts/SKILL.md +139 -16
- package/src/daemon/handlers/config-inbox.ts +4 -4
- package/src/daemon/handlers/sessions.ts +148 -4
- package/src/daemon/ipc-contract/messages.ts +16 -0
- package/src/daemon/ipc-contract-inventory.json +1 -0
- package/src/daemon/lifecycle.ts +19 -0
- package/src/daemon/pairing-store.ts +86 -3
- package/src/daemon/response-tier.ts +6 -5
- package/src/daemon/session-agent-loop.ts +5 -5
- package/src/daemon/session-lifecycle.ts +25 -17
- package/src/daemon/session-memory.ts +2 -2
- package/src/daemon/session-process.ts +1 -20
- package/src/daemon/session-runtime-assembly.ts +28 -22
- package/src/daemon/session-tool-setup.ts +2 -2
- package/src/daemon/session.ts +3 -3
- package/src/memory/canonical-guardian-store.ts +63 -1
- package/src/memory/channel-guardian-store.ts +1 -0
- package/src/memory/conversation-crud.ts +7 -7
- package/src/memory/db-init.ts +4 -0
- package/src/memory/embedding-local.ts +257 -39
- package/src/memory/embedding-runtime-manager.ts +471 -0
- package/src/memory/guardian-bindings.ts +25 -1
- package/src/memory/indexer.ts +3 -3
- package/src/memory/ingress-invite-store.ts +45 -0
- package/src/memory/job-handlers/backfill.ts +16 -9
- package/src/memory/migrations/037-voice-invite-columns.ts +16 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/qdrant-client.ts +31 -22
- package/src/memory/schema.ts +4 -0
- package/src/notifications/copy-composer.ts +15 -0
- package/src/runtime/access-request-helper.ts +43 -7
- package/src/runtime/actor-trust-resolver.ts +46 -50
- package/src/runtime/channel-invite-transports/voice.ts +58 -0
- package/src/runtime/channel-retry-sweep.ts +18 -6
- package/src/runtime/guardian-context-resolver.ts +38 -96
- package/src/runtime/guardian-reply-router.ts +31 -1
- package/src/runtime/ingress-service.ts +80 -3
- package/src/runtime/invite-redemption-service.ts +141 -2
- package/src/runtime/routes/channel-route-shared.ts +1 -1
- package/src/runtime/routes/channel-routes.ts +1 -1
- package/src/runtime/routes/conversation-routes.ts +166 -2
- package/src/runtime/routes/guardian-approval-interception.ts +17 -6
- package/src/runtime/routes/inbound-message-handler.ts +41 -10
- package/src/runtime/routes/ingress-routes.ts +52 -4
- package/src/runtime/routes/pairing-routes.ts +3 -0
- package/src/tools/guardian-control-plane-policy.ts +2 -2
- package/src/tools/reminder/reminder-store.ts +10 -14
- package/src/tools/tool-approval-handler.ts +11 -11
- package/src/tools/types.ts +2 -2
- package/src/util/logger.ts +20 -8
- package/src/util/platform.ts +10 -0
- package/src/util/voice-code.ts +29 -0
- package/src/daemon/guardian-invite-intent.ts +0 -124
|
@@ -76,6 +76,7 @@ import { handleApprovalInterception } from './guardian-approval-interception.js'
|
|
|
76
76
|
import { deliverGeneratedApprovalPrompt } from './guardian-approval-prompt.js';
|
|
77
77
|
|
|
78
78
|
import '../channel-invite-transports/telegram.js';
|
|
79
|
+
import '../channel-invite-transports/voice.js';
|
|
79
80
|
|
|
80
81
|
const log = getLogger('runtime-http');
|
|
81
82
|
|
|
@@ -229,7 +230,7 @@ export async function handleChannelInbound(
|
|
|
229
230
|
typeof (rawCommandIntentForAcl as Record<string, unknown>).payload === 'string' &&
|
|
230
231
|
((rawCommandIntentForAcl as Record<string, unknown>).payload as string).startsWith('gv_');
|
|
231
232
|
|
|
232
|
-
// Parse invite token from /start
|
|
233
|
+
// Parse invite token from /start payloads using the channel transport
|
|
233
234
|
// adapter. The token is extracted once here so both the ACL bypass and
|
|
234
235
|
// the intercept handler can reference it without re-parsing.
|
|
235
236
|
const commandIntentForAcl = rawCommandIntentForAcl && typeof rawCommandIntentForAcl === 'object' && !Array.isArray(rawCommandIntentForAcl)
|
|
@@ -291,7 +292,7 @@ export async function handleChannelInbound(
|
|
|
291
292
|
}
|
|
292
293
|
|
|
293
294
|
// ── Invite token intercept (non-member) ──
|
|
294
|
-
// /start
|
|
295
|
+
// /start invite deep links grant access without guardian approval.
|
|
295
296
|
// Intercept here — before the deny gate — so valid invites short-circuit
|
|
296
297
|
// the ACL rejection and never reach the agent pipeline.
|
|
297
298
|
if (inviteToken && denyNonMember) {
|
|
@@ -774,6 +775,21 @@ export async function handleChannelInbound(
|
|
|
774
775
|
const guardianVerifyOutcome: 'verified' | 'failed' = verifyResult.success ? 'verified' : 'failed';
|
|
775
776
|
|
|
776
777
|
if (verifyResult.success) {
|
|
778
|
+
const existingMember = (canonicalSenderId ?? rawSenderId)
|
|
779
|
+
? findMember({
|
|
780
|
+
assistantId: canonicalAssistantId,
|
|
781
|
+
sourceChannel,
|
|
782
|
+
externalUserId: canonicalSenderId ?? rawSenderId!,
|
|
783
|
+
externalChatId,
|
|
784
|
+
})
|
|
785
|
+
: null;
|
|
786
|
+
const memberMatchesSender = existingMember?.externalUserId
|
|
787
|
+
? canonicalizeInboundIdentity(sourceChannel, existingMember.externalUserId) === (canonicalSenderId ?? rawSenderId)
|
|
788
|
+
: false;
|
|
789
|
+
const preservedDisplayName = memberMatchesSender && existingMember?.displayName?.trim().length
|
|
790
|
+
? existingMember.displayName
|
|
791
|
+
: body.senderName;
|
|
792
|
+
|
|
777
793
|
upsertMember({
|
|
778
794
|
assistantId: canonicalAssistantId,
|
|
779
795
|
sourceChannel,
|
|
@@ -781,7 +797,8 @@ export async function handleChannelInbound(
|
|
|
781
797
|
externalChatId,
|
|
782
798
|
status: 'active',
|
|
783
799
|
policy: 'allow',
|
|
784
|
-
|
|
800
|
+
// Keep guardian-curated member name stable across re-verification.
|
|
801
|
+
displayName: preservedDisplayName,
|
|
785
802
|
username: body.senderUsername,
|
|
786
803
|
});
|
|
787
804
|
|
|
@@ -898,8 +915,14 @@ export async function handleChannelInbound(
|
|
|
898
915
|
externalChatId,
|
|
899
916
|
senderExternalUserId: rawSenderId,
|
|
900
917
|
senderUsername: body.senderUsername,
|
|
918
|
+
senderDisplayName: body.senderName,
|
|
901
919
|
});
|
|
902
920
|
|
|
921
|
+
// Hoisted flag: set by the canonical guardian reply router when the invite
|
|
922
|
+
// handoff bypass fires. Prevents legacy approval interception from swallowing
|
|
923
|
+
// the message when other approvals are pending in the same chat.
|
|
924
|
+
let skipApprovalInterception = false;
|
|
925
|
+
|
|
903
926
|
// ── Canonical guardian reply router ──
|
|
904
927
|
// Attempts to route inbound messages through the canonical decision pipeline
|
|
905
928
|
// before falling through to the legacy approval interception. Handles
|
|
@@ -910,7 +933,7 @@ export async function handleChannelInbound(
|
|
|
910
933
|
replyCallbackUrl &&
|
|
911
934
|
(trimmedContent.length > 0 || hasCallbackData) &&
|
|
912
935
|
rawSenderId &&
|
|
913
|
-
guardianCtx.
|
|
936
|
+
guardianCtx.trustClass === 'guardian'
|
|
914
937
|
) {
|
|
915
938
|
// Compute destination-scoped pending request hints so the router can
|
|
916
939
|
// discover canonical requests delivered to this chat even when the
|
|
@@ -983,13 +1006,21 @@ export async function handleChannelInbound(
|
|
|
983
1006
|
requestId: routerResult.requestId,
|
|
984
1007
|
});
|
|
985
1008
|
}
|
|
1009
|
+
|
|
1010
|
+
if (routerResult.skipApprovalInterception) {
|
|
1011
|
+
skipApprovalInterception = true;
|
|
1012
|
+
}
|
|
986
1013
|
}
|
|
987
1014
|
|
|
988
1015
|
// ── Approval interception ──
|
|
989
1016
|
// Keep this active whenever callback context is available.
|
|
1017
|
+
// Skipped when the canonical router flagged skipApprovalInterception (e.g.
|
|
1018
|
+
// invite handoff bypass) to prevent the legacy interceptor from swallowing
|
|
1019
|
+
// messages that should reach the assistant.
|
|
990
1020
|
if (
|
|
991
1021
|
replyCallbackUrl &&
|
|
992
|
-
!result.duplicate
|
|
1022
|
+
!result.duplicate &&
|
|
1023
|
+
!skipApprovalInterception
|
|
993
1024
|
) {
|
|
994
1025
|
const approvalResult = await handleApprovalInterception({
|
|
995
1026
|
conversationId: result.conversationId,
|
|
@@ -1369,7 +1400,7 @@ function startPendingApprovalPromptWatcher(params: {
|
|
|
1369
1400
|
conversationId: string;
|
|
1370
1401
|
sourceChannel: ChannelId;
|
|
1371
1402
|
externalChatId: string;
|
|
1372
|
-
|
|
1403
|
+
guardianTrustClass: GuardianContext['trustClass'];
|
|
1373
1404
|
replyCallbackUrl: string;
|
|
1374
1405
|
bearerToken?: string;
|
|
1375
1406
|
assistantId?: string;
|
|
@@ -1379,7 +1410,7 @@ function startPendingApprovalPromptWatcher(params: {
|
|
|
1379
1410
|
conversationId,
|
|
1380
1411
|
sourceChannel,
|
|
1381
1412
|
externalChatId,
|
|
1382
|
-
|
|
1413
|
+
guardianTrustClass,
|
|
1383
1414
|
replyCallbackUrl,
|
|
1384
1415
|
bearerToken,
|
|
1385
1416
|
assistantId,
|
|
@@ -1388,7 +1419,7 @@ function startPendingApprovalPromptWatcher(params: {
|
|
|
1388
1419
|
|
|
1389
1420
|
// Approval prompt delivery is guardian-only. Non-guardian and unverified
|
|
1390
1421
|
// actors must never receive approval prompt broadcasts for the conversation.
|
|
1391
|
-
if (
|
|
1422
|
+
if (guardianTrustClass !== 'guardian') {
|
|
1392
1423
|
return () => {};
|
|
1393
1424
|
}
|
|
1394
1425
|
|
|
@@ -1470,7 +1501,7 @@ function processChannelMessageInBackground(params: BackgroundProcessingParams):
|
|
|
1470
1501
|
conversationId,
|
|
1471
1502
|
sourceChannel,
|
|
1472
1503
|
externalChatId,
|
|
1473
|
-
|
|
1504
|
+
guardianTrustClass: guardianCtx.trustClass,
|
|
1474
1505
|
replyCallbackUrl,
|
|
1475
1506
|
bearerToken,
|
|
1476
1507
|
assistantId,
|
|
@@ -1494,7 +1525,7 @@ function processChannelMessageInBackground(params: BackgroundProcessingParams):
|
|
|
1494
1525
|
},
|
|
1495
1526
|
assistantId,
|
|
1496
1527
|
guardianContext: toGuardianRuntimeContext(sourceChannel, guardianCtx),
|
|
1497
|
-
isInteractive: guardianCtx.
|
|
1528
|
+
isInteractive: guardianCtx.trustClass === 'guardian',
|
|
1498
1529
|
...(cmdIntent ? { commandIntent: cmdIntent } : {}),
|
|
1499
1530
|
},
|
|
1500
1531
|
sourceChannel,
|
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
* POST /v1/ingress/members/:id/block — block a member
|
|
9
9
|
*
|
|
10
10
|
* Invites:
|
|
11
|
-
* GET /v1/ingress/invites
|
|
12
|
-
* POST /v1/ingress/invites
|
|
13
|
-
* DELETE /v1/ingress/invites/:id
|
|
14
|
-
* POST /v1/ingress/invites/redeem
|
|
11
|
+
* GET /v1/ingress/invites — list invites
|
|
12
|
+
* POST /v1/ingress/invites — create an invite (supports voice)
|
|
13
|
+
* DELETE /v1/ingress/invites/:id — revoke an invite
|
|
14
|
+
* POST /v1/ingress/invites/redeem — redeem an invite (token or voice code)
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import {
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
listIngressInvites,
|
|
21
21
|
listIngressMembers,
|
|
22
22
|
redeemIngressInvite,
|
|
23
|
+
redeemVoiceInviteCode,
|
|
23
24
|
revokeIngressInvite,
|
|
24
25
|
revokeIngressMember,
|
|
25
26
|
upsertIngressMember,
|
|
@@ -130,6 +131,11 @@ export function handleListInvites(url: URL): Response {
|
|
|
130
131
|
|
|
131
132
|
/**
|
|
132
133
|
* POST /v1/ingress/invites
|
|
134
|
+
*
|
|
135
|
+
* For voice invites, pass `sourceChannel: "voice"` with required
|
|
136
|
+
* `expectedExternalUserId` (E.164 phone). Voice codes are always 6 digits.
|
|
137
|
+
* The response will include a one-time `voiceCode` field that must be
|
|
138
|
+
* communicated to the invited user out-of-band.
|
|
133
139
|
*/
|
|
134
140
|
export async function handleCreateInvite(req: Request): Promise<Response> {
|
|
135
141
|
const body = (await req.json()) as Record<string, unknown>;
|
|
@@ -139,6 +145,8 @@ export async function handleCreateInvite(req: Request): Promise<Response> {
|
|
|
139
145
|
note: body.note as string | undefined,
|
|
140
146
|
maxUses: body.maxUses as number | undefined,
|
|
141
147
|
expiresInMs: body.expiresInMs as number | undefined,
|
|
148
|
+
expectedExternalUserId: body.expectedExternalUserId as string | undefined,
|
|
149
|
+
voiceCodeDigits: body.voiceCodeDigits as number | undefined,
|
|
142
150
|
});
|
|
143
151
|
|
|
144
152
|
if (!result.ok) {
|
|
@@ -161,10 +169,50 @@ export function handleRevokeInvite(inviteId: string): Response {
|
|
|
161
169
|
|
|
162
170
|
/**
|
|
163
171
|
* POST /v1/ingress/invites/redeem
|
|
172
|
+
*
|
|
173
|
+
* Unified invite redemption endpoint. Supports two modes:
|
|
174
|
+
*
|
|
175
|
+
* 1. **Token-based** (existing): pass `token`, `sourceChannel`, `externalUserId`, etc.
|
|
176
|
+
* 2. **Voice code** (new): pass `code` and `callerExternalUserId` (E.164 phone).
|
|
177
|
+
* Optionally pass `assistantId`.
|
|
178
|
+
*
|
|
179
|
+
* The presence of `code` in the body selects voice-code redemption.
|
|
164
180
|
*/
|
|
165
181
|
export async function handleRedeemInvite(req: Request): Promise<Response> {
|
|
166
182
|
const body = (await req.json()) as Record<string, unknown>;
|
|
167
183
|
|
|
184
|
+
// Voice-code redemption path: triggered when `code` is present
|
|
185
|
+
if (body.code != null) {
|
|
186
|
+
const callerExternalUserId = body.callerExternalUserId as string | undefined;
|
|
187
|
+
const code = body.code as string | undefined;
|
|
188
|
+
|
|
189
|
+
if (!callerExternalUserId || !code) {
|
|
190
|
+
return Response.json(
|
|
191
|
+
{ ok: false, error: 'callerExternalUserId and code are required' },
|
|
192
|
+
{ status: 400 },
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const result = redeemVoiceInviteCode({
|
|
197
|
+
assistantId: body.assistantId as string | undefined,
|
|
198
|
+
callerExternalUserId,
|
|
199
|
+
sourceChannel: 'voice',
|
|
200
|
+
code,
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
if (!result.ok) {
|
|
204
|
+
return Response.json({ ok: false, error: result.reason }, { status: 400 });
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return Response.json({
|
|
208
|
+
ok: true,
|
|
209
|
+
type: result.type,
|
|
210
|
+
memberId: result.memberId,
|
|
211
|
+
...(result.type === 'redeemed' ? { inviteId: result.inviteId } : {}),
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Token-based redemption path (default)
|
|
168
216
|
const result = redeemIngressInvite({
|
|
169
217
|
token: body.token as string | undefined,
|
|
170
218
|
externalUserId: body.externalUserId as string | undefined,
|
|
@@ -75,6 +75,9 @@ export async function handlePairingRequest(req: Request, ctx: PairingHandlerCont
|
|
|
75
75
|
|
|
76
76
|
const result = ctx.pairingStore.beginRequest({ pairingRequestId, pairingSecret, deviceId, deviceName });
|
|
77
77
|
if (!result.ok) {
|
|
78
|
+
if (result.reason === 'already_paired') {
|
|
79
|
+
return httpError('CONFLICT', 'This pairing request is already bound to another device', 409);
|
|
80
|
+
}
|
|
78
81
|
const statusCode = result.reason === 'invalid_secret' ? 403 : result.reason === 'not_found' ? 403 : 410;
|
|
79
82
|
return httpError('FORBIDDEN', 'Forbidden', statusCode);
|
|
80
83
|
}
|
|
@@ -124,13 +124,13 @@ export function isGuardianControlPlaneInvocation(
|
|
|
124
124
|
export function enforceGuardianOnlyPolicy(
|
|
125
125
|
toolName: string,
|
|
126
126
|
input: Record<string, unknown>,
|
|
127
|
-
|
|
127
|
+
trustClass: string | undefined,
|
|
128
128
|
): { denied: boolean; reason?: string } {
|
|
129
129
|
if (!isGuardianControlPlaneInvocation(toolName, input)) {
|
|
130
130
|
return { denied: false };
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
if (
|
|
133
|
+
if (trustClass === 'guardian' || trustClass === undefined) {
|
|
134
134
|
return { denied: false };
|
|
135
135
|
}
|
|
136
136
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { and, asc, eq, lte } from 'drizzle-orm';
|
|
2
2
|
import { v4 as uuid } from 'uuid';
|
|
3
3
|
|
|
4
|
-
import { getDb,
|
|
4
|
+
import { getDb, rawRun } from '../../memory/db.js';
|
|
5
5
|
import { reminders } from '../../memory/schema.js';
|
|
6
6
|
import { cast,createRowMapper, parseJson } from '../../util/row-mapper.js';
|
|
7
7
|
|
|
@@ -105,14 +105,11 @@ export function listReminders(options?: { pendingOnly?: boolean }): ReminderRow[
|
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
export function cancelReminder(id: string): boolean {
|
|
108
|
-
const db = getDb();
|
|
109
108
|
const now = Date.now();
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
.run();
|
|
115
|
-
return rawChanges() > 0;
|
|
109
|
+
return rawRun(
|
|
110
|
+
'UPDATE reminders SET status = ?, updated_at = ? WHERE id = ? AND status = ?',
|
|
111
|
+
'cancelled', now, id, 'pending',
|
|
112
|
+
) > 0;
|
|
116
113
|
}
|
|
117
114
|
|
|
118
115
|
/**
|
|
@@ -132,13 +129,12 @@ export function claimDueReminders(now: number): ReminderRow[] {
|
|
|
132
129
|
|
|
133
130
|
const claimed: ReminderRow[] = [];
|
|
134
131
|
for (const row of candidates) {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
.run();
|
|
132
|
+
const changed = rawRun(
|
|
133
|
+
'UPDATE reminders SET status = ?, fired_at = ?, updated_at = ? WHERE id = ? AND status = ?',
|
|
134
|
+
'firing', now, now, row.id, 'pending',
|
|
135
|
+
);
|
|
140
136
|
|
|
141
|
-
if (
|
|
137
|
+
if (changed === 0) continue;
|
|
142
138
|
|
|
143
139
|
claimed.push(parseRow({
|
|
144
140
|
...row,
|
|
@@ -10,8 +10,8 @@ import type { ExecutionTarget, Tool, ToolContext, ToolExecutionResult, ToolLifec
|
|
|
10
10
|
|
|
11
11
|
const log = getLogger('tool-approval-handler');
|
|
12
12
|
|
|
13
|
-
function
|
|
14
|
-
return role === '
|
|
13
|
+
function isUntrustedGuardianTrustClass(role: ToolContext['guardianTrustClass']): boolean {
|
|
14
|
+
return role === 'trusted_contact' || role === 'unknown';
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
function requiresGuardianApprovalForActor(
|
|
@@ -26,10 +26,10 @@ function requiresGuardianApprovalForActor(
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
function guardianApprovalDeniedMessage(
|
|
29
|
-
|
|
29
|
+
trustClass: ToolContext['guardianTrustClass'],
|
|
30
30
|
toolName: string,
|
|
31
31
|
): string {
|
|
32
|
-
if (
|
|
32
|
+
if (trustClass === 'unknown') {
|
|
33
33
|
return `Permission denied for "${toolName}": this action requires guardian approval from a verified channel identity.`;
|
|
34
34
|
}
|
|
35
35
|
return `Permission denied for "${toolName}": this action requires guardian approval and the current actor is not the guardian.`;
|
|
@@ -82,13 +82,13 @@ export class ToolApprovalHandler {
|
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
// Reject tool invocations targeting guardian control-plane endpoints from non-guardian actors.
|
|
85
|
-
const guardianCheck = enforceGuardianOnlyPolicy(name, input, context.
|
|
85
|
+
const guardianCheck = enforceGuardianOnlyPolicy(name, input, context.guardianTrustClass);
|
|
86
86
|
if (guardianCheck.denied) {
|
|
87
87
|
log.warn({
|
|
88
88
|
toolName: name,
|
|
89
89
|
sessionId: context.sessionId,
|
|
90
90
|
conversationId: context.conversationId,
|
|
91
|
-
|
|
91
|
+
trustClass: context.guardianTrustClass,
|
|
92
92
|
reason: 'guardian_only_policy',
|
|
93
93
|
}, 'Guardian-only policy blocked tool invocation');
|
|
94
94
|
const durationMs = Date.now() - startTime;
|
|
@@ -118,7 +118,7 @@ export class ToolApprovalHandler {
|
|
|
118
118
|
let deferredConsumeParams: Parameters<typeof consumeGrantForInvocation>[0] | null = null;
|
|
119
119
|
|
|
120
120
|
if (
|
|
121
|
-
|
|
121
|
+
isUntrustedGuardianTrustClass(context.guardianTrustClass)
|
|
122
122
|
&& requiresGuardianApprovalForActor(name, input, executionTarget)
|
|
123
123
|
) {
|
|
124
124
|
const inputDigest = computeToolApprovalDigest(name, input);
|
|
@@ -233,7 +233,7 @@ export class ToolApprovalHandler {
|
|
|
233
233
|
toolName: name,
|
|
234
234
|
sessionId: context.sessionId,
|
|
235
235
|
conversationId: context.conversationId,
|
|
236
|
-
|
|
236
|
+
trustClass: context.guardianTrustClass,
|
|
237
237
|
executionTarget,
|
|
238
238
|
grantId: grantResult.grant.id,
|
|
239
239
|
}, 'Scoped grant consumed — allowing untrusted actor tool invocation');
|
|
@@ -273,7 +273,7 @@ export class ToolApprovalHandler {
|
|
|
273
273
|
// actors remain fail-closed with no escalation.
|
|
274
274
|
let escalationMessage: string | undefined;
|
|
275
275
|
if (
|
|
276
|
-
context.
|
|
276
|
+
context.guardianTrustClass === 'trusted_contact'
|
|
277
277
|
&& context.assistantId
|
|
278
278
|
&& context.executionChannel
|
|
279
279
|
&& context.requesterExternalUserId
|
|
@@ -308,12 +308,12 @@ export class ToolApprovalHandler {
|
|
|
308
308
|
// If escalation.failed, fall through to generic denial message.
|
|
309
309
|
}
|
|
310
310
|
|
|
311
|
-
const reason = escalationMessage ?? guardianApprovalDeniedMessage(context.
|
|
311
|
+
const reason = escalationMessage ?? guardianApprovalDeniedMessage(context.guardianTrustClass, name);
|
|
312
312
|
log.warn({
|
|
313
313
|
toolName: name,
|
|
314
314
|
sessionId: context.sessionId,
|
|
315
315
|
conversationId: context.conversationId,
|
|
316
|
-
|
|
316
|
+
trustClass: context.guardianTrustClass,
|
|
317
317
|
executionTarget,
|
|
318
318
|
reason: 'guardian_approval_required',
|
|
319
319
|
grantMissReason: grantResult.reason,
|
package/src/tools/types.ts
CHANGED
|
@@ -137,8 +137,8 @@ export interface ToolContext {
|
|
|
137
137
|
proxyApprovalCallback?: import('./network/script-proxy/types.js').ProxyApprovalCallback;
|
|
138
138
|
/** Optional principal identifier propagated to sub-tool confirmation flows. */
|
|
139
139
|
principal?: string;
|
|
140
|
-
/**
|
|
141
|
-
|
|
140
|
+
/** Inbound trust classification for the session — used by trust/policy gates. */
|
|
141
|
+
guardianTrustClass?: 'guardian' | 'trusted_contact' | 'unknown';
|
|
142
142
|
/** Channel through which the tool invocation originates (e.g. 'telegram', 'voice'). Used for scoped grant consumption. */
|
|
143
143
|
executionChannel?: string;
|
|
144
144
|
/** Voice/call session ID, if the invocation originates from a call. Used for scoped grant consumption. */
|
package/src/util/logger.ts
CHANGED
|
@@ -3,12 +3,22 @@ import { join } from 'node:path';
|
|
|
3
3
|
import { Writable } from 'node:stream';
|
|
4
4
|
|
|
5
5
|
import pino from 'pino';
|
|
6
|
+
import type { PrettyOptions } from 'pino-pretty';
|
|
6
7
|
import pinoPretty from 'pino-pretty';
|
|
7
8
|
|
|
8
9
|
import { getDebugMode, getDebugStdoutLogs,getLogStderr } from '../config/env-registry.js';
|
|
9
10
|
import { logSerializers } from './log-redact.js';
|
|
10
11
|
import { getLogPath } from './platform.js';
|
|
11
12
|
|
|
13
|
+
/** Common pino-pretty options that inline [module] into the message prefix. */
|
|
14
|
+
function prettyOpts(extra?: PrettyOptions): PrettyOptions {
|
|
15
|
+
return {
|
|
16
|
+
messageFormat: '[{module}] {msg}',
|
|
17
|
+
ignore: 'module',
|
|
18
|
+
...extra,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
12
22
|
export type LogFileConfig = {
|
|
13
23
|
dir: string | undefined;
|
|
14
24
|
retentionDays: number;
|
|
@@ -59,7 +69,7 @@ let activeLogFileConfig: LogFileConfig | null = null;
|
|
|
59
69
|
|
|
60
70
|
function buildRotatingLogger(config: LogFileConfig): pino.Logger {
|
|
61
71
|
if (!config.dir) {
|
|
62
|
-
return pino({ name: 'assistant', serializers: logSerializers }, pinoPretty({ destination: 1 }));
|
|
72
|
+
return pino({ name: 'assistant', serializers: logSerializers }, pinoPretty(prettyOpts({ destination: 1 })));
|
|
63
73
|
}
|
|
64
74
|
|
|
65
75
|
if (!existsSync(config.dir)) {
|
|
@@ -68,9 +78,10 @@ function buildRotatingLogger(config: LogFileConfig): pino.Logger {
|
|
|
68
78
|
|
|
69
79
|
const today = formatDate(new Date());
|
|
70
80
|
const filePath = logFilePathForDate(config.dir, new Date());
|
|
71
|
-
const
|
|
81
|
+
const fileDest = pino.destination({ dest: filePath, sync: false, mkdir: true, mode: 0o600 });
|
|
72
82
|
// Tighten permissions on pre-existing log files that may have been created with looser modes
|
|
73
83
|
try { chmodSync(filePath, 0o600); } catch { /* best-effort */ }
|
|
84
|
+
const fileStream = pinoPretty(prettyOpts({ destination: fileDest, colorize: false }));
|
|
74
85
|
|
|
75
86
|
activeLogDate = today;
|
|
76
87
|
activeLogFileConfig = config;
|
|
@@ -78,7 +89,7 @@ function buildRotatingLogger(config: LogFileConfig): pino.Logger {
|
|
|
78
89
|
const level = getDebugMode() ? 'debug' : 'info';
|
|
79
90
|
|
|
80
91
|
if (getDebugMode()) {
|
|
81
|
-
const prettyStream = pinoPretty({ destination: 2 });
|
|
92
|
+
const prettyStream = pinoPretty(prettyOpts({ destination: 2 }));
|
|
82
93
|
return pino(
|
|
83
94
|
{ name: 'assistant', level, serializers: logSerializers },
|
|
84
95
|
pino.multistream([
|
|
@@ -92,7 +103,7 @@ function buildRotatingLogger(config: LogFileConfig): pino.Logger {
|
|
|
92
103
|
{ name: 'assistant', level, serializers: logSerializers },
|
|
93
104
|
pino.multistream([
|
|
94
105
|
{ stream: fileStream, level: 'info' as const },
|
|
95
|
-
{ stream: pinoPretty({ destination: 1 }), level: 'info' as const },
|
|
106
|
+
{ stream: pinoPretty(prettyOpts({ destination: 1 })), level: 'info' as const },
|
|
96
107
|
]),
|
|
97
108
|
);
|
|
98
109
|
}
|
|
@@ -135,12 +146,13 @@ function getRootLogger(): pino.Logger {
|
|
|
135
146
|
|
|
136
147
|
try {
|
|
137
148
|
const logPath = getLogPath();
|
|
138
|
-
const
|
|
149
|
+
const fileDest = pino.destination({ dest: logPath, sync: false, mkdir: true, mode: 0o600 });
|
|
139
150
|
// Tighten permissions on pre-existing log files that may have been created with looser modes
|
|
140
151
|
try { chmodSync(logPath, 0o600); } catch { /* best-effort */ }
|
|
152
|
+
const fileStream = pinoPretty(prettyOpts({ destination: fileDest, colorize: false }));
|
|
141
153
|
|
|
142
154
|
if (getDebugMode()) {
|
|
143
|
-
const prettyStream = pinoPretty({ destination: 2 });
|
|
155
|
+
const prettyStream = pinoPretty(prettyOpts({ destination: 2 }));
|
|
144
156
|
const multi = pino.multistream([
|
|
145
157
|
{ stream: fileStream, level: 'info' as const },
|
|
146
158
|
{ stream: prettyStream, level: 'debug' as const },
|
|
@@ -151,14 +163,14 @@ function getRootLogger(): pino.Logger {
|
|
|
151
163
|
{ level: 'info', serializers: logSerializers },
|
|
152
164
|
pino.multistream([
|
|
153
165
|
{ stream: fileStream, level: 'info' as const },
|
|
154
|
-
{ stream: pinoPretty({ destination: 1 }), level: 'info' as const },
|
|
166
|
+
{ stream: pinoPretty(prettyOpts({ destination: 1 })), level: 'info' as const },
|
|
155
167
|
]),
|
|
156
168
|
);
|
|
157
169
|
} else {
|
|
158
170
|
rootLogger = pino({ level: 'info', serializers: logSerializers }, fileStream);
|
|
159
171
|
}
|
|
160
172
|
} catch {
|
|
161
|
-
rootLogger = pino({ level: getDebugMode() ? 'debug' : 'info', serializers: logSerializers }, pinoPretty({ destination: 2 }));
|
|
173
|
+
rootLogger = pino({ level: getDebugMode() ? 'debug' : 'info', serializers: logSerializers }, pinoPretty(prettyOpts({ destination: 2 })));
|
|
162
174
|
}
|
|
163
175
|
}
|
|
164
176
|
return rootLogger;
|
package/src/util/platform.ts
CHANGED
|
@@ -121,6 +121,15 @@ export function getDataDir(): string {
|
|
|
121
121
|
return join(getWorkspaceDir(), 'data');
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
/**
|
|
125
|
+
* Returns the embedding models directory (~/.vellum/workspace/embedding-models).
|
|
126
|
+
* Downloaded embedding runtime (onnxruntime-node, transformers bundle, model weights)
|
|
127
|
+
* is stored here, downloaded post-hatch rather than shipped with the app.
|
|
128
|
+
*/
|
|
129
|
+
export function getEmbeddingModelsDir(): string {
|
|
130
|
+
return join(getWorkspaceDir(), 'embedding-models');
|
|
131
|
+
}
|
|
132
|
+
|
|
124
133
|
/**
|
|
125
134
|
* Returns the IPC blob directory (~/.vellum/workspace/data/ipc-blobs).
|
|
126
135
|
* Temporary blob files for zero-copy IPC payloads live here.
|
|
@@ -357,6 +366,7 @@ export function ensureDataDir(): void {
|
|
|
357
366
|
workspace,
|
|
358
367
|
join(workspace, 'hooks'),
|
|
359
368
|
join(workspace, 'skills'),
|
|
369
|
+
join(workspace, 'embedding-models'),
|
|
360
370
|
// Data sub-dirs under workspace
|
|
361
371
|
wsData,
|
|
362
372
|
join(wsData, 'db'),
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cryptographic voice invite code generation and hashing.
|
|
3
|
+
*
|
|
4
|
+
* Generates short numeric codes (default 6 digits) for voice-channel invite
|
|
5
|
+
* redemption. The plaintext code is returned once at creation time and never
|
|
6
|
+
* stored — only its SHA-256 hash is persisted.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { createHash, randomInt } from 'node:crypto';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Generate a cryptographically random numeric code of the given length.
|
|
13
|
+
* Uses node:crypto randomInt for uniform distribution.
|
|
14
|
+
*/
|
|
15
|
+
export function generateVoiceCode(digits: number = 6): string {
|
|
16
|
+
if (digits < 4 || digits > 10) {
|
|
17
|
+
throw new Error(`Voice code digit count must be between 4 and 10, got ${digits}`);
|
|
18
|
+
}
|
|
19
|
+
const min = Math.pow(10, digits - 1); // e.g. 100000 for 6 digits
|
|
20
|
+
const max = Math.pow(10, digits); // e.g. 1000000 for 6 digits
|
|
21
|
+
return String(randomInt(min, max));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* SHA-256 hash a voice code for storage comparison.
|
|
26
|
+
*/
|
|
27
|
+
export function hashVoiceCode(code: string): string {
|
|
28
|
+
return createHash('sha256').update(code).digest('hex');
|
|
29
|
+
}
|