@vellumai/assistant 0.10.3 → 0.10.4-staging.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/openapi.yaml +73 -56
- package/package.json +1 -1
- package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +83 -31
- package/src/__tests__/assistant-stream-state.test.ts +3 -76
- package/src/__tests__/background-workers-disk-pressure.test.ts +4 -2
- package/src/__tests__/channel-approval-routes.test.ts +21 -26
- package/src/__tests__/channel-delivery-store.test.ts +28 -0
- package/src/__tests__/channel-guardian.test.ts +82 -32
- package/src/__tests__/channel-inbound-disk-pressure.test.ts +11 -19
- package/src/__tests__/channel-reply-delivery.test.ts +6 -2
- package/src/__tests__/compaction-ledger-store.test.ts +128 -0
- package/src/__tests__/config-loader-backfill.test.ts +148 -0
- package/src/__tests__/consult-deadline.test.ts +60 -0
- package/src/__tests__/contact-store-interaction-info.test.ts +156 -0
- package/src/__tests__/contact-store-user-file.test.ts +7 -10
- package/src/__tests__/contacts-relay-reads.test.ts +6 -9
- package/src/__tests__/contacts-write.test.ts +0 -2
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +4 -2
- package/src/__tests__/conversation-agent-loop.test.ts +98 -7
- package/src/__tests__/conversation-attention-telegram.test.ts +9 -11
- package/src/__tests__/conversation-error.test.ts +18 -0
- package/src/__tests__/conversation-fork-crud.test.ts +354 -24
- package/src/__tests__/conversation-title-service.test.ts +222 -201
- package/src/__tests__/db-compaction-events-migration.test.ts +129 -0
- package/src/__tests__/delete-propagation.test.ts +5 -3
- package/src/__tests__/dm-backfill.test.ts +6 -4
- package/src/__tests__/emit-signal-routing-intent.test.ts +2 -6
- package/src/__tests__/guardian-binding-drift-heal.test.ts +43 -23
- package/src/__tests__/guardian-dispatch.test.ts +50 -5
- package/src/__tests__/guardian-routing-state.test.ts +6 -10
- package/src/__tests__/helpers/channel-test-adapter.ts +45 -12
- package/src/__tests__/helpers/create-guardian-binding.ts +15 -23
- package/src/__tests__/helpers/mock-logger.ts +1 -0
- package/src/__tests__/helpers/seed-contact-channel.ts +96 -0
- package/src/__tests__/inbound-invite-redemption.test.ts +87 -10
- package/src/__tests__/invite-redemption-service.test.ts +273 -53
- package/src/__tests__/invite-routes-http.test.ts +34 -0
- package/src/__tests__/invite-service-ipc.test.ts +65 -2
- package/src/__tests__/list-messages-page-latest.test.ts +173 -4
- package/src/__tests__/mcp-config-secret-boundary.test.ts +3 -0
- package/src/__tests__/non-member-access-request.test.ts +15 -13
- package/src/__tests__/onboarding-persona-write.test.ts +52 -22
- package/src/__tests__/persist-onboarding-artifacts.test.ts +1 -0
- package/src/__tests__/persona-resolver.test.ts +75 -45
- package/src/__tests__/plugin-bootstrap.test.ts +13 -5
- package/src/__tests__/plugin-disabled-state.test.ts +190 -0
- package/src/__tests__/provider-usage-tracking.test.ts +1 -1
- package/src/__tests__/reaction-intercept-cold-cache-warm.test.ts +135 -0
- package/src/__tests__/reaction-intercept-member-verdict-warm.test.ts +158 -0
- package/src/__tests__/reaction-persistence.test.ts +51 -4
- package/src/__tests__/relay-server.test.ts +88 -31
- package/src/__tests__/runtime-attachment-metadata.test.ts +9 -11
- package/src/__tests__/settings-routes.test.ts +32 -0
- package/src/__tests__/slack-block-formatting.test.ts +1 -38
- package/src/__tests__/sse-actor-principal-guardian-source.test.ts +13 -36
- package/src/__tests__/stt-hints.test.ts +6 -3
- package/src/__tests__/subagent-fork-prompt-role.test.ts +195 -0
- package/src/__tests__/subagent-fork-spawn.test.ts +6 -7
- package/src/__tests__/subagent-role-registry.test.ts +17 -4
- package/src/__tests__/subagent-spawn-and-await.test.ts +546 -0
- package/src/__tests__/subagent-tools.test.ts +398 -3
- package/src/__tests__/thread-backfill.test.ts +3 -3
- package/src/__tests__/tool-preview-lifecycle.test.ts +26 -10
- package/src/__tests__/tool-start-timestamp.test.ts +4 -3
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +37 -51
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +2 -2
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +9 -7
- package/src/__tests__/trusted-contact-multichannel.test.ts +16 -7
- package/src/__tests__/trusted-contact-verification.test.ts +79 -54
- package/src/__tests__/voice-guardian-cold-cache-warm.test.ts +137 -0
- package/src/__tests__/voice-invite-redemption.test.ts +183 -20
- package/src/__tests__/workspace-migration-102-preserve-heartbeat-enabled-for-existing-workspaces.test.ts +3 -3
- package/src/__tests__/workspace-migration-111-prune-seeded-callsite-defaults.test.ts +2 -2
- package/src/__tests__/workspace-migration-112-remove-advisor-callsite-override.test.ts +170 -0
- package/src/__tests__/workspace-migration-drop-user-md.test.ts +196 -238
- package/src/a2a/__tests__/e2e-a2a-channel.test.ts +35 -47
- package/src/agent/loop-exclusive-tool.test.ts +19 -15
- package/src/agent/loop-native-web-search.test.ts +200 -0
- package/src/agent/loop.ts +108 -1
- package/src/api/responses/conversation-message.ts +9 -0
- package/src/approvals/guardian-request-resolvers.ts +16 -4
- package/src/calls/__tests__/relay-setup-router.test.ts +10 -18
- package/src/calls/guardian-dispatch.ts +14 -11
- package/src/calls/inbound-trust-reader.ts +7 -1
- package/src/calls/relay-access-wait.ts +6 -6
- package/src/calls/relay-server.ts +22 -2
- package/src/calls/relay-setup-router.ts +10 -10
- package/src/cli/commands/__tests__/conversations-slack.test.ts +1 -0
- package/src/cli/commands/contacts.ts +10 -7
- package/src/cli/commands/memory/__tests__/worker.test.ts +147 -17
- package/src/cli/commands/memory/worker.ts +97 -30
- package/src/cli/commands/plugins.ts +3 -146
- package/src/cli/lib/__tests__/list-installed-plugins.test.ts +17 -17
- package/src/cli/lib/__tests__/publish-plugin.test.ts +98 -0
- package/src/cli/lib/publish-plugin.ts +231 -1
- package/src/config/__tests__/sync-gated-profiles.test.ts +5 -7
- package/src/config/bundled-skills/subagent/SKILL.md +16 -1
- package/src/config/bundled-skills/subagent/TOOLS.json +5 -4
- package/src/config/call-site-defaults.ts +0 -6
- package/src/config/llm-resolver.ts +0 -3
- package/src/config/schemas/call-site-catalog.ts +0 -7
- package/src/config/schemas/heartbeat.ts +2 -5
- package/src/config/schemas/llm.ts +3 -12
- package/src/config/schemas/memory-lifecycle.ts +1 -1
- package/src/config/seed-inference-profiles.ts +76 -35
- package/src/config/sync-gated-profiles.ts +0 -3
- package/src/contacts/__tests__/contacts-write-revoke-relay.test.ts +7 -8
- package/src/contacts/__tests__/member-write-relay.test.ts +35 -11
- package/src/contacts/contact-store.ts +27 -237
- package/src/contacts/contacts-write.ts +18 -58
- package/src/contacts/gateway-channel-read.ts +51 -0
- package/src/contacts/member-write-relay.ts +25 -31
- package/src/contacts/types.ts +3 -15
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +0 -44
- package/src/daemon/conversation-agent-loop-handlers.ts +29 -10
- package/src/daemon/conversation-agent-loop.ts +68 -61
- package/src/daemon/conversation-error.ts +7 -10
- package/src/daemon/conversation-tool-setup.ts +0 -10
- package/src/daemon/conversation.ts +10 -0
- package/src/daemon/external-plugins-bootstrap.ts +8 -2
- package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +0 -1
- package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +0 -2
- package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +0 -2
- package/src/daemon/handlers/__tests__/config-channels.test.ts +9 -14
- package/src/daemon/handlers/config-channels.ts +14 -29
- package/src/daemon/lifecycle.ts +16 -4
- package/src/daemon/message-types/surfaces.ts +2 -0
- package/src/heartbeat/heartbeat-service.ts +5 -0
- package/src/home/relationship-state-writer.ts +5 -0
- package/src/memory/__tests__/embedding-cache.test.ts +136 -0
- package/src/memory/compaction-ledger-store.ts +107 -0
- package/src/memory/conversation-crud.ts +136 -61
- package/src/memory/conversation-title-service.ts +173 -24
- package/src/memory/embedding-backend.ts +8 -1
- package/src/memory/embedding-cache.ts +139 -0
- package/src/memory/jobs-worker.ts +75 -29
- package/src/memory/memory-retrospective-job.ts +5 -0
- package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +27 -5
- package/src/memory/migrations/302-create-compaction-events.ts +107 -0
- package/src/memory/migrations/303-add-conversation-creation-seq.ts +33 -0
- package/src/memory/migrations/__tests__/209-strip-thinking-from-consolidated.test.ts +79 -6
- package/src/memory/schema/contacts.ts +6 -2
- package/src/memory/schema/conversations.ts +39 -0
- package/src/memory/steps.ts +1090 -367
- package/src/memory/worker-control.ts +104 -18
- package/src/memory/worker-process.ts +17 -0
- package/src/messaging/channel-binding-metadata.ts +31 -0
- package/src/messaging/channel-binding-schema.ts +51 -0
- package/src/messaging/providers/__tests__/callback-routing.test.ts +45 -0
- package/src/messaging/providers/__tests__/transport-dispatch.test.ts +195 -0
- package/src/messaging/providers/a2a/__tests__/deliver.test.ts +11 -0
- package/src/messaging/providers/a2a/deliver.ts +5 -1
- package/src/messaging/providers/a2a/transport.ts +10 -0
- package/src/messaging/providers/callback-routing.ts +48 -0
- package/src/messaging/providers/channel-transport.ts +55 -0
- package/src/messaging/providers/index.ts +65 -241
- package/src/messaging/providers/slack/binding-metadata.ts +62 -0
- package/src/messaging/providers/slack/transport.ts +92 -0
- package/src/messaging/providers/telegram-bot/transport.ts +51 -0
- package/src/messaging/providers/whatsapp/transport.ts +38 -0
- package/src/notifications/__tests__/broadcaster.test.ts +0 -8
- package/src/notifications/__tests__/connected-channels.test.ts +8 -36
- package/src/notifications/__tests__/destination-resolver.test.ts +12 -117
- package/src/notifications/destination-resolver.ts +7 -23
- package/src/notifications/emit-signal.ts +5 -11
- package/src/plugins/defaults/index.ts +0 -35
- package/src/plugins/defaults/memory-v3-shadow/__tests__/dense.test.ts +11 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/section-dense-store.test.ts +243 -2
- package/src/plugins/defaults/memory-v3-shadow/section-dense-store.ts +167 -14
- package/src/plugins/disabled-state.ts +31 -0
- package/src/plugins/registry.ts +55 -12
- package/src/prompts/persona-resolver.ts +43 -11
- package/src/providers/call-site-routing.ts +41 -0
- package/src/providers/provider-send-message.ts +6 -0
- package/src/providers/ratelimit.ts +6 -0
- package/src/providers/registry.ts +1 -1
- package/src/providers/retry.ts +6 -0
- package/src/providers/types.ts +13 -0
- package/src/providers/usage-tracking.ts +6 -0
- package/src/runtime/__tests__/guardian-vellum-migration.test.ts +30 -27
- package/src/runtime/__tests__/local-principal-trust.test.ts +16 -18
- package/src/runtime/__tests__/member-verdict-cache.test.ts +119 -0
- package/src/runtime/__tests__/trust-verdict-consumer.test.ts +115 -168
- package/src/runtime/access-request-helper.ts +1 -2
- package/src/runtime/actor-trust-resolver.ts +44 -17
- package/src/runtime/anchored-guardian.test.ts +7 -54
- package/src/runtime/anchored-guardian.ts +4 -53
- package/src/runtime/assistant-stream-state.ts +12 -74
- package/src/runtime/channel-reply-delivery.ts +3 -8
- package/src/runtime/guardian-vellum-migration.ts +18 -16
- package/src/runtime/invite-redemption-service.ts +25 -10
- package/src/runtime/local-actor-identity.test.ts +108 -0
- package/src/runtime/local-actor-identity.ts +27 -20
- package/src/runtime/member-verdict-cache.ts +0 -0
- package/src/runtime/routes/__tests__/contact-routes.test.ts +100 -7
- package/src/runtime/routes/__tests__/global-search-routes.test.ts +1 -2
- package/src/runtime/routes/__tests__/surface-action-routes.test.ts +2 -1
- package/src/runtime/routes/contact-routes.ts +40 -25
- package/src/runtime/routes/conversation-list-routes.ts +1 -29
- package/src/runtime/routes/conversation-routes.ts +27 -7
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +0 -10
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +4 -8
- package/src/runtime/routes/inbound-stages/reaction-intercept.ts +19 -0
- package/src/runtime/routes/settings-routes.ts +8 -3
- package/src/runtime/services/conversation-serializer.ts +6 -49
- package/src/runtime/slack-block-formatting.ts +0 -15
- package/src/runtime/trust-verdict-consumer.ts +36 -41
- package/src/subagent/__tests__/consult-prompt.test.ts +35 -0
- package/src/{plugins/defaults/advisor/__tests__/transcript.test.ts → subagent/__tests__/consult-transcript.test.ts} +47 -10
- package/src/{plugins/defaults/advisor/steering.ts → subagent/consult-prompt.ts} +17 -39
- package/src/{plugins/defaults/advisor/transcript.ts → subagent/consult-transcript.ts} +18 -8
- package/src/subagent/index.ts +1 -1
- package/src/subagent/manager.ts +245 -33
- package/src/subagent/types.ts +8 -1
- package/src/tools/registry.ts +10 -3
- package/src/tools/subagent/consult-deadline.ts +49 -0
- package/src/tools/subagent/spawn.ts +234 -5
- package/src/util/logger.ts +9 -0
- package/src/util/platform.ts +14 -0
- package/src/workspace/migrations/031-drop-user-md.ts +232 -148
- package/src/workspace/migrations/112-remove-advisor-callsite-override.ts +64 -0
- package/src/workspace/migrations/registry.ts +2 -0
- package/src/plugins/defaults/advisor/__tests__/advisor-gate.test.ts +0 -56
- package/src/plugins/defaults/advisor/__tests__/advisor-state-store.test.ts +0 -43
- package/src/plugins/defaults/advisor/__tests__/agent-loop-integration.test.ts +0 -137
- package/src/plugins/defaults/advisor/__tests__/consult.test.ts +0 -314
- package/src/plugins/defaults/advisor/__tests__/context-pack-gating.test.ts +0 -106
- package/src/plugins/defaults/advisor/__tests__/context-pack.test.ts +0 -60
- package/src/plugins/defaults/advisor/__tests__/hooks.test.ts +0 -138
- package/src/plugins/defaults/advisor/advisor-gate.ts +0 -29
- package/src/plugins/defaults/advisor/advisor-state-store.ts +0 -94
- package/src/plugins/defaults/advisor/config.ts +0 -21
- package/src/plugins/defaults/advisor/consult.ts +0 -197
- package/src/plugins/defaults/advisor/context-pack.ts +0 -288
- package/src/plugins/defaults/advisor/hooks/post-model-call.ts +0 -34
- package/src/plugins/defaults/advisor/hooks/pre-model-call.ts +0 -30
- package/src/plugins/defaults/advisor/hooks/user-prompt-submit.ts +0 -19
- package/src/plugins/defaults/advisor/package.json +0 -14
- package/src/plugins/defaults/advisor/tools/advisor.ts +0 -92
|
@@ -4,6 +4,8 @@ import type { TrustVerdict } from "@vellumai/gateway-client";
|
|
|
4
4
|
|
|
5
5
|
import { channelStatusToMemberStatus } from "../../contacts/member-status.js";
|
|
6
6
|
import type {
|
|
7
|
+
ChannelPolicy,
|
|
8
|
+
ChannelStatus,
|
|
7
9
|
ContactChannel,
|
|
8
10
|
ContactWithChannels,
|
|
9
11
|
} from "../../contacts/types.js";
|
|
@@ -11,7 +13,6 @@ import type { ActorTrustContext } from "../actor-trust-resolver.js";
|
|
|
11
13
|
import { toTrustContext } from "../actor-trust-resolver.js";
|
|
12
14
|
import {
|
|
13
15
|
actorTrustContextFromVerdict,
|
|
14
|
-
resolvedMemberFromVerdict,
|
|
15
16
|
trustContextFromVerdict,
|
|
16
17
|
verdictHasMemberIdentity,
|
|
17
18
|
verdictMemberFromVerdict,
|
|
@@ -288,8 +289,77 @@ describe("actorTrustContextFromVerdict", () => {
|
|
|
288
289
|
expect(ctx.memberRecord).not.toBeNull();
|
|
289
290
|
expect(ctx.memberRecord!.contact.id).toBe("contact-1");
|
|
290
291
|
expect(ctx.memberRecord!.channel.id).toBe("channel-1");
|
|
291
|
-
expect(ctx.memberRecord!.
|
|
292
|
-
expect(ctx.memberRecord!.
|
|
292
|
+
expect(ctx.memberRecord!.status).toBe("blocked");
|
|
293
|
+
expect(ctx.memberRecord!.policy).toBe("deny");
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
test("memberRecord surfaces channel ACL/identity, info fields null", () => {
|
|
297
|
+
const verdict = {
|
|
298
|
+
trustClass: "trusted_contact",
|
|
299
|
+
canonicalSenderId: "u-1",
|
|
300
|
+
contactId: "contact-1",
|
|
301
|
+
channelId: "channel-1",
|
|
302
|
+
type: "slack",
|
|
303
|
+
address: "u-1",
|
|
304
|
+
status: "active",
|
|
305
|
+
policy: "allow",
|
|
306
|
+
externalChatId: "chat-1",
|
|
307
|
+
verifiedAt: 1700000000,
|
|
308
|
+
verifiedVia: "code",
|
|
309
|
+
memberDisplayName: "Dora",
|
|
310
|
+
} satisfies TrustVerdict;
|
|
311
|
+
|
|
312
|
+
const { memberRecord } = actorTrustContextFromVerdict(verdict, {
|
|
313
|
+
sourceChannel: "slack",
|
|
314
|
+
conversationExternalId: CONV,
|
|
315
|
+
});
|
|
316
|
+
expect(memberRecord!.status).toBe("active");
|
|
317
|
+
expect(memberRecord!.policy).toBe("allow");
|
|
318
|
+
expect(memberRecord!.channel.externalChatId).toBe("chat-1");
|
|
319
|
+
|
|
320
|
+
expect(memberRecord!.contact.displayName).toBe("Dora");
|
|
321
|
+
expect(memberRecord!.role).toBe("contact");
|
|
322
|
+
// INFO fields must be null/default placeholders.
|
|
323
|
+
expect(memberRecord!.contact.notes).toBeNull();
|
|
324
|
+
expect(memberRecord!.contact.userFile).toBeNull();
|
|
325
|
+
expect(memberRecord!.contact.interactionCount).toBe(0);
|
|
326
|
+
expect(memberRecord!.contact.lastInteraction).toBeNull();
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
test("guardian member verdict maps role guardian + principalId", () => {
|
|
330
|
+
const verdict = {
|
|
331
|
+
trustClass: "guardian",
|
|
332
|
+
canonicalSenderId: "u-g",
|
|
333
|
+
contactId: "contact-g",
|
|
334
|
+
channelId: "channel-g",
|
|
335
|
+
guardianPrincipalId: "vellum-principal-g",
|
|
336
|
+
status: "active",
|
|
337
|
+
policy: "allow",
|
|
338
|
+
} satisfies TrustVerdict;
|
|
339
|
+
|
|
340
|
+
const ctx = actorTrustContextFromVerdict(verdict, {
|
|
341
|
+
sourceChannel: "slack",
|
|
342
|
+
conversationExternalId: CONV,
|
|
343
|
+
});
|
|
344
|
+
expect(ctx.memberRecord!.role).toBe("guardian");
|
|
345
|
+
expect(ctx.guardianPrincipalId).toBe("vellum-principal-g");
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
test("memberRecord null when status missing (fail-closed)", () => {
|
|
349
|
+
const verdict = {
|
|
350
|
+
trustClass: "trusted_contact",
|
|
351
|
+
canonicalSenderId: "u-4",
|
|
352
|
+
contactId: "contact-4",
|
|
353
|
+
channelId: "channel-4",
|
|
354
|
+
policy: "allow",
|
|
355
|
+
} satisfies TrustVerdict;
|
|
356
|
+
|
|
357
|
+
expect(
|
|
358
|
+
actorTrustContextFromVerdict(verdict, {
|
|
359
|
+
sourceChannel: "slack",
|
|
360
|
+
conversationExternalId: CONV,
|
|
361
|
+
}).memberRecord,
|
|
362
|
+
).toBeNull();
|
|
293
363
|
});
|
|
294
364
|
|
|
295
365
|
test("stranger verdict (no contactId/channelId) leaves memberRecord null", () => {
|
|
@@ -347,20 +417,18 @@ describe("actorTrustContextFromVerdict", () => {
|
|
|
347
417
|
actorTrustContextFromVerdict(verdict, input),
|
|
348
418
|
input.conversationExternalId,
|
|
349
419
|
);
|
|
350
|
-
const member =
|
|
420
|
+
const member = verdictMemberFromVerdict(verdict);
|
|
351
421
|
expect(member).not.toBeNull();
|
|
352
|
-
expected.requesterContactId = member!.
|
|
353
|
-
expected.memberStatus = channelStatusToMemberStatus(member!.
|
|
354
|
-
expected.memberPolicy = member!.
|
|
422
|
+
expected.requesterContactId = member!.contactId;
|
|
423
|
+
expected.memberStatus = channelStatusToMemberStatus(member!.status);
|
|
424
|
+
expected.memberPolicy = member!.policy;
|
|
355
425
|
|
|
356
426
|
expect(trustContextFromVerdict(verdict, input)).toEqual(expected);
|
|
357
427
|
});
|
|
358
428
|
});
|
|
359
429
|
|
|
360
430
|
describe("toTrustContext member grounding", () => {
|
|
361
|
-
function memberChannel(
|
|
362
|
-
overrides: Partial<ContactChannel> = {},
|
|
363
|
-
): ContactChannel {
|
|
431
|
+
function memberChannel(): ContactChannel {
|
|
364
432
|
return {
|
|
365
433
|
id: "channel-1",
|
|
366
434
|
contactId: "contact-1",
|
|
@@ -368,19 +436,12 @@ describe("toTrustContext member grounding", () => {
|
|
|
368
436
|
address: "+15550100",
|
|
369
437
|
isPrimary: true,
|
|
370
438
|
externalChatId: null,
|
|
371
|
-
status: "unverified",
|
|
372
|
-
policy: "escalate",
|
|
373
|
-
verifiedAt: null,
|
|
374
|
-
verifiedVia: null,
|
|
375
439
|
inviteId: null,
|
|
376
|
-
revokedReason: null,
|
|
377
|
-
blockedReason: null,
|
|
378
440
|
lastSeenAt: null,
|
|
379
441
|
interactionCount: 0,
|
|
380
442
|
lastInteraction: null,
|
|
381
443
|
updatedAt: null,
|
|
382
444
|
createdAt: 0,
|
|
383
|
-
...overrides,
|
|
384
445
|
};
|
|
385
446
|
}
|
|
386
447
|
|
|
@@ -389,24 +450,34 @@ describe("toTrustContext member grounding", () => {
|
|
|
389
450
|
id: "contact-1",
|
|
390
451
|
displayName: "Frank",
|
|
391
452
|
notes: null,
|
|
453
|
+
role: "contact",
|
|
392
454
|
lastInteraction: null,
|
|
393
455
|
interactionCount: 0,
|
|
394
456
|
createdAt: 0,
|
|
395
457
|
updatedAt: 0,
|
|
396
|
-
role: "contact",
|
|
397
458
|
contactType: "human",
|
|
398
|
-
principalId: null,
|
|
399
459
|
userFile: null,
|
|
400
460
|
channels: [memberChannel()],
|
|
401
461
|
};
|
|
402
462
|
}
|
|
403
463
|
|
|
404
|
-
function ctxWithMember(
|
|
464
|
+
function ctxWithMember(
|
|
465
|
+
acl: { status: ChannelStatus; policy: ChannelPolicy } = {
|
|
466
|
+
status: "unverified",
|
|
467
|
+
policy: "escalate",
|
|
468
|
+
},
|
|
469
|
+
): ActorTrustContext {
|
|
405
470
|
return {
|
|
406
471
|
canonicalSenderId: "+15550100",
|
|
407
472
|
guardianBindingMatch: null,
|
|
408
473
|
guardianPrincipalId: undefined,
|
|
409
|
-
memberRecord: {
|
|
474
|
+
memberRecord: {
|
|
475
|
+
contact: memberContact(),
|
|
476
|
+
channel: memberChannel(),
|
|
477
|
+
status: acl.status,
|
|
478
|
+
policy: acl.policy,
|
|
479
|
+
role: "contact",
|
|
480
|
+
},
|
|
410
481
|
trustClass: "trusted_contact",
|
|
411
482
|
actorMetadata: {
|
|
412
483
|
identifier: "+15550100",
|
|
@@ -421,7 +492,7 @@ describe("toTrustContext member grounding", () => {
|
|
|
421
492
|
}
|
|
422
493
|
|
|
423
494
|
test("populates member fields from memberRecord (voice path)", () => {
|
|
424
|
-
const context = toTrustContext(ctxWithMember(
|
|
495
|
+
const context = toTrustContext(ctxWithMember(), CONV);
|
|
425
496
|
expect(context.requesterContactId).toBe("contact-1");
|
|
426
497
|
// "unverified" maps to the API-facing "pending" member status.
|
|
427
498
|
expect(context.memberStatus).toBe("pending");
|
|
@@ -430,7 +501,7 @@ describe("toTrustContext member grounding", () => {
|
|
|
430
501
|
|
|
431
502
|
test("passes through active status + allow policy", () => {
|
|
432
503
|
const context = toTrustContext(
|
|
433
|
-
ctxWithMember(
|
|
504
|
+
ctxWithMember({ status: "active", policy: "allow" }),
|
|
434
505
|
CONV,
|
|
435
506
|
);
|
|
436
507
|
expect(context.memberStatus).toBe("active");
|
|
@@ -463,151 +534,6 @@ describe("toTrustContext member grounding", () => {
|
|
|
463
534
|
});
|
|
464
535
|
});
|
|
465
536
|
|
|
466
|
-
describe("resolvedMemberFromVerdict", () => {
|
|
467
|
-
test("member verdict surfaces channel ACL/identity, info fields null", () => {
|
|
468
|
-
const verdict = {
|
|
469
|
-
trustClass: "trusted_contact",
|
|
470
|
-
canonicalSenderId: "u-1",
|
|
471
|
-
contactId: "contact-1",
|
|
472
|
-
channelId: "channel-1",
|
|
473
|
-
type: "slack",
|
|
474
|
-
address: "u-1",
|
|
475
|
-
status: "active",
|
|
476
|
-
policy: "allow",
|
|
477
|
-
externalChatId: "chat-1",
|
|
478
|
-
verifiedAt: 1700000000,
|
|
479
|
-
verifiedVia: "code",
|
|
480
|
-
memberDisplayName: "Dora",
|
|
481
|
-
} satisfies TrustVerdict;
|
|
482
|
-
|
|
483
|
-
const member = resolvedMemberFromVerdict(verdict);
|
|
484
|
-
expect(member).not.toBeNull();
|
|
485
|
-
expect(member!.channel.id).toBe("channel-1");
|
|
486
|
-
expect(member!.channel.status).toBe("active");
|
|
487
|
-
expect(member!.channel.policy).toBe("allow");
|
|
488
|
-
expect(member!.channel.verifiedAt).toBe(1700000000);
|
|
489
|
-
expect(member!.channel.verifiedVia).toBe("code");
|
|
490
|
-
expect(member!.channel.externalChatId).toBe("chat-1");
|
|
491
|
-
|
|
492
|
-
expect(member!.contact.id).toBe("contact-1");
|
|
493
|
-
expect(member!.contact.displayName).toBe("Dora");
|
|
494
|
-
expect(member!.contact.role).toBe("contact");
|
|
495
|
-
// INFO fields must be null/default placeholders.
|
|
496
|
-
expect(member!.contact.notes).toBeNull();
|
|
497
|
-
expect(member!.contact.userFile).toBeNull();
|
|
498
|
-
expect(member!.contact.interactionCount).toBe(0);
|
|
499
|
-
expect(member!.contact.lastInteraction).toBeNull();
|
|
500
|
-
});
|
|
501
|
-
|
|
502
|
-
test("guardian member verdict maps role guardian + principalId", () => {
|
|
503
|
-
const verdict = {
|
|
504
|
-
trustClass: "guardian",
|
|
505
|
-
canonicalSenderId: "u-g",
|
|
506
|
-
contactId: "contact-g",
|
|
507
|
-
channelId: "channel-g",
|
|
508
|
-
guardianPrincipalId: "vellum-principal-g",
|
|
509
|
-
status: "active",
|
|
510
|
-
policy: "allow",
|
|
511
|
-
} satisfies TrustVerdict;
|
|
512
|
-
|
|
513
|
-
const member = resolvedMemberFromVerdict(verdict);
|
|
514
|
-
expect(member!.contact.role).toBe("guardian");
|
|
515
|
-
expect(member!.contact.principalId).toBe("vellum-principal-g");
|
|
516
|
-
});
|
|
517
|
-
|
|
518
|
-
test("memberless verdict (no contactId) returns null", () => {
|
|
519
|
-
const verdict = {
|
|
520
|
-
trustClass: "unknown",
|
|
521
|
-
canonicalSenderId: "u-2",
|
|
522
|
-
} satisfies TrustVerdict;
|
|
523
|
-
|
|
524
|
-
expect(resolvedMemberFromVerdict(verdict)).toBeNull();
|
|
525
|
-
});
|
|
526
|
-
|
|
527
|
-
test("member verdict missing status returns null (fail-closed)", () => {
|
|
528
|
-
const verdict = {
|
|
529
|
-
trustClass: "trusted_contact",
|
|
530
|
-
canonicalSenderId: "u-4",
|
|
531
|
-
contactId: "contact-4",
|
|
532
|
-
channelId: "channel-4",
|
|
533
|
-
policy: "allow",
|
|
534
|
-
} satisfies TrustVerdict;
|
|
535
|
-
|
|
536
|
-
expect(resolvedMemberFromVerdict(verdict)).toBeNull();
|
|
537
|
-
});
|
|
538
|
-
|
|
539
|
-
test("member verdict missing policy returns null (fail-closed)", () => {
|
|
540
|
-
const verdict = {
|
|
541
|
-
trustClass: "trusted_contact",
|
|
542
|
-
canonicalSenderId: "u-5",
|
|
543
|
-
contactId: "contact-5",
|
|
544
|
-
channelId: "channel-5",
|
|
545
|
-
status: "active",
|
|
546
|
-
} satisfies TrustVerdict;
|
|
547
|
-
|
|
548
|
-
expect(resolvedMemberFromVerdict(verdict)).toBeNull();
|
|
549
|
-
});
|
|
550
|
-
|
|
551
|
-
test("member verdict with unknown policy returns null (fail-closed)", () => {
|
|
552
|
-
const verdict = {
|
|
553
|
-
trustClass: "trusted_contact",
|
|
554
|
-
canonicalSenderId: "u-6",
|
|
555
|
-
contactId: "contact-6",
|
|
556
|
-
channelId: "channel-6",
|
|
557
|
-
status: "active",
|
|
558
|
-
policy: "bogus",
|
|
559
|
-
} satisfies TrustVerdict;
|
|
560
|
-
|
|
561
|
-
expect(resolvedMemberFromVerdict(verdict)).toBeNull();
|
|
562
|
-
});
|
|
563
|
-
|
|
564
|
-
test("member verdict with unknown status returns null (fail-closed)", () => {
|
|
565
|
-
const verdict = {
|
|
566
|
-
trustClass: "trusted_contact",
|
|
567
|
-
canonicalSenderId: "u-7",
|
|
568
|
-
contactId: "contact-7",
|
|
569
|
-
channelId: "channel-7",
|
|
570
|
-
status: "quarantined",
|
|
571
|
-
policy: "allow",
|
|
572
|
-
} satisfies TrustVerdict;
|
|
573
|
-
|
|
574
|
-
expect(resolvedMemberFromVerdict(verdict)).toBeNull();
|
|
575
|
-
});
|
|
576
|
-
|
|
577
|
-
test("member verdict with valid known status+policy returns a member", () => {
|
|
578
|
-
const verdict = {
|
|
579
|
-
trustClass: "trusted_contact",
|
|
580
|
-
canonicalSenderId: "u-8",
|
|
581
|
-
contactId: "contact-8",
|
|
582
|
-
channelId: "channel-8",
|
|
583
|
-
status: "active",
|
|
584
|
-
policy: "allow",
|
|
585
|
-
} satisfies TrustVerdict;
|
|
586
|
-
|
|
587
|
-
const member = resolvedMemberFromVerdict(verdict);
|
|
588
|
-
expect(member).not.toBeNull();
|
|
589
|
-
expect(member!.channel.status).toBe("active");
|
|
590
|
-
expect(member!.channel.policy).toBe("allow");
|
|
591
|
-
});
|
|
592
|
-
|
|
593
|
-
test("blocked/revoked verdict surfaces channel.status verbatim", () => {
|
|
594
|
-
for (const status of ["blocked", "revoked"] as const) {
|
|
595
|
-
const verdict = {
|
|
596
|
-
trustClass: "unknown",
|
|
597
|
-
canonicalSenderId: "u-3",
|
|
598
|
-
contactId: "contact-3",
|
|
599
|
-
channelId: "channel-3",
|
|
600
|
-
status,
|
|
601
|
-
policy: "deny",
|
|
602
|
-
} satisfies TrustVerdict;
|
|
603
|
-
|
|
604
|
-
const member = resolvedMemberFromVerdict(verdict);
|
|
605
|
-
expect(member!.channel.status).toBe(status);
|
|
606
|
-
expect(member!.channel.policy).toBe("deny");
|
|
607
|
-
}
|
|
608
|
-
});
|
|
609
|
-
});
|
|
610
|
-
|
|
611
537
|
describe("verdictMemberFromVerdict", () => {
|
|
612
538
|
test("active member verdict yields the narrow ACL view", () => {
|
|
613
539
|
const verdict = {
|
|
@@ -684,6 +610,27 @@ describe("verdictMemberFromVerdict", () => {
|
|
|
684
610
|
} satisfies TrustVerdict),
|
|
685
611
|
).toBeNull();
|
|
686
612
|
});
|
|
613
|
+
|
|
614
|
+
test("missing status or policy returns null (fail-closed)", () => {
|
|
615
|
+
expect(
|
|
616
|
+
verdictMemberFromVerdict({
|
|
617
|
+
trustClass: "trusted_contact",
|
|
618
|
+
canonicalSenderId: "u-4",
|
|
619
|
+
contactId: "contact-4",
|
|
620
|
+
channelId: "channel-4",
|
|
621
|
+
policy: "allow",
|
|
622
|
+
} satisfies TrustVerdict),
|
|
623
|
+
).toBeNull();
|
|
624
|
+
expect(
|
|
625
|
+
verdictMemberFromVerdict({
|
|
626
|
+
trustClass: "trusted_contact",
|
|
627
|
+
canonicalSenderId: "u-5",
|
|
628
|
+
contactId: "contact-5",
|
|
629
|
+
channelId: "channel-5",
|
|
630
|
+
status: "active",
|
|
631
|
+
} satisfies TrustVerdict),
|
|
632
|
+
).toBeNull();
|
|
633
|
+
});
|
|
687
634
|
});
|
|
688
635
|
|
|
689
636
|
describe("verdict predicates", () => {
|
|
@@ -97,11 +97,10 @@ export async function notifyGuardianOfAccessRequest(
|
|
|
97
97
|
|
|
98
98
|
// Resolve guardian identity with the assistant-anchored strategy (gateway
|
|
99
99
|
// source-channel match validated against the vellum anchor, else the vellum
|
|
100
|
-
// anchor)
|
|
100
|
+
// anchor).
|
|
101
101
|
const anchored = resolveAnchoredGuardian({
|
|
102
102
|
guardians: await getGuardianDelivery(),
|
|
103
103
|
sourceChannel,
|
|
104
|
-
useLocalFallback: true,
|
|
105
104
|
});
|
|
106
105
|
const guardianExternalUserId = anchored?.address ?? null;
|
|
107
106
|
const guardianPrincipalId = anchored?.principalId ?? null;
|
|
@@ -17,15 +17,23 @@
|
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
import type { ChannelId } from "../channels/types.js";
|
|
20
|
+
import { findContactByAddress } from "../contacts/contact-store.js";
|
|
20
21
|
import {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
} from "../contacts/
|
|
22
|
+
guardianForChannel,
|
|
23
|
+
peekCachedGuardianDelivery,
|
|
24
|
+
} from "../contacts/guardian-delivery-reader.js";
|
|
24
25
|
import { channelStatusToMemberStatus } from "../contacts/member-status.js";
|
|
25
|
-
import type {
|
|
26
|
+
import type {
|
|
27
|
+
ChannelPolicy,
|
|
28
|
+
ChannelStatus,
|
|
29
|
+
ContactChannel,
|
|
30
|
+
ContactRole,
|
|
31
|
+
ContactWithChannels,
|
|
32
|
+
} from "../contacts/types.js";
|
|
26
33
|
import type { TrustContext } from "../daemon/trust-context.js";
|
|
27
34
|
import { canonicalizeInboundIdentity } from "../util/canonicalize-identity.js";
|
|
28
35
|
import { getLogger } from "../util/logger.js";
|
|
36
|
+
import { getCachedMemberAcl } from "./member-verdict-cache.js";
|
|
29
37
|
|
|
30
38
|
const log = getLogger("actor-trust-resolver");
|
|
31
39
|
|
|
@@ -78,10 +86,18 @@ export interface ActorTrustContext {
|
|
|
78
86
|
} | null;
|
|
79
87
|
/** Canonical principal ID from the guardian binding. */
|
|
80
88
|
guardianPrincipalId?: string;
|
|
81
|
-
/**
|
|
89
|
+
/**
|
|
90
|
+
* Resolved contact + channel for this sender, if any. The ACL view
|
|
91
|
+
* (status/policy/role) is carried here rather than on the contact/channel
|
|
92
|
+
* objects, sourced from the gateway verdict — the verdict path reads it
|
|
93
|
+
* inline, the sync fallback from the in-memory member-verdict cache.
|
|
94
|
+
*/
|
|
82
95
|
memberRecord: {
|
|
83
96
|
contact: ContactWithChannels;
|
|
84
97
|
channel: ContactChannel;
|
|
98
|
+
status: ChannelStatus;
|
|
99
|
+
policy: ChannelPolicy;
|
|
100
|
+
role: ContactRole;
|
|
85
101
|
} | null;
|
|
86
102
|
/** Trust classification. */
|
|
87
103
|
trustClass: TrustClass;
|
|
@@ -175,21 +191,27 @@ export function resolveActorTrust(
|
|
|
175
191
|
}
|
|
176
192
|
|
|
177
193
|
// --- Guardian lookup ---
|
|
178
|
-
|
|
194
|
+
// Sync read of the gateway guardian delivery from the IO-free cache snapshot
|
|
195
|
+
// (kept warm by the async hot paths + daemon-startup warm). A cold cache
|
|
196
|
+
// yields no guardian match, the same outcome as no binding.
|
|
197
|
+
const cachedGuardians = peekCachedGuardianDelivery({
|
|
198
|
+
channelTypes: [input.sourceChannel],
|
|
199
|
+
});
|
|
200
|
+
const guardianDelivery = cachedGuardians
|
|
201
|
+
? guardianForChannel(cachedGuardians, input.sourceChannel)
|
|
202
|
+
: undefined;
|
|
179
203
|
let guardianBindingMatch: ActorTrustContext["guardianBindingMatch"] = null;
|
|
180
204
|
let guardianPrincipalId: string | undefined;
|
|
181
205
|
let isGuardian = false;
|
|
182
206
|
|
|
183
|
-
if (
|
|
184
|
-
const { contact: guardianContact, channel: guardianChannel } =
|
|
185
|
-
guardianResult;
|
|
207
|
+
if (guardianDelivery) {
|
|
186
208
|
guardianBindingMatch = {
|
|
187
|
-
guardianExternalUserId:
|
|
188
|
-
guardianDeliveryChatId:
|
|
209
|
+
guardianExternalUserId: guardianDelivery.address,
|
|
210
|
+
guardianDeliveryChatId: guardianDelivery.externalChatId ?? null,
|
|
189
211
|
};
|
|
190
|
-
guardianPrincipalId =
|
|
212
|
+
guardianPrincipalId = guardianDelivery.principalId ?? undefined;
|
|
191
213
|
isGuardian =
|
|
192
|
-
|
|
214
|
+
guardianDelivery.address.toLowerCase() === canonicalSenderId.toLowerCase();
|
|
193
215
|
}
|
|
194
216
|
|
|
195
217
|
log.debug(
|
|
@@ -213,7 +235,12 @@ export function resolveActorTrust(
|
|
|
213
235
|
ch.address.toLowerCase() === canonicalSenderId.toLowerCase(),
|
|
214
236
|
);
|
|
215
237
|
if (byAddress && byAddressChannel) {
|
|
216
|
-
|
|
238
|
+
const acl = getCachedMemberAcl(input.sourceChannel, canonicalSenderId);
|
|
239
|
+
if (acl) {
|
|
240
|
+
memberRecord = { contact: byAddress, channel: byAddressChannel, ...acl };
|
|
241
|
+
}
|
|
242
|
+
// Fail-closed: already in the sync fallback (no live verdict) and no cached
|
|
243
|
+
// verdict → leave memberRecord null so trustClass resolves to unknown.
|
|
217
244
|
}
|
|
218
245
|
|
|
219
246
|
log.debug(
|
|
@@ -252,7 +279,7 @@ export function resolveActorTrust(
|
|
|
252
279
|
if (isGuardian) {
|
|
253
280
|
trustClass = "guardian";
|
|
254
281
|
} else if (memberMatchesSender && memberRecord) {
|
|
255
|
-
const status = memberRecord.
|
|
282
|
+
const status = memberRecord.status;
|
|
256
283
|
if (status === "active") {
|
|
257
284
|
trustClass = "trusted_contact";
|
|
258
285
|
} else if (status === "unverified" || status === "pending") {
|
|
@@ -327,8 +354,8 @@ export function toTrustContext(
|
|
|
327
354
|
// both populate it).
|
|
328
355
|
requesterContactId: ctx.memberRecord?.contact.id,
|
|
329
356
|
memberStatus: ctx.memberRecord
|
|
330
|
-
? channelStatusToMemberStatus(ctx.memberRecord.
|
|
357
|
+
? channelStatusToMemberStatus(ctx.memberRecord.status)
|
|
331
358
|
: undefined,
|
|
332
|
-
memberPolicy: ctx.memberRecord?.
|
|
359
|
+
memberPolicy: ctx.memberRecord?.policy,
|
|
333
360
|
};
|
|
334
361
|
}
|
|
@@ -2,24 +2,14 @@
|
|
|
2
2
|
* Unit tests for `resolveAnchoredGuardian`.
|
|
3
3
|
*
|
|
4
4
|
* Covers the gateway arms (source-channel match validated against the vellum
|
|
5
|
-
* anchor, vellum-anchor fallback)
|
|
6
|
-
*
|
|
5
|
+
* anchor, vellum-anchor fallback) and the cosmetic `requireAnchorPrincipal`
|
|
6
|
+
* guard.
|
|
7
7
|
*/
|
|
8
|
-
import {
|
|
8
|
+
import { describe, expect, test } from "bun:test";
|
|
9
9
|
|
|
10
10
|
import type { GuardianDelivery } from "@vellumai/gateway-client";
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
let localGuardians: Record<
|
|
14
|
-
string,
|
|
15
|
-
{ contact: { principalId: string | null; displayName: string }; channel: { address: string; type: string } } | null
|
|
16
|
-
> = {};
|
|
17
|
-
mock.module("../contacts/contact-store.js", () => ({
|
|
18
|
-
findGuardianForChannel: (channelType: string) =>
|
|
19
|
-
localGuardians[channelType] ?? null,
|
|
20
|
-
}));
|
|
21
|
-
|
|
22
|
-
const { resolveAnchoredGuardian } = await import("./anchored-guardian.js");
|
|
12
|
+
import { resolveAnchoredGuardian } from "./anchored-guardian.js";
|
|
23
13
|
|
|
24
14
|
function gw(g: Partial<GuardianDelivery> & { channelType: string; address: string }): GuardianDelivery {
|
|
25
15
|
return {
|
|
@@ -29,10 +19,6 @@ function gw(g: Partial<GuardianDelivery> & { channelType: string; address: strin
|
|
|
29
19
|
};
|
|
30
20
|
}
|
|
31
21
|
|
|
32
|
-
afterEach(() => {
|
|
33
|
-
localGuardians = {};
|
|
34
|
-
});
|
|
35
|
-
|
|
36
22
|
describe("resolveAnchoredGuardian — gateway arm", () => {
|
|
37
23
|
test("source-channel guardian matching the anchor wins", () => {
|
|
38
24
|
const result = resolveAnchoredGuardian({
|
|
@@ -80,40 +66,8 @@ describe("resolveAnchoredGuardian — gateway arm", () => {
|
|
|
80
66
|
});
|
|
81
67
|
});
|
|
82
68
|
|
|
83
|
-
describe("resolveAnchoredGuardian —
|
|
84
|
-
test("gateway
|
|
85
|
-
localGuardians = {
|
|
86
|
-
vellum: { contact: { principalId: "p-local", displayName: "LocalVellum" }, channel: { address: "lv-addr", type: "vellum" } },
|
|
87
|
-
telegram: { contact: { principalId: "p-local", displayName: "LocalAlice" }, channel: { address: "ltg-addr", type: "telegram" } },
|
|
88
|
-
};
|
|
89
|
-
const result = resolveAnchoredGuardian({
|
|
90
|
-
guardians: null,
|
|
91
|
-
sourceChannel: "telegram",
|
|
92
|
-
useLocalFallback: true,
|
|
93
|
-
});
|
|
94
|
-
expect(result).toEqual({
|
|
95
|
-
principalId: "p-local",
|
|
96
|
-
address: "ltg-addr",
|
|
97
|
-
displayName: "LocalAlice",
|
|
98
|
-
channelType: "telegram",
|
|
99
|
-
source: "source-channel-contact",
|
|
100
|
-
});
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
test("gateway empty + only local vellum returns the local vellum-anchor", () => {
|
|
104
|
-
localGuardians = {
|
|
105
|
-
vellum: { contact: { principalId: "p-local", displayName: "LocalVellum" }, channel: { address: "lv-addr", type: "vellum" } },
|
|
106
|
-
};
|
|
107
|
-
const result = resolveAnchoredGuardian({
|
|
108
|
-
guardians: null,
|
|
109
|
-
sourceChannel: "telegram",
|
|
110
|
-
useLocalFallback: true,
|
|
111
|
-
});
|
|
112
|
-
expect(result?.source).toBe("vellum-anchor");
|
|
113
|
-
expect(result?.address).toBe("lv-addr");
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
test("gateway empty + no local + fallback disabled returns null", () => {
|
|
69
|
+
describe("resolveAnchoredGuardian — gateway empty", () => {
|
|
70
|
+
test("null gateway list returns null", () => {
|
|
117
71
|
const result = resolveAnchoredGuardian({
|
|
118
72
|
guardians: null,
|
|
119
73
|
sourceChannel: "telegram",
|
|
@@ -121,11 +75,10 @@ describe("resolveAnchoredGuardian — local fallback", () => {
|
|
|
121
75
|
expect(result).toBeNull();
|
|
122
76
|
});
|
|
123
77
|
|
|
124
|
-
test("
|
|
78
|
+
test("empty gateway list returns null", () => {
|
|
125
79
|
const result = resolveAnchoredGuardian({
|
|
126
80
|
guardians: [],
|
|
127
81
|
sourceChannel: "telegram",
|
|
128
|
-
useLocalFallback: true,
|
|
129
82
|
});
|
|
130
83
|
expect(result).toBeNull();
|
|
131
84
|
});
|
|
@@ -7,16 +7,13 @@
|
|
|
7
7
|
* anchor identity is used. This blocks stale or cross-assistant contacts from
|
|
8
8
|
* being bound to a request.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* (restart, timeout, malformed IPC). The local fallback is transitional and is
|
|
13
|
-
* removed in a later step.
|
|
10
|
+
* Resolves from the gateway delivery list, the source of truth for guardian
|
|
11
|
+
* principals.
|
|
14
12
|
*/
|
|
15
13
|
|
|
16
14
|
import type { GuardianDelivery } from "@vellumai/gateway-client";
|
|
17
15
|
|
|
18
16
|
import type { ChannelId } from "../channels/types.js";
|
|
19
|
-
import { findGuardianForChannel } from "../contacts/contact-store.js";
|
|
20
17
|
import { guardianForChannel } from "../contacts/guardian-delivery-reader.js";
|
|
21
18
|
import type { GuardianResolutionSource } from "../notifications/signal.js";
|
|
22
19
|
|
|
@@ -33,13 +30,6 @@ export interface ResolveAnchoredGuardianInput {
|
|
|
33
30
|
/** Gateway delivery list; `null` when the gateway read failed/was empty. */
|
|
34
31
|
guardians: GuardianDelivery[] | null;
|
|
35
32
|
sourceChannel: ChannelId;
|
|
36
|
-
/**
|
|
37
|
-
* Fall back to the LOCAL dual-written binding when the gateway arm resolves
|
|
38
|
-
* nothing. Access-request enables this to avoid an undecidable request; the
|
|
39
|
-
* cosmetic guardian-label path leaves it off so a missing gateway read
|
|
40
|
-
* degrades gracefully to the default reference.
|
|
41
|
-
*/
|
|
42
|
-
useLocalFallback?: boolean;
|
|
43
33
|
/**
|
|
44
34
|
* Require a non-null anchor principal for the vellum-anchor arm. When the
|
|
45
35
|
* vellum guardian has no principal, return `null` instead of a vellum-anchor
|
|
@@ -52,12 +42,12 @@ export interface ResolveAnchoredGuardianInput {
|
|
|
52
42
|
/**
|
|
53
43
|
* Resolve the anchored guardian for `sourceChannel`, or `null` when none can be
|
|
54
44
|
* resolved. Gateway source-channel match → that record; gateway anchor-only →
|
|
55
|
-
* vellum-anchor
|
|
45
|
+
* vellum-anchor.
|
|
56
46
|
*/
|
|
57
47
|
export function resolveAnchoredGuardian(
|
|
58
48
|
input: ResolveAnchoredGuardianInput,
|
|
59
49
|
): AnchoredGuardian | null {
|
|
60
|
-
const { sourceChannel,
|
|
50
|
+
const { sourceChannel, requireAnchorPrincipal } = input;
|
|
61
51
|
const guardians = input.guardians ?? [];
|
|
62
52
|
|
|
63
53
|
const vellumGuardian = guardianForChannel(guardians, "vellum");
|
|
@@ -92,44 +82,5 @@ export function resolveAnchoredGuardian(
|
|
|
92
82
|
};
|
|
93
83
|
}
|
|
94
84
|
|
|
95
|
-
// Gateway resolved a principal — done.
|
|
96
|
-
if (resolved?.principalId) {
|
|
97
|
-
return resolved;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
if (!useLocalFallback) {
|
|
101
|
-
return resolved;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// Fallback: gateway read was empty/unavailable (or carried no principal), so
|
|
105
|
-
// resolve from the LOCAL dual-written binding to avoid an undecidable
|
|
106
|
-
// request. A local match overwrites the principal-less gateway record; with
|
|
107
|
-
// no local match the gateway record (if any) is retained.
|
|
108
|
-
const localVellum = findGuardianForChannel("vellum");
|
|
109
|
-
const localAnchorPrincipalId = localVellum?.contact.principalId;
|
|
110
|
-
const localSource = findGuardianForChannel(sourceChannel);
|
|
111
|
-
if (
|
|
112
|
-
localAnchorPrincipalId &&
|
|
113
|
-
localSource &&
|
|
114
|
-
localSource.contact.principalId === localAnchorPrincipalId
|
|
115
|
-
) {
|
|
116
|
-
return {
|
|
117
|
-
principalId: localSource.contact.principalId,
|
|
118
|
-
address: localSource.channel.address,
|
|
119
|
-
displayName: localSource.contact.displayName,
|
|
120
|
-
channelType: localSource.channel.type,
|
|
121
|
-
source: "source-channel-contact",
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
if (localVellum) {
|
|
125
|
-
return {
|
|
126
|
-
principalId: localAnchorPrincipalId ?? null,
|
|
127
|
-
address: localVellum.channel.address,
|
|
128
|
-
displayName: localVellum.contact.displayName,
|
|
129
|
-
channelType: "vellum",
|
|
130
|
-
source: "vellum-anchor",
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
|
|
134
85
|
return resolved;
|
|
135
86
|
}
|