@vellumai/assistant 0.3.27 → 0.4.0
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 +81 -4
- package/Dockerfile +2 -2
- package/bun.lock +4 -1
- package/docs/trusted-contact-access.md +9 -2
- package/package.json +6 -3
- package/scripts/ipc/generate-swift.ts +9 -5
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +80 -0
- package/src/__tests__/agent-loop-thinking.test.ts +1 -1
- package/src/__tests__/agent-loop.test.ts +119 -0
- 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__/bundled-asset.test.ts +107 -0
- 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__/canonical-guardian-store.test.ts +636 -0
- package/src/__tests__/channel-approval-routes.test.ts +174 -1
- 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__/emit-signal-routing-intent.test.ts +43 -1
- 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-actions-endpoint.test.ts +205 -345
- package/src/__tests__/guardian-control-plane-policy.test.ts +19 -19
- package/src/__tests__/guardian-decision-primitive-canonical.test.ts +599 -0
- package/src/__tests__/guardian-dispatch.test.ts +21 -19
- 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 +1092 -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__/mcp-cli.test.ts +77 -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 +212 -36
- package/src/__tests__/notification-decision-fallback.test.ts +63 -3
- package/src/__tests__/notification-decision-strategy.test.ts +78 -0
- package/src/__tests__/notification-guardian-path.test.ts +15 -15
- 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__/onboarding-template-contract.test.ts +116 -21
- 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__/secret-scanner-executor.test.ts +59 -0
- package/src/__tests__/secret-scanner.test.ts +8 -0
- package/src/__tests__/send-endpoint-busy.test.ts +4 -0
- package/src/__tests__/sensitive-output-placeholders.test.ts +208 -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 +126 -59
- 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 +497 -0
- 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/agent/loop.ts +46 -3
- package/src/approvals/guardian-decision-primitive.ts +285 -0
- package/src/approvals/guardian-request-resolvers.ts +539 -0
- package/src/calls/call-controller.ts +26 -23
- package/src/calls/guardian-action-sweep.ts +10 -2
- package/src/calls/guardian-dispatch.ts +46 -40
- package/src/calls/relay-server.ts +358 -24
- 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/system-prompt.ts +2 -1
- package/src/config/templates/BOOTSTRAP.md +47 -31
- package/src/config/templates/USER.md +5 -0
- package/src/config/update-bulletin-template-path.ts +4 -1
- package/src/config/vellum-skills/trusted-contacts/SKILL.md +149 -21
- package/src/daemon/handlers/config-inbox.ts +4 -4
- package/src/daemon/handlers/guardian-actions.ts +45 -66
- package/src/daemon/handlers/sessions.ts +148 -4
- package/src/daemon/ipc-contract/guardian-actions.ts +7 -0
- package/src/daemon/ipc-contract/messages.ts +16 -0
- package/src/daemon/ipc-contract-inventory.json +1 -0
- package/src/daemon/lifecycle.ts +22 -16
- package/src/daemon/pairing-store.ts +86 -3
- package/src/daemon/server.ts +18 -0
- package/src/daemon/session-agent-loop-handlers.ts +5 -4
- package/src/daemon/session-agent-loop.ts +33 -6
- package/src/daemon/session-lifecycle.ts +25 -17
- package/src/daemon/session-memory.ts +2 -2
- package/src/daemon/session-process.ts +68 -326
- package/src/daemon/session-runtime-assembly.ts +119 -25
- package/src/daemon/session-tool-setup.ts +3 -2
- package/src/daemon/session.ts +4 -3
- package/src/home-base/prebuilt/seed.ts +2 -1
- package/src/hooks/templates.ts +2 -1
- package/src/memory/canonical-guardian-store.ts +586 -0
- package/src/memory/channel-guardian-store.ts +2 -0
- package/src/memory/conversation-crud.ts +7 -7
- package/src/memory/db-init.ts +20 -0
- package/src/memory/embedding-local.ts +257 -39
- package/src/memory/embedding-runtime-manager.ts +471 -0
- package/src/memory/guardian-action-store.ts +7 -60
- package/src/memory/guardian-approvals.ts +9 -4
- 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/036-normalize-phone-identities.ts +289 -0
- package/src/memory/migrations/037-voice-invite-columns.ts +16 -0
- package/src/memory/migrations/118-reminder-routing-intent.ts +3 -3
- package/src/memory/migrations/121-canonical-guardian-requests.ts +59 -0
- package/src/memory/migrations/122-canonical-guardian-requester-chat-id.ts +15 -0
- package/src/memory/migrations/123-canonical-guardian-deliveries-destination-index.ts +15 -0
- package/src/memory/migrations/index.ts +5 -0
- package/src/memory/migrations/registry.ts +5 -0
- package/src/memory/qdrant-client.ts +31 -22
- package/src/memory/schema-migration.ts +1 -0
- package/src/memory/schema.ts +56 -0
- package/src/notifications/copy-composer.ts +31 -4
- package/src/notifications/decision-engine.ts +57 -0
- package/src/permissions/defaults.ts +2 -0
- package/src/runtime/access-request-helper.ts +173 -0
- package/src/runtime/actor-trust-resolver.ts +221 -0
- package/src/runtime/channel-guardian-service.ts +12 -4
- 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 -71
- package/src/runtime/guardian-decision-types.ts +6 -0
- package/src/runtime/guardian-reply-router.ts +717 -0
- package/src/runtime/http-server.ts +8 -0
- package/src/runtime/ingress-service.ts +80 -3
- package/src/runtime/invite-redemption-service.ts +141 -2
- package/src/runtime/routes/canonical-guardian-expiry-sweep.ts +116 -0
- 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 +20 -2
- package/src/runtime/routes/guardian-action-routes.ts +100 -109
- package/src/runtime/routes/guardian-approval-interception.ts +17 -6
- package/src/runtime/routes/inbound-message-handler.ts +205 -529
- package/src/runtime/routes/ingress-routes.ts +52 -4
- package/src/runtime/routes/pairing-routes.ts +3 -0
- package/src/runtime/tool-grant-request-helper.ts +195 -0
- package/src/tools/executor.ts +13 -1
- package/src/tools/guardian-control-plane-policy.ts +2 -2
- package/src/tools/sensitive-output-placeholders.ts +203 -0
- package/src/tools/tool-approval-handler.ts +53 -10
- package/src/tools/types.ts +13 -2
- package/src/util/bundled-asset.ts +31 -0
- package/src/util/canonicalize-identity.ts +52 -0
- 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
package/src/memory/schema.ts
CHANGED
|
@@ -874,6 +874,58 @@ export const guardianActionDeliveries = sqliteTable('guardian_action_deliveries'
|
|
|
874
874
|
index('idx_guardian_action_deliveries_dest_conversation').on(table.destinationConversationId),
|
|
875
875
|
]);
|
|
876
876
|
|
|
877
|
+
// ── Canonical Guardian Requests (unified cross-source guardian domain) ─
|
|
878
|
+
|
|
879
|
+
export const canonicalGuardianRequests = sqliteTable('canonical_guardian_requests', {
|
|
880
|
+
id: text('id').primaryKey(),
|
|
881
|
+
kind: text('kind').notNull(),
|
|
882
|
+
sourceType: text('source_type').notNull(),
|
|
883
|
+
sourceChannel: text('source_channel'),
|
|
884
|
+
conversationId: text('conversation_id'),
|
|
885
|
+
requesterExternalUserId: text('requester_external_user_id'),
|
|
886
|
+
requesterChatId: text('requester_chat_id'),
|
|
887
|
+
guardianExternalUserId: text('guardian_external_user_id'),
|
|
888
|
+
callSessionId: text('call_session_id'),
|
|
889
|
+
pendingQuestionId: text('pending_question_id'),
|
|
890
|
+
questionText: text('question_text'),
|
|
891
|
+
requestCode: text('request_code'),
|
|
892
|
+
toolName: text('tool_name'),
|
|
893
|
+
inputDigest: text('input_digest'),
|
|
894
|
+
status: text('status').notNull().default('pending'),
|
|
895
|
+
answerText: text('answer_text'),
|
|
896
|
+
decidedByExternalUserId: text('decided_by_external_user_id'),
|
|
897
|
+
followupState: text('followup_state'),
|
|
898
|
+
expiresAt: text('expires_at'),
|
|
899
|
+
createdAt: text('created_at').notNull(),
|
|
900
|
+
updatedAt: text('updated_at').notNull(),
|
|
901
|
+
}, (table) => [
|
|
902
|
+
index('idx_canonical_guardian_requests_status').on(table.status),
|
|
903
|
+
index('idx_canonical_guardian_requests_guardian').on(table.guardianExternalUserId, table.status),
|
|
904
|
+
index('idx_canonical_guardian_requests_conversation').on(table.conversationId, table.status),
|
|
905
|
+
index('idx_canonical_guardian_requests_source').on(table.sourceType, table.status),
|
|
906
|
+
index('idx_canonical_guardian_requests_kind').on(table.kind, table.status),
|
|
907
|
+
index('idx_canonical_guardian_requests_request_code').on(table.requestCode),
|
|
908
|
+
]);
|
|
909
|
+
|
|
910
|
+
// ── Canonical Guardian Deliveries (per-channel delivery tracking) ─────
|
|
911
|
+
|
|
912
|
+
export const canonicalGuardianDeliveries = sqliteTable('canonical_guardian_deliveries', {
|
|
913
|
+
id: text('id').primaryKey(),
|
|
914
|
+
requestId: text('request_id')
|
|
915
|
+
.notNull()
|
|
916
|
+
.references(() => canonicalGuardianRequests.id, { onDelete: 'cascade' }),
|
|
917
|
+
destinationChannel: text('destination_channel').notNull(),
|
|
918
|
+
destinationConversationId: text('destination_conversation_id'),
|
|
919
|
+
destinationChatId: text('destination_chat_id'),
|
|
920
|
+
destinationMessageId: text('destination_message_id'),
|
|
921
|
+
status: text('status').notNull().default('pending'),
|
|
922
|
+
createdAt: text('created_at').notNull(),
|
|
923
|
+
updatedAt: text('updated_at').notNull(),
|
|
924
|
+
}, (table) => [
|
|
925
|
+
index('idx_canonical_guardian_deliveries_request_id').on(table.requestId),
|
|
926
|
+
index('idx_canonical_guardian_deliveries_status').on(table.status),
|
|
927
|
+
]);
|
|
928
|
+
|
|
877
929
|
// ── Assistant Inbox ──────────────────────────────────────────────────
|
|
878
930
|
|
|
879
931
|
export const assistantIngressInvites = sqliteTable('assistant_ingress_invites', {
|
|
@@ -890,6 +942,10 @@ export const assistantIngressInvites = sqliteTable('assistant_ingress_invites',
|
|
|
890
942
|
redeemedByExternalUserId: text('redeemed_by_external_user_id'),
|
|
891
943
|
redeemedByExternalChatId: text('redeemed_by_external_chat_id'),
|
|
892
944
|
redeemedAt: integer('redeemed_at'),
|
|
945
|
+
// Voice invite fields (nullable — non-voice invites leave these NULL)
|
|
946
|
+
expectedExternalUserId: text('expected_external_user_id'),
|
|
947
|
+
voiceCodeHash: text('voice_code_hash'),
|
|
948
|
+
voiceCodeDigits: integer('voice_code_digits'),
|
|
893
949
|
createdAt: integer('created_at').notNull(),
|
|
894
950
|
updatedAt: integer('updated_at').notNull(),
|
|
895
951
|
});
|
|
@@ -37,10 +37,37 @@ const TEMPLATES: Record<string, CopyTemplate> = {
|
|
|
37
37
|
body: `${str(payload.name, 'A schedule')} has finished running`,
|
|
38
38
|
}),
|
|
39
39
|
|
|
40
|
-
'guardian.question': (payload) =>
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
'guardian.question': (payload) => {
|
|
41
|
+
const question = str(payload.questionText, 'A guardian question needs your attention');
|
|
42
|
+
const requestCode = nonEmpty(typeof payload.requestCode === 'string' ? payload.requestCode : undefined);
|
|
43
|
+
if (!requestCode) {
|
|
44
|
+
return {
|
|
45
|
+
title: 'Guardian Question',
|
|
46
|
+
body: question,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const normalizedCode = requestCode.toUpperCase();
|
|
51
|
+
return {
|
|
52
|
+
title: 'Guardian Question',
|
|
53
|
+
body: `${question}\n\nReference code: ${normalizedCode}. Reply "${normalizedCode} approve" or "${normalizedCode} reject".`,
|
|
54
|
+
};
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
'ingress.access_request': (payload) => {
|
|
58
|
+
const requester = str(payload.senderIdentifier, 'Someone');
|
|
59
|
+
const requestCode = nonEmpty(typeof payload.requestCode === 'string' ? payload.requestCode : undefined);
|
|
60
|
+
const lines: string[] = [`${requester} is requesting access to the assistant.`];
|
|
61
|
+
if (requestCode) {
|
|
62
|
+
const code = requestCode.toUpperCase();
|
|
63
|
+
lines.push(`Reply "${code} approve" to grant access or "${code} reject" to deny.`);
|
|
64
|
+
}
|
|
65
|
+
lines.push('Reply "open invite flow" to start Trusted Contacts invite flow.');
|
|
66
|
+
return {
|
|
67
|
+
title: 'Access Request',
|
|
68
|
+
body: lines.join('\n'),
|
|
69
|
+
};
|
|
70
|
+
},
|
|
44
71
|
|
|
45
72
|
'ingress.escalation': (payload) => ({
|
|
46
73
|
title: 'Escalation',
|
|
@@ -406,6 +406,61 @@ export function validateThreadActions(
|
|
|
406
406
|
return result;
|
|
407
407
|
}
|
|
408
408
|
|
|
409
|
+
function ensureGuardianRequestCodeInCopy(
|
|
410
|
+
copy: RenderedChannelCopy,
|
|
411
|
+
requestCode: string,
|
|
412
|
+
): RenderedChannelCopy {
|
|
413
|
+
const instruction = `Reference code: ${requestCode}. Reply "${requestCode} approve" or "${requestCode} reject".`;
|
|
414
|
+
const hasParserCompatibleInstructions = (text: string | undefined): boolean => {
|
|
415
|
+
if (typeof text !== 'string') return false;
|
|
416
|
+
const upper = text.toUpperCase();
|
|
417
|
+
return upper.includes(`${requestCode} APPROVE`) && upper.includes(`${requestCode} REJECT`);
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
const ensureText = (text: string | undefined): string => {
|
|
421
|
+
const base = typeof text === 'string' ? text.trim() : '';
|
|
422
|
+
if (hasParserCompatibleInstructions(base)) return base;
|
|
423
|
+
return base.length > 0 ? `${base}\n\n${instruction}` : instruction;
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
return {
|
|
427
|
+
...copy,
|
|
428
|
+
body: ensureText(copy.body),
|
|
429
|
+
deliveryText: copy.deliveryText ? ensureText(copy.deliveryText) : copy.deliveryText,
|
|
430
|
+
threadSeedMessage: copy.threadSeedMessage ? ensureText(copy.threadSeedMessage) : copy.threadSeedMessage,
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Guardian questions that share a conversation require explicit request-code
|
|
436
|
+
* targeting. Enforce request-code instructions in rendered copy so guardians
|
|
437
|
+
* can always disambiguate replies even when model copy omits them.
|
|
438
|
+
*/
|
|
439
|
+
function enforceGuardianRequestCode(
|
|
440
|
+
decision: NotificationDecision,
|
|
441
|
+
signal: NotificationSignal,
|
|
442
|
+
): NotificationDecision {
|
|
443
|
+
if (signal.sourceEventName !== 'guardian.question') return decision;
|
|
444
|
+
const rawCode = signal.contextPayload.requestCode;
|
|
445
|
+
if (typeof rawCode !== 'string' || rawCode.trim().length === 0) return decision;
|
|
446
|
+
|
|
447
|
+
const requestCode = rawCode.trim().toUpperCase();
|
|
448
|
+
const nextCopy: Partial<Record<NotificationChannel, RenderedChannelCopy>> = {
|
|
449
|
+
...decision.renderedCopy,
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
for (const channel of Object.keys(nextCopy) as NotificationChannel[]) {
|
|
453
|
+
const copy = nextCopy[channel];
|
|
454
|
+
if (!copy) continue;
|
|
455
|
+
nextCopy[channel] = ensureGuardianRequestCodeInCopy(copy, requestCode);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
return {
|
|
459
|
+
...decision,
|
|
460
|
+
renderedCopy: nextCopy,
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
|
|
409
464
|
// ── Core evaluation function ───────────────────────────────────────────
|
|
410
465
|
|
|
411
466
|
export async function evaluateSignal(
|
|
@@ -444,6 +499,7 @@ export async function evaluateSignal(
|
|
|
444
499
|
if (!provider) {
|
|
445
500
|
log.warn('Configured provider unavailable for notification decision, using fallback');
|
|
446
501
|
let decision = buildFallbackDecision(signal, availableChannels);
|
|
502
|
+
decision = enforceGuardianRequestCode(decision, signal);
|
|
447
503
|
decision = enforceConversationAffinity(decision, signal.conversationAffinityHint);
|
|
448
504
|
decision.persistedDecisionId = persistDecision(signal, decision);
|
|
449
505
|
return decision;
|
|
@@ -458,6 +514,7 @@ export async function evaluateSignal(
|
|
|
458
514
|
decision = buildFallbackDecision(signal, availableChannels);
|
|
459
515
|
}
|
|
460
516
|
|
|
517
|
+
decision = enforceGuardianRequestCode(decision, signal);
|
|
461
518
|
decision = enforceConversationAffinity(decision, signal.conversationAffinityHint);
|
|
462
519
|
decision.persistedDecisionId = persistDecision(signal, decision);
|
|
463
520
|
|
|
@@ -132,6 +132,7 @@ export function getDefaultRuleTemplates(): DefaultRuleTemplate[] {
|
|
|
132
132
|
scope: workspaceDir,
|
|
133
133
|
decision: 'allow',
|
|
134
134
|
priority: 100,
|
|
135
|
+
allowHighRisk: true,
|
|
135
136
|
};
|
|
136
137
|
|
|
137
138
|
const updatesDeleteRule: DefaultRuleTemplate = {
|
|
@@ -141,6 +142,7 @@ export function getDefaultRuleTemplates(): DefaultRuleTemplate[] {
|
|
|
141
142
|
scope: workspaceDir,
|
|
142
143
|
decision: 'allow',
|
|
143
144
|
priority: 100,
|
|
145
|
+
allowHighRisk: true,
|
|
144
146
|
};
|
|
145
147
|
|
|
146
148
|
// Skill source directories — writing or editing skill source files should
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared access-request creation and notification helper.
|
|
3
|
+
*
|
|
4
|
+
* Encapsulates the "create/dedupe canonical access request + emit notification"
|
|
5
|
+
* logic so both text-channel and voice-channel ingress paths use identical
|
|
6
|
+
* guardian notification flows.
|
|
7
|
+
*
|
|
8
|
+
* Access requests are a special case: they always create a canonical request
|
|
9
|
+
* and emit a notification signal, even when no same-channel guardian binding
|
|
10
|
+
* exists. Guardian binding resolution uses a fallback strategy:
|
|
11
|
+
* 1. Source-channel active binding first.
|
|
12
|
+
* 2. Any active binding for the assistant (deterministic, most-recently-verified).
|
|
13
|
+
* 3. No guardian identity (trusted/vellum-only resolution path).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { ChannelId } from '../channels/types.js';
|
|
17
|
+
import {
|
|
18
|
+
createCanonicalGuardianRequest,
|
|
19
|
+
listCanonicalGuardianRequests,
|
|
20
|
+
} from '../memory/canonical-guardian-store.js';
|
|
21
|
+
import { listActiveBindingsByAssistant } from '../memory/channel-guardian-store.js';
|
|
22
|
+
import { emitNotificationSignal } from '../notifications/emit-signal.js';
|
|
23
|
+
import { getLogger } from '../util/logger.js';
|
|
24
|
+
import { getGuardianBinding } from './channel-guardian-service.js';
|
|
25
|
+
import { GUARDIAN_APPROVAL_TTL_MS } from './routes/channel-route-shared.js';
|
|
26
|
+
|
|
27
|
+
const log = getLogger('access-request-helper');
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Types
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
export interface AccessRequestParams {
|
|
34
|
+
canonicalAssistantId: string;
|
|
35
|
+
sourceChannel: ChannelId;
|
|
36
|
+
externalChatId: string;
|
|
37
|
+
senderExternalUserId?: string;
|
|
38
|
+
senderName?: string;
|
|
39
|
+
senderUsername?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type AccessRequestResult =
|
|
43
|
+
| { notified: true; created: boolean; requestId: string }
|
|
44
|
+
| { notified: false; reason: 'no_sender_id' };
|
|
45
|
+
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Helper
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Create/dedupe a canonical access request and emit a notification signal
|
|
52
|
+
* so the guardian can approve or deny the unknown sender.
|
|
53
|
+
*
|
|
54
|
+
* Returns a result indicating whether the guardian was notified and whether
|
|
55
|
+
* a new request was created or an existing one was deduped.
|
|
56
|
+
*
|
|
57
|
+
* Guardian binding resolution: source-channel first, then any active binding
|
|
58
|
+
* for the assistant, then null (notification pipeline handles delivery via
|
|
59
|
+
* trusted/vellum channels when no binding exists).
|
|
60
|
+
*
|
|
61
|
+
* This is intentionally synchronous with respect to the canonical store writes
|
|
62
|
+
* and fire-and-forget for the notification signal emission.
|
|
63
|
+
*/
|
|
64
|
+
export function notifyGuardianOfAccessRequest(
|
|
65
|
+
params: AccessRequestParams,
|
|
66
|
+
): AccessRequestResult {
|
|
67
|
+
const {
|
|
68
|
+
canonicalAssistantId,
|
|
69
|
+
sourceChannel,
|
|
70
|
+
externalChatId,
|
|
71
|
+
senderExternalUserId,
|
|
72
|
+
senderName,
|
|
73
|
+
senderUsername,
|
|
74
|
+
} = params;
|
|
75
|
+
|
|
76
|
+
if (!senderExternalUserId) {
|
|
77
|
+
return { notified: false, reason: 'no_sender_id' };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Resolve guardian binding with fallback strategy:
|
|
81
|
+
// 1. Source-channel active binding
|
|
82
|
+
// 2. Any active binding for the assistant (deterministic order)
|
|
83
|
+
// 3. null (no guardian identity — notification pipeline uses trusted channels)
|
|
84
|
+
const sourceBinding = getGuardianBinding(canonicalAssistantId, sourceChannel);
|
|
85
|
+
let guardianExternalUserId: string | null = null;
|
|
86
|
+
let guardianBindingChannel: string | null = null;
|
|
87
|
+
|
|
88
|
+
if (sourceBinding) {
|
|
89
|
+
guardianExternalUserId = sourceBinding.guardianExternalUserId;
|
|
90
|
+
guardianBindingChannel = sourceBinding.channel;
|
|
91
|
+
} else {
|
|
92
|
+
const allBindings = listActiveBindingsByAssistant(canonicalAssistantId);
|
|
93
|
+
if (allBindings.length > 0) {
|
|
94
|
+
guardianExternalUserId = allBindings[0].guardianExternalUserId;
|
|
95
|
+
guardianBindingChannel = allBindings[0].channel;
|
|
96
|
+
log.debug(
|
|
97
|
+
{ sourceChannel, fallbackChannel: guardianBindingChannel, canonicalAssistantId },
|
|
98
|
+
'Using cross-channel guardian binding fallback for access request',
|
|
99
|
+
);
|
|
100
|
+
} else {
|
|
101
|
+
log.debug(
|
|
102
|
+
{ sourceChannel, canonicalAssistantId },
|
|
103
|
+
'No guardian binding for access request — proceeding without guardian identity',
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Deduplicate: skip creation if there is already a pending canonical request
|
|
109
|
+
// for the same requester on this channel. Still return notified: true with
|
|
110
|
+
// the existing request ID so callers know the guardian was already notified.
|
|
111
|
+
const existingCanonical = listCanonicalGuardianRequests({
|
|
112
|
+
status: 'pending',
|
|
113
|
+
requesterExternalUserId: senderExternalUserId,
|
|
114
|
+
sourceChannel,
|
|
115
|
+
kind: 'access_request',
|
|
116
|
+
});
|
|
117
|
+
if (existingCanonical.length > 0) {
|
|
118
|
+
log.debug(
|
|
119
|
+
{ sourceChannel, senderExternalUserId, existingId: existingCanonical[0].id },
|
|
120
|
+
'Skipping duplicate access request notification',
|
|
121
|
+
);
|
|
122
|
+
return { notified: true, created: false, requestId: existingCanonical[0].id };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const senderIdentifier = senderName || senderUsername || senderExternalUserId;
|
|
126
|
+
const requestId = `access-req-${canonicalAssistantId}-${sourceChannel}-${senderExternalUserId}-${Date.now()}`;
|
|
127
|
+
|
|
128
|
+
const canonicalRequest = createCanonicalGuardianRequest({
|
|
129
|
+
id: requestId,
|
|
130
|
+
kind: 'access_request',
|
|
131
|
+
sourceType: 'channel',
|
|
132
|
+
sourceChannel,
|
|
133
|
+
conversationId: `access-req-${sourceChannel}-${senderExternalUserId}`,
|
|
134
|
+
requesterExternalUserId: senderExternalUserId,
|
|
135
|
+
requesterChatId: externalChatId,
|
|
136
|
+
guardianExternalUserId: guardianExternalUserId ?? undefined,
|
|
137
|
+
toolName: 'ingress_access_request',
|
|
138
|
+
questionText: `${senderIdentifier} is requesting access to the assistant`,
|
|
139
|
+
expiresAt: new Date(Date.now() + GUARDIAN_APPROVAL_TTL_MS).toISOString(),
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
void emitNotificationSignal({
|
|
143
|
+
sourceEventName: 'ingress.access_request',
|
|
144
|
+
sourceChannel,
|
|
145
|
+
sourceSessionId: `access-req-${sourceChannel}-${senderExternalUserId}`,
|
|
146
|
+
assistantId: canonicalAssistantId,
|
|
147
|
+
attentionHints: {
|
|
148
|
+
requiresAction: true,
|
|
149
|
+
urgency: 'high',
|
|
150
|
+
isAsyncBackground: false,
|
|
151
|
+
visibleInSourceNow: false,
|
|
152
|
+
},
|
|
153
|
+
contextPayload: {
|
|
154
|
+
requestId,
|
|
155
|
+
requestCode: canonicalRequest.requestCode,
|
|
156
|
+
sourceChannel,
|
|
157
|
+
externalChatId,
|
|
158
|
+
senderExternalUserId,
|
|
159
|
+
senderName: senderName ?? null,
|
|
160
|
+
senderUsername: senderUsername ?? null,
|
|
161
|
+
senderIdentifier,
|
|
162
|
+
guardianBindingChannel,
|
|
163
|
+
},
|
|
164
|
+
dedupeKey: `access-request:${canonicalRequest.id}`,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
log.info(
|
|
168
|
+
{ sourceChannel, senderExternalUserId, senderIdentifier, guardianBindingChannel },
|
|
169
|
+
'Guardian notified of access request',
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
return { notified: true, created: true, requestId: canonicalRequest.id };
|
|
173
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified inbound actor trust resolver.
|
|
3
|
+
*
|
|
4
|
+
* Produces a single trust-resolved actor context from raw inbound identity
|
|
5
|
+
* fields. Normalizes sender identity via channel-agnostic canonicalization,
|
|
6
|
+
* then resolves trust classification by checking guardian bindings and
|
|
7
|
+
* ingress member records.
|
|
8
|
+
*
|
|
9
|
+
* Trust classifications:
|
|
10
|
+
* - `guardian`: sender matches the active guardian binding for this channel.
|
|
11
|
+
* - `trusted_contact`: sender is an active ingress member (not the guardian).
|
|
12
|
+
* - `unknown`: sender has no member record or no identity could be established.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { ChannelId } from '../channels/types.js';
|
|
16
|
+
import type { GuardianRuntimeContext } from '../daemon/session-runtime-assembly.js';
|
|
17
|
+
import type { IngressMember } from '../memory/ingress-member-store.js';
|
|
18
|
+
import { findMember } from '../memory/ingress-member-store.js';
|
|
19
|
+
import { canonicalizeInboundIdentity } from '../util/canonicalize-identity.js';
|
|
20
|
+
import { normalizeAssistantId } from '../util/platform.js';
|
|
21
|
+
import { getGuardianBinding } from './channel-guardian-service.js';
|
|
22
|
+
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Types
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
export type TrustClass = 'guardian' | 'trusted_contact' | 'unknown';
|
|
28
|
+
export type DenialReason = 'no_binding' | 'no_identity';
|
|
29
|
+
|
|
30
|
+
export interface ActorTrustContext {
|
|
31
|
+
/** Canonical (normalized) sender identity. Null when identity could not be established. */
|
|
32
|
+
canonicalSenderId: string | null;
|
|
33
|
+
/** Guardian binding match, if any, for this (assistantId, channel). */
|
|
34
|
+
guardianBindingMatch: {
|
|
35
|
+
guardianExternalUserId: string;
|
|
36
|
+
guardianDeliveryChatId: string | null;
|
|
37
|
+
} | null;
|
|
38
|
+
/** Ingress member record, if any, for this sender. */
|
|
39
|
+
memberRecord: IngressMember | null;
|
|
40
|
+
/** Trust classification. */
|
|
41
|
+
trustClass: TrustClass;
|
|
42
|
+
/** Assistant-facing metadata for downstream consumption. */
|
|
43
|
+
actorMetadata: {
|
|
44
|
+
identifier: string | undefined;
|
|
45
|
+
displayName: string | undefined;
|
|
46
|
+
senderDisplayName: string | undefined;
|
|
47
|
+
memberDisplayName: string | undefined;
|
|
48
|
+
username: string | undefined;
|
|
49
|
+
channel: ChannelId;
|
|
50
|
+
trustStatus: TrustClass;
|
|
51
|
+
};
|
|
52
|
+
/** Legacy denial reason for backward-compatible unverified_channel paths. */
|
|
53
|
+
denialReason?: DenialReason;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface ResolveActorTrustInput {
|
|
57
|
+
assistantId: string;
|
|
58
|
+
sourceChannel: ChannelId;
|
|
59
|
+
externalChatId: string;
|
|
60
|
+
senderExternalUserId?: string;
|
|
61
|
+
senderUsername?: string;
|
|
62
|
+
senderDisplayName?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// Resolver
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Resolve the inbound actor's trust context from raw identity fields.
|
|
71
|
+
*
|
|
72
|
+
* 1. Canonicalize the sender identity (E.164 for phone channels, trimmed ID otherwise).
|
|
73
|
+
* 2. Look up the guardian binding for (assistantId, channel).
|
|
74
|
+
* 3. Compare canonical sender identity to the guardian binding.
|
|
75
|
+
* 4. Look up the ingress member record using the canonical identity.
|
|
76
|
+
* 5. Classify: guardian > trusted_contact (active member) > unknown.
|
|
77
|
+
*/
|
|
78
|
+
export function resolveActorTrust(input: ResolveActorTrustInput): ActorTrustContext {
|
|
79
|
+
const assistantId = normalizeAssistantId(input.assistantId);
|
|
80
|
+
|
|
81
|
+
const rawUserId = typeof input.senderExternalUserId === 'string' && input.senderExternalUserId.trim().length > 0
|
|
82
|
+
? input.senderExternalUserId.trim()
|
|
83
|
+
: undefined;
|
|
84
|
+
|
|
85
|
+
const senderUsername = typeof input.senderUsername === 'string' && input.senderUsername.trim().length > 0
|
|
86
|
+
? input.senderUsername.trim()
|
|
87
|
+
: undefined;
|
|
88
|
+
|
|
89
|
+
const senderDisplayName = typeof input.senderDisplayName === 'string' && input.senderDisplayName.trim().length > 0
|
|
90
|
+
? input.senderDisplayName.trim()
|
|
91
|
+
: undefined;
|
|
92
|
+
|
|
93
|
+
// Canonical identity: normalize phone-like channels to E.164.
|
|
94
|
+
const canonicalSenderId = rawUserId
|
|
95
|
+
? canonicalizeInboundIdentity(input.sourceChannel, rawUserId)
|
|
96
|
+
: null;
|
|
97
|
+
|
|
98
|
+
const identifier = senderUsername ? `@${senderUsername}` : canonicalSenderId ?? undefined;
|
|
99
|
+
|
|
100
|
+
// No identity at all => unknown
|
|
101
|
+
if (!canonicalSenderId) {
|
|
102
|
+
return {
|
|
103
|
+
canonicalSenderId: null,
|
|
104
|
+
guardianBindingMatch: null,
|
|
105
|
+
memberRecord: null,
|
|
106
|
+
trustClass: 'unknown',
|
|
107
|
+
actorMetadata: {
|
|
108
|
+
identifier,
|
|
109
|
+
displayName: senderDisplayName,
|
|
110
|
+
senderDisplayName,
|
|
111
|
+
memberDisplayName: undefined,
|
|
112
|
+
username: senderUsername,
|
|
113
|
+
channel: input.sourceChannel,
|
|
114
|
+
trustStatus: 'unknown',
|
|
115
|
+
},
|
|
116
|
+
denialReason: 'no_identity',
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Guardian binding lookup
|
|
121
|
+
const binding = getGuardianBinding(assistantId, input.sourceChannel);
|
|
122
|
+
const guardianBindingMatch = binding
|
|
123
|
+
? { guardianExternalUserId: binding.guardianExternalUserId, guardianDeliveryChatId: binding.guardianDeliveryChatId }
|
|
124
|
+
: null;
|
|
125
|
+
|
|
126
|
+
// Check if sender IS the guardian. Compare canonical sender against the
|
|
127
|
+
// binding's guardian identity (also canonicalize for phone channels to
|
|
128
|
+
// handle formatting variance in the stored binding).
|
|
129
|
+
let isGuardian = false;
|
|
130
|
+
if (binding) {
|
|
131
|
+
const canonicalGuardianId = canonicalizeInboundIdentity(input.sourceChannel, binding.guardianExternalUserId);
|
|
132
|
+
isGuardian = canonicalGuardianId === canonicalSenderId;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Ingress member lookup using canonical identity.
|
|
136
|
+
const memberRecord = findMember({
|
|
137
|
+
assistantId,
|
|
138
|
+
sourceChannel: input.sourceChannel,
|
|
139
|
+
externalUserId: canonicalSenderId,
|
|
140
|
+
externalChatId: input.externalChatId,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// In group chats, findMember may match on externalChatId and return a
|
|
144
|
+
// record for a different user. Only use member metadata when the record's
|
|
145
|
+
// externalUserId matches the current sender to avoid misidentification.
|
|
146
|
+
// Canonicalize the stored member ID to handle formatting variance (e.g.
|
|
147
|
+
// phone numbers stored without E.164 normalization).
|
|
148
|
+
const memberMatchesSender = memberRecord?.externalUserId
|
|
149
|
+
? canonicalizeInboundIdentity(input.sourceChannel, memberRecord.externalUserId) === canonicalSenderId
|
|
150
|
+
: false;
|
|
151
|
+
|
|
152
|
+
const memberUsername = memberMatchesSender && typeof memberRecord?.username === 'string' && memberRecord.username.trim().length > 0
|
|
153
|
+
? memberRecord.username.trim()
|
|
154
|
+
: undefined;
|
|
155
|
+
const memberDisplayName = memberMatchesSender && typeof memberRecord?.displayName === 'string' && memberRecord.displayName.trim().length > 0
|
|
156
|
+
? memberRecord.displayName.trim()
|
|
157
|
+
: undefined;
|
|
158
|
+
// Prefer member profile metadata over transient sender metadata so guardian-
|
|
159
|
+
// curated contact details are canonical for assistant-facing identity —
|
|
160
|
+
// but only when the member record actually belongs to the current sender.
|
|
161
|
+
const resolvedUsername = memberUsername ?? senderUsername;
|
|
162
|
+
const resolvedDisplayName = memberDisplayName ?? senderDisplayName;
|
|
163
|
+
const resolvedIdentifier = resolvedUsername ? `@${resolvedUsername}` : canonicalSenderId ?? undefined;
|
|
164
|
+
|
|
165
|
+
// Trust classification
|
|
166
|
+
let trustClass: TrustClass;
|
|
167
|
+
if (isGuardian) {
|
|
168
|
+
trustClass = 'guardian';
|
|
169
|
+
} else if (memberMatchesSender && memberRecord && memberRecord.status === 'active') {
|
|
170
|
+
trustClass = 'trusted_contact';
|
|
171
|
+
} else {
|
|
172
|
+
trustClass = 'unknown';
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Denial reason for legacy compatibility
|
|
176
|
+
let denialReason: DenialReason | undefined;
|
|
177
|
+
if (!isGuardian && !binding) {
|
|
178
|
+
denialReason = 'no_binding';
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
canonicalSenderId,
|
|
183
|
+
guardianBindingMatch,
|
|
184
|
+
memberRecord,
|
|
185
|
+
trustClass,
|
|
186
|
+
actorMetadata: {
|
|
187
|
+
identifier: resolvedIdentifier,
|
|
188
|
+
displayName: resolvedDisplayName,
|
|
189
|
+
senderDisplayName,
|
|
190
|
+
memberDisplayName,
|
|
191
|
+
username: resolvedUsername,
|
|
192
|
+
channel: input.sourceChannel,
|
|
193
|
+
trustStatus: trustClass,
|
|
194
|
+
},
|
|
195
|
+
denialReason,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Convert an ActorTrustContext into the runtime trust context shape used by
|
|
201
|
+
* sessions/tooling.
|
|
202
|
+
*/
|
|
203
|
+
export function toGuardianRuntimeContextFromTrust(
|
|
204
|
+
ctx: ActorTrustContext,
|
|
205
|
+
externalChatId: string,
|
|
206
|
+
): GuardianRuntimeContext {
|
|
207
|
+
return {
|
|
208
|
+
sourceChannel: ctx.actorMetadata.channel,
|
|
209
|
+
trustClass: ctx.trustClass,
|
|
210
|
+
guardianChatId: ctx.guardianBindingMatch?.guardianDeliveryChatId ??
|
|
211
|
+
(ctx.trustClass === 'guardian' ? externalChatId : undefined),
|
|
212
|
+
guardianExternalUserId: ctx.guardianBindingMatch?.guardianExternalUserId,
|
|
213
|
+
requesterIdentifier: ctx.actorMetadata.identifier,
|
|
214
|
+
requesterDisplayName: ctx.actorMetadata.displayName,
|
|
215
|
+
requesterSenderDisplayName: ctx.actorMetadata.senderDisplayName,
|
|
216
|
+
requesterMemberDisplayName: ctx.actorMetadata.memberDisplayName,
|
|
217
|
+
requesterExternalUserId: ctx.canonicalSenderId ?? undefined,
|
|
218
|
+
requesterChatId: externalChatId,
|
|
219
|
+
denialReason: ctx.denialReason,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
@@ -231,11 +231,19 @@ export function validateAndConsumeChallenge(
|
|
|
231
231
|
}
|
|
232
232
|
}
|
|
233
233
|
|
|
234
|
-
// For Telegram
|
|
235
|
-
//
|
|
234
|
+
// For chat-based channels (Telegram, Slack, etc.): when both
|
|
235
|
+
// expectedExternalUserId and expectedChatId are set, require the
|
|
236
|
+
// externalUserId match — chatId alone is insufficient because chat IDs
|
|
237
|
+
// can be shared (e.g. Slack channel IDs, Telegram group chat IDs) and
|
|
238
|
+
// would let any participant in the same chat satisfy identity binding.
|
|
239
|
+
// Fall back to chatId-only match only when expectedExternalUserId is
|
|
240
|
+
// not available (legacy sessions or channels without user-level identity).
|
|
236
241
|
if (challenge.expectedChatId != null) {
|
|
237
|
-
if (
|
|
238
|
-
|
|
242
|
+
if (challenge.expectedExternalUserId != null) {
|
|
243
|
+
if (actorExternalUserId === challenge.expectedExternalUserId) {
|
|
244
|
+
identityMatch = true;
|
|
245
|
+
}
|
|
246
|
+
} else if (actorChatId === challenge.expectedChatId) {
|
|
239
247
|
identityMatch = true;
|
|
240
248
|
}
|
|
241
249
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Voice channel invite transport adapter.
|
|
3
|
+
*
|
|
4
|
+
* Voice invites are identity-bound: the invitee must call from a specific
|
|
5
|
+
* phone number and enter a numeric code. Unlike Telegram invites, there is
|
|
6
|
+
* no shareable deep link — the guardian relays the code and calling
|
|
7
|
+
* instructions verbally or via another channel.
|
|
8
|
+
*
|
|
9
|
+
* The transport builds human-readable instruction text and provides a
|
|
10
|
+
* no-op token extractor since voice invite redemption uses the dedicated
|
|
11
|
+
* voice-code path rather than generic token extraction.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { ChannelId } from '../../channels/types.js';
|
|
15
|
+
import {
|
|
16
|
+
type ChannelInviteTransport,
|
|
17
|
+
type InviteSharePayload,
|
|
18
|
+
registerTransport,
|
|
19
|
+
} from '../channel-invite-transport.js';
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Transport implementation
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
export const voiceInviteTransport: ChannelInviteTransport = {
|
|
26
|
+
channel: 'voice' as ChannelId,
|
|
27
|
+
|
|
28
|
+
buildShareableInvite(_params: {
|
|
29
|
+
rawToken: string;
|
|
30
|
+
sourceChannel: ChannelId;
|
|
31
|
+
}): InviteSharePayload {
|
|
32
|
+
// Voice invites do not produce a clickable URL. The "url" field contains
|
|
33
|
+
// a placeholder — callers should use displayText for presentation.
|
|
34
|
+
return {
|
|
35
|
+
url: '',
|
|
36
|
+
displayText: [
|
|
37
|
+
'Voice invite created.',
|
|
38
|
+
'The invitee must call the assistant\'s phone number from the authorized number and enter their invite code when prompted.',
|
|
39
|
+
].join(' '),
|
|
40
|
+
};
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
extractInboundToken(_params: {
|
|
44
|
+
commandIntent?: Record<string, unknown>;
|
|
45
|
+
content: string;
|
|
46
|
+
sourceMetadata?: Record<string, unknown>;
|
|
47
|
+
}): string | undefined {
|
|
48
|
+
// Voice invite redemption bypasses generic token extraction — it uses
|
|
49
|
+
// the identity-bound voice-code flow in invite-redemption-service.ts.
|
|
50
|
+
return undefined;
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// Auto-register on import
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
registerTransport(voiceInviteTransport);
|