@vellumai/assistant 0.4.2 → 0.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +3 -0
- package/ARCHITECTURE.md +124 -10
- package/README.md +43 -35
- package/docs/trusted-contact-access.md +20 -0
- package/package.json +1 -1
- package/scripts/ipc/generate-swift.ts +1 -0
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +58 -120
- package/src/__tests__/access-request-decision.test.ts +0 -1
- package/src/__tests__/actor-token-service.test.ts +1099 -0
- package/src/__tests__/agent-loop.test.ts +51 -0
- package/src/__tests__/approval-routes-http.test.ts +2 -0
- package/src/__tests__/assistant-events-sse-hardening.test.ts +7 -5
- package/src/__tests__/assistant-id-boundary-guard.test.ts +415 -0
- package/src/__tests__/call-controller.test.ts +49 -0
- package/src/__tests__/call-pointer-message-composer.test.ts +171 -0
- package/src/__tests__/call-pointer-messages.test.ts +93 -3
- package/src/__tests__/call-pointer-no-hardcoded-copy.guard.test.ts +42 -0
- package/src/__tests__/call-routes-http.test.ts +0 -25
- package/src/__tests__/callback-handoff-copy.test.ts +186 -0
- package/src/__tests__/channel-approval-routes.test.ts +133 -12
- package/src/__tests__/channel-guardian.test.ts +0 -86
- package/src/__tests__/channel-readiness-service.test.ts +10 -16
- package/src/__tests__/checker.test.ts +33 -12
- package/src/__tests__/config-schema.test.ts +6 -0
- package/src/__tests__/confirmation-request-guardian-bridge.test.ts +410 -0
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +256 -0
- package/src/__tests__/conversation-routes.test.ts +12 -3
- package/src/__tests__/credential-security-invariants.test.ts +1 -1
- package/src/__tests__/daemon-server-session-init.test.ts +4 -0
- package/src/__tests__/deterministic-verification-control-plane.test.ts +0 -1
- package/src/__tests__/guardian-actions-endpoint.test.ts +39 -13
- package/src/__tests__/guardian-dispatch.test.ts +8 -0
- package/src/__tests__/guardian-outbound-http.test.ts +4 -5
- package/src/__tests__/guardian-question-mode.test.ts +200 -0
- package/src/__tests__/guardian-routing-invariants.test.ts +178 -0
- package/src/__tests__/guardian-routing-state.test.ts +525 -0
- package/src/__tests__/handle-user-message-secret-resume.test.ts +2 -0
- package/src/__tests__/handlers-telegram-config.test.ts +0 -83
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +55 -0
- package/src/__tests__/headless-browser-navigate.test.ts +2 -0
- package/src/__tests__/inbound-invite-redemption.test.ts +0 -1
- package/src/__tests__/ingress-routes-http.test.ts +55 -0
- package/src/__tests__/ipc-snapshot.test.ts +18 -51
- package/src/__tests__/non-member-access-request.test.ts +159 -9
- package/src/__tests__/notification-decision-fallback.test.ts +129 -4
- package/src/__tests__/notification-decision-strategy.test.ts +106 -2
- package/src/__tests__/notification-guardian-path.test.ts +3 -0
- package/src/__tests__/recording-intent-handler.test.ts +1 -0
- package/src/__tests__/relay-server.test.ts +1475 -33
- package/src/__tests__/send-endpoint-busy.test.ts +5 -0
- package/src/__tests__/session-agent-loop.test.ts +1 -0
- package/src/__tests__/session-confirmation-signals.test.ts +523 -0
- package/src/__tests__/session-init.benchmark.test.ts +0 -2
- package/src/__tests__/session-runtime-assembly.test.ts +4 -1
- package/src/__tests__/session-surfaces-task-progress.test.ts +44 -1
- package/src/__tests__/session-tool-setup-app-refresh.test.ts +81 -2
- package/src/__tests__/session-tool-setup-memory-scope.test.ts +1 -1
- package/src/__tests__/session-tool-setup-side-effect-flag.test.ts +1 -1
- package/src/__tests__/tool-executor.test.ts +21 -2
- package/src/__tests__/tool-grant-request-escalation.test.ts +333 -27
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +678 -0
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +1064 -0
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +11 -1
- package/src/__tests__/trusted-contact-multichannel.test.ts +0 -1
- package/src/__tests__/trusted-contact-verification.test.ts +0 -1
- package/src/__tests__/twilio-config.test.ts +2 -13
- package/src/__tests__/twilio-routes.test.ts +4 -3
- package/src/__tests__/update-bulletin.test.ts +0 -1
- package/src/agent/loop.ts +1 -1
- package/src/approvals/guardian-decision-primitive.ts +12 -3
- package/src/approvals/guardian-request-resolvers.ts +169 -11
- package/src/calls/call-constants.ts +29 -0
- package/src/calls/call-controller.ts +11 -3
- package/src/calls/call-domain.ts +33 -11
- package/src/calls/call-pointer-message-composer.ts +154 -0
- package/src/calls/call-pointer-messages.ts +106 -27
- package/src/calls/guardian-dispatch.ts +4 -2
- package/src/calls/relay-server.ts +921 -112
- package/src/calls/twilio-config.ts +4 -11
- package/src/calls/twilio-routes.ts +4 -6
- package/src/calls/types.ts +3 -1
- package/src/calls/voice-session-bridge.ts +4 -3
- package/src/cli/core-commands.ts +7 -4
- package/src/cli.ts +5 -4
- package/src/config/bundled-skills/agentmail/SKILL.md +4 -0
- package/src/config/bundled-skills/app-builder/SKILL.md +309 -10
- package/src/config/bundled-skills/app-builder/TOOLS.json +1 -1
- package/src/config/bundled-skills/email-setup/SKILL.md +1 -1
- package/src/config/bundled-skills/google-oauth-setup/SKILL.md +105 -81
- package/src/config/bundled-skills/messaging/SKILL.md +61 -12
- package/src/config/bundled-skills/messaging/TOOLS.json +58 -0
- package/src/config/bundled-skills/messaging/tools/gmail-sender-digest.ts +6 -1
- package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +35 -0
- package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +52 -0
- package/src/config/bundled-skills/phone-calls/SKILL.md +30 -39
- package/src/config/bundled-skills/twitter/SKILL.md +3 -3
- package/src/config/bundled-skills/vercel-token-setup/SKILL.md +215 -0
- package/src/config/calls-schema.ts +36 -0
- package/src/config/env.ts +22 -0
- package/src/config/feature-flag-registry.json +8 -8
- package/src/config/schema.ts +2 -2
- package/src/config/skills.ts +11 -0
- package/src/config/system-prompt.ts +11 -1
- package/src/config/templates/SOUL.md +2 -0
- package/src/config/vellum-skills/sms-setup/SKILL.md +71 -82
- package/src/config/vellum-skills/trusted-contacts/SKILL.md +8 -1
- package/src/config/vellum-skills/twilio-setup/SKILL.md +88 -73
- package/src/daemon/call-pointer-generators.ts +59 -0
- package/src/daemon/computer-use-session.ts +2 -5
- package/src/daemon/handlers/apps.ts +76 -20
- package/src/daemon/handlers/config-channels.ts +9 -61
- package/src/daemon/handlers/config-inbox.ts +11 -3
- package/src/daemon/handlers/config-ingress.ts +28 -3
- package/src/daemon/handlers/config-telegram.ts +12 -0
- package/src/daemon/handlers/config.ts +2 -6
- package/src/daemon/handlers/index.ts +2 -1
- package/src/daemon/handlers/pairing.ts +2 -0
- package/src/daemon/handlers/publish.ts +11 -46
- package/src/daemon/handlers/sessions.ts +59 -5
- package/src/daemon/handlers/shared.ts +17 -2
- package/src/daemon/ipc-contract/apps.ts +1 -0
- package/src/daemon/ipc-contract/inbox.ts +4 -0
- package/src/daemon/ipc-contract/integrations.ts +1 -97
- package/src/daemon/ipc-contract/messages.ts +47 -1
- package/src/daemon/ipc-contract/notifications.ts +11 -0
- package/src/daemon/ipc-contract-inventory.json +2 -4
- package/src/daemon/lifecycle.ts +17 -0
- package/src/daemon/server.ts +16 -2
- package/src/daemon/session-agent-loop-handlers.ts +20 -0
- package/src/daemon/session-agent-loop.ts +24 -12
- package/src/daemon/session-lifecycle.ts +1 -1
- package/src/daemon/session-process.ts +11 -1
- package/src/daemon/session-runtime-assembly.ts +6 -1
- package/src/daemon/session-surfaces.ts +32 -3
- package/src/daemon/session.ts +88 -1
- package/src/daemon/tool-side-effects.ts +22 -0
- package/src/home-base/prebuilt/brain-graph.html +1483 -0
- package/src/home-base/prebuilt/index.html +40 -0
- package/src/inbound/platform-callback-registration.ts +157 -0
- package/src/memory/canonical-guardian-store.ts +1 -1
- package/src/memory/conversation-crud.ts +2 -1
- package/src/memory/conversation-title-service.ts +16 -2
- package/src/memory/db-init.ts +8 -0
- package/src/memory/delivery-crud.ts +2 -1
- package/src/memory/guardian-action-store.ts +2 -1
- package/src/memory/guardian-approvals.ts +3 -2
- package/src/memory/ingress-invite-store.ts +12 -2
- package/src/memory/ingress-member-store.ts +4 -3
- package/src/memory/migrations/038-actor-token-records.ts +39 -0
- package/src/memory/migrations/124-voice-invite-display-metadata.ts +14 -0
- package/src/memory/migrations/index.ts +2 -0
- package/src/memory/schema.ts +26 -5
- package/src/messaging/provider-types.ts +24 -0
- package/src/messaging/provider.ts +7 -0
- package/src/messaging/providers/gmail/adapter.ts +127 -0
- package/src/messaging/providers/sms/adapter.ts +40 -37
- package/src/notifications/adapters/macos.ts +45 -2
- package/src/notifications/broadcaster.ts +16 -0
- package/src/notifications/copy-composer.ts +50 -2
- package/src/notifications/decision-engine.ts +22 -9
- package/src/notifications/destination-resolver.ts +16 -2
- package/src/notifications/emit-signal.ts +18 -9
- package/src/notifications/guardian-question-mode.ts +419 -0
- package/src/notifications/signal.ts +14 -3
- package/src/permissions/checker.ts +13 -1
- package/src/permissions/prompter.ts +14 -0
- package/src/providers/anthropic/client.ts +20 -0
- package/src/providers/provider-send-message.ts +15 -3
- package/src/runtime/access-request-helper.ts +82 -4
- package/src/runtime/actor-token-service.ts +234 -0
- package/src/runtime/actor-token-store.ts +236 -0
- package/src/runtime/actor-trust-resolver.ts +2 -2
- package/src/runtime/assistant-scope.ts +10 -0
- package/src/runtime/channel-approvals.ts +5 -3
- package/src/runtime/channel-readiness-service.ts +23 -64
- package/src/runtime/channel-readiness-types.ts +3 -4
- package/src/runtime/channel-retry-sweep.ts +4 -1
- package/src/runtime/confirmation-request-guardian-bridge.ts +197 -0
- package/src/runtime/guardian-action-followup-executor.ts +1 -1
- package/src/runtime/guardian-context-resolver.ts +82 -0
- package/src/runtime/guardian-outbound-actions.ts +5 -7
- package/src/runtime/guardian-reply-router.ts +67 -30
- package/src/runtime/guardian-vellum-migration.ts +57 -0
- package/src/runtime/http-server.ts +75 -31
- package/src/runtime/http-types.ts +13 -0
- package/src/runtime/ingress-service.ts +14 -0
- package/src/runtime/invite-redemption-service.ts +10 -1
- package/src/runtime/local-actor-identity.ts +76 -0
- package/src/runtime/middleware/actor-token.ts +271 -0
- package/src/runtime/middleware/twilio-validation.ts +2 -4
- package/src/runtime/routes/approval-routes.ts +82 -7
- package/src/runtime/routes/brain-graph-routes.ts +222 -0
- package/src/runtime/routes/call-routes.ts +2 -1
- package/src/runtime/routes/channel-readiness-routes.ts +71 -0
- package/src/runtime/routes/channel-route-shared.ts +3 -3
- package/src/runtime/routes/conversation-attention-routes.ts +2 -1
- package/src/runtime/routes/conversation-routes.ts +142 -53
- package/src/runtime/routes/events-routes.ts +22 -8
- package/src/runtime/routes/guardian-action-routes.ts +45 -3
- package/src/runtime/routes/guardian-approval-interception.ts +29 -0
- package/src/runtime/routes/guardian-bootstrap-routes.ts +145 -0
- package/src/runtime/routes/inbound-conversation.ts +4 -3
- package/src/runtime/routes/inbound-message-handler.ts +147 -5
- package/src/runtime/routes/ingress-routes.ts +2 -0
- package/src/runtime/routes/integration-routes.ts +7 -15
- package/src/runtime/routes/pairing-routes.ts +163 -0
- package/src/runtime/routes/twilio-routes.ts +934 -0
- package/src/runtime/tool-grant-request-helper.ts +3 -1
- package/src/security/oauth2.ts +27 -2
- package/src/security/token-manager.ts +46 -10
- package/src/tools/browser/browser-execution.ts +4 -3
- package/src/tools/browser/browser-handoff.ts +10 -18
- package/src/tools/browser/browser-manager.ts +80 -25
- package/src/tools/browser/browser-screencast.ts +35 -119
- package/src/tools/calls/call-start.ts +2 -1
- package/src/tools/permission-checker.ts +15 -4
- package/src/tools/terminal/parser.ts +12 -0
- package/src/tools/tool-approval-handler.ts +244 -19
- package/src/workspace/git-service.ts +19 -0
- package/src/__tests__/handlers-twilio-config.test.ts +0 -1928
- package/src/daemon/handlers/config-twilio.ts +0 -1082
|
@@ -21,13 +21,20 @@ import {
|
|
|
21
21
|
applyCanonicalGuardianDecision,
|
|
22
22
|
type CanonicalDecisionResult,
|
|
23
23
|
} from '../approvals/guardian-decision-primitive.js';
|
|
24
|
-
import type { ActorContext, ChannelDeliveryContext } from '../approvals/guardian-request-resolvers.js';
|
|
24
|
+
import type { ActorContext, ChannelDeliveryContext, ResolverEmissionContext } from '../approvals/guardian-request-resolvers.js';
|
|
25
25
|
import {
|
|
26
26
|
type CanonicalGuardianRequest,
|
|
27
27
|
getCanonicalGuardianRequest,
|
|
28
28
|
getCanonicalGuardianRequestByCode,
|
|
29
29
|
listCanonicalGuardianRequests,
|
|
30
30
|
} from '../memory/canonical-guardian-store.js';
|
|
31
|
+
import {
|
|
32
|
+
buildGuardianCodeOnlyClarification,
|
|
33
|
+
buildGuardianDisambiguationExample,
|
|
34
|
+
buildGuardianDisambiguationLabel,
|
|
35
|
+
buildGuardianInvalidActionReply,
|
|
36
|
+
resolveGuardianInstructionModeForRequest,
|
|
37
|
+
} from '../notifications/guardian-question-mode.js';
|
|
31
38
|
import { getLogger } from '../util/logger.js';
|
|
32
39
|
import { runApprovalConversationTurn } from './approval-conversation-turn.js';
|
|
33
40
|
import type { ApprovalAction } from './channel-approval-types.js';
|
|
@@ -60,6 +67,8 @@ export interface GuardianReplyContext {
|
|
|
60
67
|
approvalConversationGenerator?: ApprovalConversationGenerator;
|
|
61
68
|
/** Optional channel delivery context for resolver-driven side effects. */
|
|
62
69
|
channelDeliveryContext?: ChannelDeliveryContext;
|
|
70
|
+
/** Optional emission context threaded to handleConfirmationResponse for correct source attribution. */
|
|
71
|
+
emissionContext?: ResolverEmissionContext;
|
|
63
72
|
}
|
|
64
73
|
|
|
65
74
|
export type GuardianReplyResultType =
|
|
@@ -235,9 +244,11 @@ function notConsumed(): GuardianReplyResult {
|
|
|
235
244
|
export async function routeGuardianReply(
|
|
236
245
|
ctx: GuardianReplyContext,
|
|
237
246
|
): Promise<GuardianReplyResult> {
|
|
238
|
-
const { messageText, actor, conversationId, callbackData, approvalConversationGenerator, channelDeliveryContext } = ctx;
|
|
247
|
+
const { messageText, actor, conversationId, callbackData, approvalConversationGenerator, channelDeliveryContext, emissionContext } = ctx;
|
|
239
248
|
const pendingRequests = findPendingCanonicalRequests(actor, ctx.pendingRequestIds, conversationId);
|
|
240
|
-
const scopedPendingRequestIds = ctx.pendingRequestIds
|
|
249
|
+
const scopedPendingRequestIds = ctx.pendingRequestIds && ctx.pendingRequestIds.length > 0
|
|
250
|
+
? new Set(ctx.pendingRequestIds)
|
|
251
|
+
: null;
|
|
241
252
|
|
|
242
253
|
// ── 1. Deterministic callback parsing (button presses) ──
|
|
243
254
|
// No conversationId scoping here — the guardian's reply comes from a
|
|
@@ -247,7 +258,7 @@ export async function routeGuardianReply(
|
|
|
247
258
|
if (callbackData) {
|
|
248
259
|
const parsed = parseCallbackAction(callbackData);
|
|
249
260
|
if (parsed) {
|
|
250
|
-
return applyDecision(parsed.requestId, parsed.action, actor, undefined, channelDeliveryContext);
|
|
261
|
+
return applyDecision(parsed.requestId, parsed.action, actor, undefined, channelDeliveryContext, emissionContext);
|
|
251
262
|
}
|
|
252
263
|
}
|
|
253
264
|
|
|
@@ -280,7 +291,7 @@ export async function routeGuardianReply(
|
|
|
280
291
|
consumed: true,
|
|
281
292
|
type: 'canonical_decision_stale',
|
|
282
293
|
requestId: request.id,
|
|
283
|
-
replyText: failureReplyText('already_resolved', request.requestCode),
|
|
294
|
+
replyText: failureReplyText('already_resolved', request.requestCode, request),
|
|
284
295
|
};
|
|
285
296
|
}
|
|
286
297
|
|
|
@@ -333,7 +344,7 @@ export async function routeGuardianReply(
|
|
|
333
344
|
// If the text indicates rejection, use reject; otherwise approve_once.
|
|
334
345
|
const action = inferActionFromText(codeResult.remainingText);
|
|
335
346
|
|
|
336
|
-
return applyDecision(request.id, action, actor, codeResult.remainingText, channelDeliveryContext);
|
|
347
|
+
return applyDecision(request.id, action, actor, codeResult.remainingText, channelDeliveryContext, emissionContext);
|
|
337
348
|
}
|
|
338
349
|
}
|
|
339
350
|
|
|
@@ -375,6 +386,7 @@ export async function routeGuardianReply(
|
|
|
375
386
|
actor,
|
|
376
387
|
messageText,
|
|
377
388
|
channelDeliveryContext,
|
|
389
|
+
emissionContext,
|
|
378
390
|
);
|
|
379
391
|
}
|
|
380
392
|
|
|
@@ -475,12 +487,13 @@ export async function routeGuardianReply(
|
|
|
475
487
|
};
|
|
476
488
|
}
|
|
477
489
|
|
|
478
|
-
const result = await applyDecision(targetId, decisionAction, actor, messageText, channelDeliveryContext);
|
|
490
|
+
const result = await applyDecision(targetId, decisionAction, actor, messageText, channelDeliveryContext, emissionContext);
|
|
479
491
|
|
|
480
492
|
// Attach the engine's reply text for stale/expired/identity-mismatch cases,
|
|
481
|
-
// but preserve
|
|
482
|
-
//
|
|
483
|
-
|
|
493
|
+
// but preserve resolver-authored replies (for example verification codes)
|
|
494
|
+
// and explicit resolver-failure text.
|
|
495
|
+
const hasResolverReplyText = Boolean(result.canonicalResult?.applied && result.canonicalResult.resolverReplyText);
|
|
496
|
+
if (engineResult.replyText && result.type !== 'canonical_resolver_failed' && !hasResolverReplyText) {
|
|
484
497
|
result.replyText = engineResult.replyText;
|
|
485
498
|
}
|
|
486
499
|
|
|
@@ -504,6 +517,7 @@ async function applyDecision(
|
|
|
504
517
|
actor: ActorContext,
|
|
505
518
|
userText?: string,
|
|
506
519
|
channelDeliveryContext?: ChannelDeliveryContext,
|
|
520
|
+
emissionContext?: ResolverEmissionContext,
|
|
507
521
|
): Promise<GuardianReplyResult> {
|
|
508
522
|
const canonicalResult = await applyCanonicalGuardianDecision({
|
|
509
523
|
requestId,
|
|
@@ -511,6 +525,7 @@ async function applyDecision(
|
|
|
511
525
|
actorContext: actor,
|
|
512
526
|
userText,
|
|
513
527
|
channelDeliveryContext,
|
|
528
|
+
emissionContext,
|
|
514
529
|
});
|
|
515
530
|
|
|
516
531
|
if (canonicalResult.applied) {
|
|
@@ -549,6 +564,7 @@ async function applyDecision(
|
|
|
549
564
|
decisionApplied: true,
|
|
550
565
|
consumed: true,
|
|
551
566
|
type: 'canonical_decision_applied',
|
|
567
|
+
...(canonicalResult.resolverReplyText ? { replyText: canonicalResult.resolverReplyText } : {}),
|
|
552
568
|
requestId,
|
|
553
569
|
canonicalResult,
|
|
554
570
|
};
|
|
@@ -570,13 +586,15 @@ async function applyDecision(
|
|
|
570
586
|
return notConsumed();
|
|
571
587
|
}
|
|
572
588
|
|
|
589
|
+
const request = getCanonicalGuardianRequest(requestId);
|
|
590
|
+
|
|
573
591
|
return {
|
|
574
592
|
decisionApplied: false,
|
|
575
593
|
consumed: true,
|
|
576
594
|
type: 'canonical_decision_stale',
|
|
577
595
|
requestId,
|
|
578
596
|
canonicalResult,
|
|
579
|
-
replyText: failureReplyText(canonicalResult.reason),
|
|
597
|
+
replyText: failureReplyText(canonicalResult.reason, request?.requestCode, request ?? undefined),
|
|
580
598
|
};
|
|
581
599
|
}
|
|
582
600
|
|
|
@@ -643,6 +661,12 @@ function inferActionFromText(text: string): ApprovalAction {
|
|
|
643
661
|
return 'approve_once';
|
|
644
662
|
}
|
|
645
663
|
|
|
664
|
+
function resolveRequestInstructionMode(
|
|
665
|
+
request?: Pick<CanonicalGuardianRequest, 'kind' | 'toolName'> | null,
|
|
666
|
+
): 'approval' | 'answer' {
|
|
667
|
+
return resolveGuardianInstructionModeForRequest(request);
|
|
668
|
+
}
|
|
669
|
+
|
|
646
670
|
// ---------------------------------------------------------------------------
|
|
647
671
|
// Failure reason reply text
|
|
648
672
|
// ---------------------------------------------------------------------------
|
|
@@ -653,7 +677,11 @@ type CanonicalFailureReason = 'already_resolved' | 'identity_mismatch' | 'invali
|
|
|
653
677
|
* Map a canonical decision failure reason to a distinct, actionable reply
|
|
654
678
|
* so the guardian understands exactly what happened and what to do next.
|
|
655
679
|
*/
|
|
656
|
-
function failureReplyText(
|
|
680
|
+
function failureReplyText(
|
|
681
|
+
reason: CanonicalFailureReason,
|
|
682
|
+
requestCode?: string | null,
|
|
683
|
+
request?: CanonicalGuardianRequest,
|
|
684
|
+
): string {
|
|
657
685
|
switch (reason) {
|
|
658
686
|
case 'already_resolved':
|
|
659
687
|
return 'This request has already been resolved.';
|
|
@@ -662,9 +690,7 @@ function failureReplyText(reason: CanonicalFailureReason, requestCode?: string |
|
|
|
662
690
|
case 'identity_mismatch':
|
|
663
691
|
return "You don't have permission to decide on this request.";
|
|
664
692
|
case 'invalid_action':
|
|
665
|
-
return requestCode
|
|
666
|
-
? `I found request ${requestCode}, but I need to know your decision. Reply "${requestCode} approve" or "${requestCode} reject".`
|
|
667
|
-
: "I couldn't determine your intended action. Reply with the request code followed by 'approve' or 'reject' (e.g., \"ABC123 approve\").";
|
|
693
|
+
return buildGuardianInvalidActionReply(resolveRequestInstructionMode(request), requestCode ?? undefined);
|
|
668
694
|
default:
|
|
669
695
|
return "I couldn't process that request. Please try again.";
|
|
670
696
|
}
|
|
@@ -681,15 +707,12 @@ function failureReplyText(reason: CanonicalFailureReason, requestCode?: string |
|
|
|
681
707
|
*/
|
|
682
708
|
function composeCodeOnlyClarification(request: CanonicalGuardianRequest): string {
|
|
683
709
|
const code = request.requestCode ?? 'unknown';
|
|
684
|
-
const
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
}
|
|
691
|
-
lines.push(`Reply "${code} approve" to approve or "${code} reject" to reject.`);
|
|
692
|
-
return lines.join('\n');
|
|
710
|
+
const mode = resolveRequestInstructionMode(request);
|
|
711
|
+
return buildGuardianCodeOnlyClarification(mode, {
|
|
712
|
+
requestCode: code,
|
|
713
|
+
questionText: request.questionText,
|
|
714
|
+
toolName: request.toolName,
|
|
715
|
+
});
|
|
693
716
|
}
|
|
694
717
|
|
|
695
718
|
// ---------------------------------------------------------------------------
|
|
@@ -706,6 +729,10 @@ function composeDisambiguationReply(
|
|
|
706
729
|
engineReplyText?: string,
|
|
707
730
|
): string {
|
|
708
731
|
const lines: string[] = [];
|
|
732
|
+
const requestsWithMode = pendingRequests.map((request) => ({
|
|
733
|
+
request,
|
|
734
|
+
mode: resolveRequestInstructionMode(request),
|
|
735
|
+
}));
|
|
709
736
|
|
|
710
737
|
if (engineReplyText) {
|
|
711
738
|
lines.push(engineReplyText);
|
|
@@ -714,16 +741,26 @@ function composeDisambiguationReply(
|
|
|
714
741
|
|
|
715
742
|
lines.push(`You have ${pendingRequests.length} pending requests. Please specify which one:`);
|
|
716
743
|
|
|
717
|
-
for (const
|
|
718
|
-
const toolLabel =
|
|
719
|
-
|
|
744
|
+
for (const { request, mode } of requestsWithMode) {
|
|
745
|
+
const toolLabel = buildGuardianDisambiguationLabel(mode, {
|
|
746
|
+
questionText: request.questionText,
|
|
747
|
+
toolName: request.toolName,
|
|
748
|
+
});
|
|
749
|
+
const code = request.requestCode ?? request.id.slice(0, 6).toUpperCase();
|
|
720
750
|
lines.push(` - ${code}: ${toolLabel}`);
|
|
721
751
|
}
|
|
722
752
|
|
|
723
|
-
|
|
724
|
-
const
|
|
753
|
+
const questionRequest = requestsWithMode.find(({ mode }) => mode === 'answer');
|
|
754
|
+
const decisionRequest = requestsWithMode.find(({ mode }) => mode === 'approval');
|
|
725
755
|
lines.push('');
|
|
726
|
-
|
|
756
|
+
if (questionRequest) {
|
|
757
|
+
const exampleCode = questionRequest.request.requestCode ?? questionRequest.request.id.slice(0, 6).toUpperCase();
|
|
758
|
+
lines.push(buildGuardianDisambiguationExample(questionRequest.mode, exampleCode));
|
|
759
|
+
}
|
|
760
|
+
if (decisionRequest) {
|
|
761
|
+
const exampleCode = decisionRequest.request.requestCode ?? decisionRequest.request.id.slice(0, 6).toUpperCase();
|
|
762
|
+
lines.push(buildGuardianDisambiguationExample(decisionRequest.mode, exampleCode));
|
|
763
|
+
}
|
|
727
764
|
|
|
728
765
|
return lines.join('\n');
|
|
729
766
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Startup migration: backfill channel='vellum' guardian binding.
|
|
3
|
+
*
|
|
4
|
+
* On runtime start, ensures that a guardian binding exists for the
|
|
5
|
+
* 'vellum' channel with a guardianPrincipalId. This is required for
|
|
6
|
+
* the identity-bound hatch bootstrap flow.
|
|
7
|
+
*
|
|
8
|
+
* - If a vellum binding already exists, no-op.
|
|
9
|
+
* - If no vellum binding exists, creates one with a fresh principal.
|
|
10
|
+
* - Preserves existing guardian bindings for other channels unchanged.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { v4 as uuid } from 'uuid';
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
createBinding,
|
|
17
|
+
getActiveBinding,
|
|
18
|
+
} from '../memory/guardian-bindings.js';
|
|
19
|
+
import { getLogger } from '../util/logger.js';
|
|
20
|
+
import { DAEMON_INTERNAL_ASSISTANT_ID } from './assistant-scope.js';
|
|
21
|
+
|
|
22
|
+
const log = getLogger('guardian-vellum-migration');
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Ensure a vellum guardian binding exists for the given assistant.
|
|
26
|
+
* Called during daemon startup to backfill existing installations.
|
|
27
|
+
*
|
|
28
|
+
* Returns the guardianPrincipalId (existing or newly created).
|
|
29
|
+
*/
|
|
30
|
+
export function ensureVellumGuardianBinding(assistantId: string = DAEMON_INTERNAL_ASSISTANT_ID): string {
|
|
31
|
+
const existing = getActiveBinding(assistantId, 'vellum');
|
|
32
|
+
if (existing) {
|
|
33
|
+
log.debug(
|
|
34
|
+
{ assistantId, guardianPrincipalId: existing.guardianExternalUserId },
|
|
35
|
+
'Vellum guardian binding already exists',
|
|
36
|
+
);
|
|
37
|
+
return existing.guardianExternalUserId;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const guardianPrincipalId = `vellum-principal-${uuid()}`;
|
|
41
|
+
|
|
42
|
+
createBinding({
|
|
43
|
+
assistantId,
|
|
44
|
+
channel: 'vellum',
|
|
45
|
+
guardianExternalUserId: guardianPrincipalId,
|
|
46
|
+
guardianDeliveryChatId: 'local',
|
|
47
|
+
verifiedVia: 'startup-migration',
|
|
48
|
+
metadataJson: JSON.stringify({ migratedAt: Date.now() }),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
log.info(
|
|
52
|
+
{ assistantId, guardianPrincipalId },
|
|
53
|
+
'Backfilled vellum guardian binding on startup',
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
return guardianPrincipalId;
|
|
57
|
+
}
|
|
@@ -40,6 +40,7 @@ import { consumeCallback, consumeCallbackError } from '../security/oauth-callbac
|
|
|
40
40
|
import { getLogger } from '../util/logger.js';
|
|
41
41
|
import { buildAssistantEvent } from './assistant-event.js';
|
|
42
42
|
import { assistantEventHub } from './assistant-event-hub.js';
|
|
43
|
+
import { DAEMON_INTERNAL_ASSISTANT_ID } from './assistant-scope.js';
|
|
43
44
|
import { sweepFailedEvents } from './channel-retry-sweep.js';
|
|
44
45
|
import { httpError } from './http-errors.js';
|
|
45
46
|
// Middleware
|
|
@@ -86,6 +87,7 @@ import {
|
|
|
86
87
|
handleGetAttachmentContent,
|
|
87
88
|
handleUploadAttachment,
|
|
88
89
|
} from './routes/attachment-routes.js';
|
|
90
|
+
import { handleGetBrainGraph, handleServeBrainGraphUI, handleServeHomeBaseUI } from './routes/brain-graph-routes.js';
|
|
89
91
|
import {
|
|
90
92
|
handleAnswerCall,
|
|
91
93
|
handleCancelCall,
|
|
@@ -97,7 +99,10 @@ import {
|
|
|
97
99
|
startCanonicalGuardianExpirySweep,
|
|
98
100
|
stopCanonicalGuardianExpirySweep,
|
|
99
101
|
} from './routes/canonical-guardian-expiry-sweep.js';
|
|
100
|
-
import {
|
|
102
|
+
import {
|
|
103
|
+
handleGetChannelReadiness,
|
|
104
|
+
handleRefreshChannelReadiness,
|
|
105
|
+
} from './routes/channel-readiness-routes.js';
|
|
101
106
|
import {
|
|
102
107
|
handleChannelDeliveryAck,
|
|
103
108
|
handleChannelInbound,
|
|
@@ -126,6 +131,7 @@ import {
|
|
|
126
131
|
handleGuardianActionDecision,
|
|
127
132
|
handleGuardianActionsPending,
|
|
128
133
|
} from './routes/guardian-action-routes.js';
|
|
134
|
+
import { handleGuardianBootstrap } from './routes/guardian-bootstrap-routes.js';
|
|
129
135
|
import { handleGetIdentity,handleHealth } from './routes/identity-routes.js';
|
|
130
136
|
import {
|
|
131
137
|
handleBlockMember,
|
|
@@ -160,6 +166,21 @@ import {
|
|
|
160
166
|
handlePairingStatus,
|
|
161
167
|
} from './routes/pairing-routes.js';
|
|
162
168
|
import { handleAddSecret } from './routes/secret-routes.js';
|
|
169
|
+
import {
|
|
170
|
+
handleAssignTwilioNumber,
|
|
171
|
+
handleClearTwilioCredentials,
|
|
172
|
+
handleDeleteTollfreeVerification,
|
|
173
|
+
handleGetSmsCompliance,
|
|
174
|
+
handleGetTwilioConfig,
|
|
175
|
+
handleListTwilioNumbers,
|
|
176
|
+
handleProvisionTwilioNumber,
|
|
177
|
+
handleReleaseTwilioNumber,
|
|
178
|
+
handleSetTwilioCredentials,
|
|
179
|
+
handleSmsDoctor,
|
|
180
|
+
handleSmsSendTest,
|
|
181
|
+
handleSubmitTollfreeVerification,
|
|
182
|
+
handleUpdateTollfreeVerification,
|
|
183
|
+
} from './routes/twilio-routes.js';
|
|
163
184
|
|
|
164
185
|
// Re-export for consumers
|
|
165
186
|
export { isPrivateAddress } from './middleware/auth.js';
|
|
@@ -270,7 +291,7 @@ export class RuntimeHttpServer {
|
|
|
270
291
|
ipcBroadcast(msg);
|
|
271
292
|
// Also publish to the event hub so HTTP/SSE clients (e.g. macOS
|
|
272
293
|
// app with localHttpEnabled) receive pairing approval requests.
|
|
273
|
-
void assistantEventHub.publish(buildAssistantEvent(
|
|
294
|
+
void assistantEventHub.publish(buildAssistantEvent(DAEMON_INTERNAL_ASSISTANT_ID, msg));
|
|
274
295
|
}
|
|
275
296
|
: undefined,
|
|
276
297
|
};
|
|
@@ -445,7 +466,7 @@ export class RuntimeHttpServer {
|
|
|
445
466
|
return rateLimitResponse(result);
|
|
446
467
|
}
|
|
447
468
|
// Attach rate limit headers to the eventual response
|
|
448
|
-
const originalResponse = await this.handleAuthenticatedRequest(req, url, path);
|
|
469
|
+
const originalResponse = await this.handleAuthenticatedRequest(req, url, path, server);
|
|
449
470
|
const headers = new Headers(originalResponse.headers);
|
|
450
471
|
for (const [k, v] of Object.entries(rateLimitHeaders(result))) {
|
|
451
472
|
headers.set(k, v);
|
|
@@ -457,13 +478,13 @@ export class RuntimeHttpServer {
|
|
|
457
478
|
});
|
|
458
479
|
}
|
|
459
480
|
|
|
460
|
-
return this.handleAuthenticatedRequest(req, url, path);
|
|
481
|
+
return this.handleAuthenticatedRequest(req, url, path, server);
|
|
461
482
|
}
|
|
462
483
|
|
|
463
484
|
/**
|
|
464
485
|
* Handle requests that have already passed auth and rate limiting.
|
|
465
486
|
*/
|
|
466
|
-
private async handleAuthenticatedRequest(req: Request, url: URL, path: string): Promise<Response> {
|
|
487
|
+
private async handleAuthenticatedRequest(req: Request, url: URL, path: string, server: ReturnType<typeof Bun.serve>): Promise<Response> {
|
|
467
488
|
// Pairing registration (bearer-authenticated)
|
|
468
489
|
if (path === '/v1/pairing/register' && req.method === 'POST') {
|
|
469
490
|
return await handlePairingRegister(req, this.pairingContext);
|
|
@@ -521,22 +542,13 @@ export class RuntimeHttpServer {
|
|
|
521
542
|
}
|
|
522
543
|
}
|
|
523
544
|
|
|
524
|
-
//
|
|
525
|
-
const
|
|
526
|
-
if (
|
|
527
|
-
return this.dispatchEndpoint(
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
// Legacy: /v1/assistants/:assistantId/<endpoint>
|
|
531
|
-
const match = path.match(/^\/v1\/assistants\/([^/]+)\/(.+)$/);
|
|
532
|
-
if (!match) {
|
|
533
|
-
return httpError('NOT_FOUND', 'Not found', 404);
|
|
545
|
+
// Runtime routes: /v1/<endpoint>
|
|
546
|
+
const routeMatch = path.match(/^\/v1\/(.+)$/);
|
|
547
|
+
if (routeMatch) {
|
|
548
|
+
return this.dispatchEndpoint(routeMatch[1], req, url, server);
|
|
534
549
|
}
|
|
535
550
|
|
|
536
|
-
|
|
537
|
-
const endpoint = match[2];
|
|
538
|
-
log.warn({ endpoint, assistantId }, '[deprecated] /v1/assistants/:assistantId/... route used; migrate to /v1/...');
|
|
539
|
-
return this.dispatchEndpoint(endpoint, req, url, assistantId);
|
|
551
|
+
return httpError('NOT_FOUND', 'Not found', 404);
|
|
540
552
|
}
|
|
541
553
|
|
|
542
554
|
private handleBrowserRelayUpgrade(req: Request, server: ReturnType<typeof Bun.serve>): Response {
|
|
@@ -616,8 +628,9 @@ export class RuntimeHttpServer {
|
|
|
616
628
|
endpoint: string,
|
|
617
629
|
req: Request,
|
|
618
630
|
url: URL,
|
|
619
|
-
|
|
631
|
+
server: ReturnType<typeof Bun.serve>,
|
|
620
632
|
): Promise<Response> {
|
|
633
|
+
const assistantId = DAEMON_INTERNAL_ASSISTANT_ID;
|
|
621
634
|
return withErrorHandling(endpoint, async () => {
|
|
622
635
|
if (endpoint === 'health' && req.method === 'GET') return handleHealth();
|
|
623
636
|
if (endpoint === 'debug' && req.method === 'GET') return handleDebug();
|
|
@@ -690,7 +703,7 @@ export class RuntimeHttpServer {
|
|
|
690
703
|
try {
|
|
691
704
|
recordConversationSeenSignal({
|
|
692
705
|
conversationId,
|
|
693
|
-
assistantId:
|
|
706
|
+
assistantId: DAEMON_INTERNAL_ASSISTANT_ID,
|
|
694
707
|
sourceChannel: (body.sourceChannel as string) ?? 'vellum',
|
|
695
708
|
signalType: (body.signalType as string ?? 'macos_conversation_opened') as SignalType,
|
|
696
709
|
confidence: (body.confidence as string ?? 'explicit') as Confidence,
|
|
@@ -715,18 +728,18 @@ export class RuntimeHttpServer {
|
|
|
715
728
|
persistAndProcessMessage: this.persistAndProcessMessage,
|
|
716
729
|
sendMessageDeps: this.sendMessageDeps,
|
|
717
730
|
approvalConversationGenerator: this.approvalConversationGenerator,
|
|
718
|
-
});
|
|
731
|
+
}, server);
|
|
719
732
|
}
|
|
720
733
|
|
|
721
734
|
// Standalone approval endpoints — keyed by requestId, orthogonal to message sending
|
|
722
|
-
if (endpoint === 'confirm' && req.method === 'POST') return await handleConfirm(req);
|
|
723
|
-
if (endpoint === 'secret' && req.method === 'POST') return await handleSecret(req);
|
|
724
|
-
if (endpoint === 'trust-rules' && req.method === 'POST') return await handleTrustRule(req);
|
|
725
|
-
if (endpoint === 'pending-interactions' && req.method === 'GET') return handleListPendingInteractions(url);
|
|
735
|
+
if (endpoint === 'confirm' && req.method === 'POST') return await handleConfirm(req, server);
|
|
736
|
+
if (endpoint === 'secret' && req.method === 'POST') return await handleSecret(req, server);
|
|
737
|
+
if (endpoint === 'trust-rules' && req.method === 'POST') return await handleTrustRule(req, server);
|
|
738
|
+
if (endpoint === 'pending-interactions' && req.method === 'GET') return handleListPendingInteractions(url, req, server);
|
|
726
739
|
|
|
727
740
|
// Guardian action endpoints — deterministic button-based decisions
|
|
728
|
-
if (endpoint === 'guardian-actions/pending' && req.method === 'GET') return handleGuardianActionsPending(req);
|
|
729
|
-
if (endpoint === 'guardian-actions/decision' && req.method === 'POST') return await handleGuardianActionDecision(req);
|
|
741
|
+
if (endpoint === 'guardian-actions/pending' && req.method === 'GET') return handleGuardianActionsPending(req, server);
|
|
742
|
+
if (endpoint === 'guardian-actions/decision' && req.method === 'POST') return await handleGuardianActionDecision(req, server);
|
|
730
743
|
|
|
731
744
|
// Contacts
|
|
732
745
|
if (endpoint === 'contacts' && req.method === 'GET') return handleListContacts(url);
|
|
@@ -768,6 +781,34 @@ export class RuntimeHttpServer {
|
|
|
768
781
|
if (endpoint === 'integrations/guardian/outbound/resend' && req.method === 'POST') return await handleResendOutbound(req);
|
|
769
782
|
if (endpoint === 'integrations/guardian/outbound/cancel' && req.method === 'POST') return await handleCancelOutbound(req);
|
|
770
783
|
|
|
784
|
+
// Guardian vellum channel bootstrap
|
|
785
|
+
if (endpoint === 'integrations/guardian/vellum/bootstrap' && req.method === 'POST') return await handleGuardianBootstrap(req, server);
|
|
786
|
+
|
|
787
|
+
// Integrations — Twilio config
|
|
788
|
+
if (endpoint === 'integrations/twilio/config' && req.method === 'GET') return handleGetTwilioConfig();
|
|
789
|
+
if (endpoint === 'integrations/twilio/credentials' && req.method === 'POST') return await handleSetTwilioCredentials(req);
|
|
790
|
+
if (endpoint === 'integrations/twilio/credentials' && req.method === 'DELETE') return handleClearTwilioCredentials();
|
|
791
|
+
if (endpoint === 'integrations/twilio/numbers' && req.method === 'GET') return await handleListTwilioNumbers();
|
|
792
|
+
if (endpoint === 'integrations/twilio/numbers/provision' && req.method === 'POST') return await handleProvisionTwilioNumber(req);
|
|
793
|
+
if (endpoint === 'integrations/twilio/numbers/assign' && req.method === 'POST') return await handleAssignTwilioNumber(req);
|
|
794
|
+
if (endpoint === 'integrations/twilio/numbers/release' && req.method === 'POST') return await handleReleaseTwilioNumber(req);
|
|
795
|
+
if (endpoint === 'integrations/twilio/sms/compliance' && req.method === 'GET') return await handleGetSmsCompliance();
|
|
796
|
+
if (endpoint === 'integrations/twilio/sms/compliance/tollfree' && req.method === 'POST') return await handleSubmitTollfreeVerification(req);
|
|
797
|
+
if (endpoint === 'integrations/twilio/sms/test' && req.method === 'POST') return await handleSmsSendTest(req);
|
|
798
|
+
if (endpoint === 'integrations/twilio/sms/doctor' && req.method === 'POST') return await handleSmsDoctor();
|
|
799
|
+
|
|
800
|
+
// Twilio toll-free verification PATCH/DELETE with :verificationSid
|
|
801
|
+
const tollfreeVerificationMatch = endpoint.match(/^integrations\/twilio\/sms\/compliance\/tollfree\/([^/]+)$/);
|
|
802
|
+
if (tollfreeVerificationMatch) {
|
|
803
|
+
const verificationSid = tollfreeVerificationMatch[1];
|
|
804
|
+
if (req.method === 'PATCH') return await handleUpdateTollfreeVerification(req, verificationSid);
|
|
805
|
+
if (req.method === 'DELETE') return await handleDeleteTollfreeVerification(verificationSid);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// Channel readiness
|
|
809
|
+
if (endpoint === 'channels/readiness' && req.method === 'GET') return await handleGetChannelReadiness(url);
|
|
810
|
+
if (endpoint === 'channels/readiness/refresh' && req.method === 'POST') return await handleRefreshChannelReadiness(req);
|
|
811
|
+
|
|
771
812
|
if (endpoint === 'attachments' && req.method === 'POST') return await handleUploadAttachment(req);
|
|
772
813
|
if (endpoint === 'attachments' && req.method === 'DELETE') return await handleDeleteAttachment(req);
|
|
773
814
|
|
|
@@ -813,11 +854,11 @@ export class RuntimeHttpServer {
|
|
|
813
854
|
|
|
814
855
|
// Internal Twilio forwarding endpoints (gateway -> runtime)
|
|
815
856
|
if (endpoint === 'internal/twilio/voice-webhook' && req.method === 'POST') {
|
|
816
|
-
const json = await req.json() as { params: Record<string, string>; originalUrl?: string
|
|
857
|
+
const json = await req.json() as { params: Record<string, string>; originalUrl?: string };
|
|
817
858
|
const formBody = new URLSearchParams(json.params).toString();
|
|
818
859
|
const reconstructedUrl = json.originalUrl ?? req.url;
|
|
819
860
|
const fakeReq = new Request(reconstructedUrl, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: formBody });
|
|
820
|
-
return await handleVoiceWebhook(fakeReq
|
|
861
|
+
return await handleVoiceWebhook(fakeReq);
|
|
821
862
|
}
|
|
822
863
|
|
|
823
864
|
if (endpoint === 'internal/twilio/status' && req.method === 'POST') {
|
|
@@ -835,7 +876,10 @@ export class RuntimeHttpServer {
|
|
|
835
876
|
}
|
|
836
877
|
|
|
837
878
|
if (endpoint === 'identity' && req.method === 'GET') return handleGetIdentity();
|
|
838
|
-
if (endpoint === '
|
|
879
|
+
if (endpoint === 'brain-graph' && req.method === 'GET') return handleGetBrainGraph();
|
|
880
|
+
if (endpoint === 'brain-graph-ui' && req.method === 'GET') return handleServeBrainGraphUI(this.bearerToken);
|
|
881
|
+
if (endpoint === 'home-base-ui' && req.method === 'GET') return handleServeHomeBaseUI(this.bearerToken);
|
|
882
|
+
if (endpoint === 'events' && req.method === 'GET') return handleSubscribeAssistantEvents(req, url, { server });
|
|
839
883
|
|
|
840
884
|
// Internal OAuth callback endpoint (gateway -> runtime)
|
|
841
885
|
if (endpoint === 'internal/oauth/callback' && req.method === 'POST') {
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared types for the runtime HTTP server and its route handlers.
|
|
3
3
|
*/
|
|
4
|
+
import type {
|
|
5
|
+
CallPointerMessageContext,
|
|
6
|
+
ComposeCallPointerMessageOptions,
|
|
7
|
+
} from '../calls/call-pointer-message-composer.js';
|
|
4
8
|
import type { ChannelId, InterfaceId } from '../channels/types.js';
|
|
5
9
|
import type { Session } from '../daemon/session.js';
|
|
6
10
|
import type { GuardianRuntimeContext } from '../daemon/session-runtime-assembly.js';
|
|
@@ -55,6 +59,15 @@ export type ApprovalConversationGenerator = (
|
|
|
55
59
|
context: ApprovalConversationContext,
|
|
56
60
|
) => Promise<ApprovalConversationResult>;
|
|
57
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Daemon-injected function that generates call pointer copy using a provider.
|
|
64
|
+
* Returns generated text or `null` on failure (caller falls back to deterministic text).
|
|
65
|
+
*/
|
|
66
|
+
export type PointerCopyGenerator = (
|
|
67
|
+
context: CallPointerMessageContext,
|
|
68
|
+
options?: ComposeCallPointerMessageOptions,
|
|
69
|
+
) => Promise<string | null>;
|
|
70
|
+
|
|
58
71
|
/**
|
|
59
72
|
* Daemon-injected function that generates guardian action copy using a provider.
|
|
60
73
|
* Returns generated text or `null` on failure (caller falls back to deterministic text).
|
|
@@ -57,6 +57,8 @@ export interface InviteResponseData {
|
|
|
57
57
|
expectedExternalUserId?: string;
|
|
58
58
|
voiceCode?: string;
|
|
59
59
|
voiceCodeDigits?: number;
|
|
60
|
+
friendName?: string;
|
|
61
|
+
guardianName?: string;
|
|
60
62
|
createdAt: number;
|
|
61
63
|
}
|
|
62
64
|
|
|
@@ -110,6 +112,8 @@ function inviteToResponse(inv: IngressInvite, opts?: { rawToken?: string; voiceC
|
|
|
110
112
|
...(inv.expectedExternalUserId ? { expectedExternalUserId: inv.expectedExternalUserId } : {}),
|
|
111
113
|
...(opts?.voiceCode ? { voiceCode: opts.voiceCode } : {}),
|
|
112
114
|
...(inv.voiceCodeDigits != null ? { voiceCodeDigits: inv.voiceCodeDigits } : {}),
|
|
115
|
+
...(inv.friendName ? { friendName: inv.friendName } : {}),
|
|
116
|
+
...(inv.guardianName ? { guardianName: inv.guardianName } : {}),
|
|
113
117
|
createdAt: inv.createdAt,
|
|
114
118
|
};
|
|
115
119
|
}
|
|
@@ -149,6 +153,8 @@ export function createIngressInvite(params: {
|
|
|
149
153
|
// Voice invite parameters
|
|
150
154
|
expectedExternalUserId?: string;
|
|
151
155
|
voiceCodeDigits?: number;
|
|
156
|
+
friendName?: string;
|
|
157
|
+
guardianName?: string;
|
|
152
158
|
}): IngressResult<InviteResponseData> {
|
|
153
159
|
if (!params.sourceChannel) {
|
|
154
160
|
return { ok: false, error: 'sourceChannel is required for create' };
|
|
@@ -168,6 +174,12 @@ export function createIngressInvite(params: {
|
|
|
168
174
|
if (!isValidE164(params.expectedExternalUserId)) {
|
|
169
175
|
return { ok: false, error: 'expectedExternalUserId must be in E.164 format (e.g., +15551234567)' };
|
|
170
176
|
}
|
|
177
|
+
if (typeof params.friendName !== 'string' || !params.friendName.trim()) {
|
|
178
|
+
return { ok: false, error: 'friendName is required for voice invites' };
|
|
179
|
+
}
|
|
180
|
+
if (typeof params.guardianName !== 'string' || !params.guardianName.trim()) {
|
|
181
|
+
return { ok: false, error: 'guardianName is required for voice invites' };
|
|
182
|
+
}
|
|
171
183
|
voiceCode = generateVoiceCode(6);
|
|
172
184
|
voiceCodeHash = hashVoiceCode(voiceCode);
|
|
173
185
|
}
|
|
@@ -181,6 +193,8 @@ export function createIngressInvite(params: {
|
|
|
181
193
|
expectedExternalUserId: params.expectedExternalUserId,
|
|
182
194
|
voiceCodeHash,
|
|
183
195
|
voiceCodeDigits: 6,
|
|
196
|
+
friendName: params.friendName,
|
|
197
|
+
guardianName: params.guardianName,
|
|
184
198
|
} : {}),
|
|
185
199
|
});
|
|
186
200
|
// Voice invites must not expose the token — callers must redeem via the
|
|
@@ -13,6 +13,7 @@ import { findActiveVoiceInvites,findByTokenHash, hashToken, markInviteExpired, r
|
|
|
13
13
|
import { findMember, upsertMember } from '../memory/ingress-member-store.js';
|
|
14
14
|
import { canonicalizeInboundIdentity } from '../util/canonicalize-identity.js';
|
|
15
15
|
import { hashVoiceCode } from '../util/voice-code.js';
|
|
16
|
+
import { DAEMON_INTERNAL_ASSISTANT_ID } from './assistant-scope.js';
|
|
16
17
|
|
|
17
18
|
// ---------------------------------------------------------------------------
|
|
18
19
|
// Outcome type
|
|
@@ -223,7 +224,7 @@ export function redeemVoiceInviteCode(params: {
|
|
|
223
224
|
sourceChannel: 'voice';
|
|
224
225
|
code: string;
|
|
225
226
|
}): VoiceRedemptionOutcome {
|
|
226
|
-
const { assistantId =
|
|
227
|
+
const { assistantId = DAEMON_INTERNAL_ASSISTANT_ID, callerExternalUserId, code } = params;
|
|
227
228
|
|
|
228
229
|
if (!callerExternalUserId) {
|
|
229
230
|
return { ok: false, reason: 'invalid_or_expired' };
|
|
@@ -285,12 +286,20 @@ export function redeemVoiceInviteCode(params: {
|
|
|
285
286
|
const STALE_INVITE = Symbol('stale_invite');
|
|
286
287
|
let memberId: string | undefined;
|
|
287
288
|
|
|
289
|
+
// Reactivation should not overwrite a guardian-managed nickname (same
|
|
290
|
+
// protection as the token-based redemption path above).
|
|
291
|
+
const preservedDisplayName = existingMember?.displayName?.trim().length
|
|
292
|
+
? existingMember.displayName
|
|
293
|
+
: (invite.friendName ?? undefined);
|
|
294
|
+
|
|
288
295
|
try {
|
|
289
296
|
getSqlite().transaction(() => {
|
|
290
297
|
const member = upsertMember({
|
|
291
298
|
assistantId: invite.assistantId,
|
|
292
299
|
sourceChannel: 'voice',
|
|
293
300
|
externalUserId: callerExternalUserId,
|
|
301
|
+
externalChatId: callerExternalUserId,
|
|
302
|
+
displayName: preservedDisplayName,
|
|
294
303
|
status: 'active',
|
|
295
304
|
policy: 'allow',
|
|
296
305
|
inviteId: invite.id,
|