@vellumai/assistant 0.10.2-dev.202606251104.36cd100 → 0.10.2-dev.202606251348.a66ca6e

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.
Files changed (137) hide show
  1. package/openapi.yaml +0 -35
  2. package/package.json +1 -1
  3. package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +36 -27
  4. package/src/__tests__/channel-approval-routes.test.ts +21 -26
  5. package/src/__tests__/channel-guardian.test.ts +82 -32
  6. package/src/__tests__/channel-inbound-disk-pressure.test.ts +11 -19
  7. package/src/__tests__/contact-store-interaction-info.test.ts +156 -0
  8. package/src/__tests__/contact-store-user-file.test.ts +7 -10
  9. package/src/__tests__/contacts-relay-reads.test.ts +6 -9
  10. package/src/__tests__/contacts-write.test.ts +0 -2
  11. package/src/__tests__/conversation-attention-telegram.test.ts +9 -11
  12. package/src/__tests__/delete-propagation.test.ts +5 -3
  13. package/src/__tests__/dm-backfill.test.ts +6 -4
  14. package/src/__tests__/emit-signal-routing-intent.test.ts +2 -6
  15. package/src/__tests__/guardian-binding-drift-heal.test.ts +43 -23
  16. package/src/__tests__/guardian-dispatch.test.ts +50 -5
  17. package/src/__tests__/guardian-routing-state.test.ts +6 -10
  18. package/src/__tests__/helpers/channel-test-adapter.ts +45 -12
  19. package/src/__tests__/helpers/create-guardian-binding.ts +15 -23
  20. package/src/__tests__/helpers/seed-contact-channel.ts +77 -0
  21. package/src/__tests__/inbound-invite-redemption.test.ts +87 -10
  22. package/src/__tests__/invite-redemption-service.test.ts +273 -53
  23. package/src/__tests__/invite-routes-http.test.ts +34 -0
  24. package/src/__tests__/invite-service-ipc.test.ts +65 -2
  25. package/src/__tests__/non-member-access-request.test.ts +15 -13
  26. package/src/__tests__/onboarding-persona-write.test.ts +52 -22
  27. package/src/__tests__/persona-resolver.test.ts +75 -45
  28. package/src/__tests__/plugin-bootstrap.test.ts +5 -5
  29. package/src/__tests__/plugin-route-contribution.test.ts +2 -2
  30. package/src/__tests__/plugin-tool-contribution.test.ts +2 -2
  31. package/src/__tests__/plugin-types.test.ts +2 -2
  32. package/src/__tests__/reaction-intercept-cold-cache-warm.test.ts +135 -0
  33. package/src/__tests__/reaction-persistence.test.ts +51 -4
  34. package/src/__tests__/relay-server.test.ts +88 -31
  35. package/src/__tests__/runtime-attachment-metadata.test.ts +9 -11
  36. package/src/__tests__/settings-routes.test.ts +32 -0
  37. package/src/__tests__/sse-actor-principal-guardian-source.test.ts +13 -36
  38. package/src/__tests__/stt-hints.test.ts +0 -2
  39. package/src/__tests__/thread-backfill.test.ts +3 -3
  40. package/src/__tests__/trusted-contact-approval-notifier.test.ts +37 -51
  41. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +2 -2
  42. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +9 -7
  43. package/src/__tests__/trusted-contact-multichannel.test.ts +13 -7
  44. package/src/__tests__/trusted-contact-verification.test.ts +50 -54
  45. package/src/__tests__/voice-guardian-cold-cache-warm.test.ts +138 -0
  46. package/src/__tests__/voice-invite-redemption.test.ts +183 -20
  47. package/src/__tests__/workspace-migration-drop-user-md.test.ts +196 -238
  48. package/src/a2a/__tests__/e2e-a2a-channel.test.ts +27 -47
  49. package/src/approvals/guardian-request-resolvers.ts +16 -4
  50. package/src/calls/__tests__/relay-setup-router.test.ts +7 -15
  51. package/src/calls/guardian-dispatch.ts +14 -11
  52. package/src/calls/relay-access-wait.ts +9 -7
  53. package/src/calls/relay-server.ts +22 -2
  54. package/src/calls/relay-setup-router.ts +10 -10
  55. package/src/cli/commands/contacts.ts +10 -7
  56. package/src/cli/commands/plugins.ts +194 -0
  57. package/src/cli/lib/__tests__/publish-plugin.test.ts +306 -0
  58. package/src/cli/lib/publish-plugin.ts +403 -0
  59. package/src/contacts/__tests__/contacts-write-revoke-relay.test.ts +7 -8
  60. package/src/contacts/__tests__/member-write-relay.test.ts +35 -11
  61. package/src/contacts/contact-store.ts +43 -227
  62. package/src/contacts/contacts-write.ts +18 -58
  63. package/src/contacts/gateway-channel-read.ts +51 -0
  64. package/src/contacts/member-write-relay.ts +25 -31
  65. package/src/contacts/types.ts +2 -15
  66. package/src/daemon/external-plugins-bootstrap.ts +5 -5
  67. package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +0 -1
  68. package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +0 -2
  69. package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +0 -2
  70. package/src/daemon/handlers/__tests__/config-channels.test.ts +9 -14
  71. package/src/daemon/handlers/config-channels.ts +14 -29
  72. package/src/daemon/lifecycle.ts +11 -0
  73. package/src/heartbeat/heartbeat-service.ts +5 -0
  74. package/src/home/relationship-state-writer.ts +5 -0
  75. package/src/hooks/hook-loader.ts +13 -13
  76. package/src/memory/memory-retrospective-job.ts +5 -0
  77. package/src/notifications/__tests__/broadcaster.test.ts +0 -8
  78. package/src/notifications/__tests__/connected-channels.test.ts +8 -36
  79. package/src/notifications/__tests__/destination-resolver.test.ts +12 -117
  80. package/src/notifications/destination-resolver.ts +7 -23
  81. package/src/notifications/emit-signal.ts +5 -11
  82. package/src/plugin-api/index.ts +6 -6
  83. package/src/plugin-api/types.ts +9 -9
  84. package/src/plugins/defaults/advisor/hooks/post-model-call.ts +2 -2
  85. package/src/plugins/defaults/advisor/hooks/pre-model-call.ts +2 -2
  86. package/src/plugins/defaults/advisor/hooks/user-prompt-submit.ts +2 -2
  87. package/src/plugins/defaults/empty-response/hooks/post-model-call.ts +2 -2
  88. package/src/plugins/defaults/empty-response/hooks/stop.ts +2 -2
  89. package/src/plugins/defaults/exploration-drift/hooks/post-tool-use.ts +2 -2
  90. package/src/plugins/defaults/history-repair/hooks/post-model-call.ts +2 -2
  91. package/src/plugins/defaults/history-repair/hooks/stop.ts +2 -2
  92. package/src/plugins/defaults/history-repair/hooks/user-prompt-submit.ts +2 -2
  93. package/src/plugins/defaults/image-fallback/hooks/post-tool-use.ts +2 -2
  94. package/src/plugins/defaults/image-fallback/hooks/user-prompt-submit.ts +2 -2
  95. package/src/plugins/defaults/image-recovery/hooks/post-model-call.ts +2 -2
  96. package/src/plugins/defaults/image-recovery/hooks/stop.ts +2 -2
  97. package/src/plugins/defaults/max-tokens-continue/hooks/post-model-call.ts +2 -2
  98. package/src/plugins/defaults/max-tokens-continue/hooks/stop.ts +2 -2
  99. package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +2 -2
  100. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit.ts +2 -2
  101. package/src/plugins/defaults/memory-v3-shadow/hooks/post-compact.ts +2 -2
  102. package/src/plugins/defaults/memory-v3-shadow/hooks/user-prompt-submit.ts +2 -2
  103. package/src/plugins/defaults/surface-completion-nudge/hooks/post-model-call.ts +2 -2
  104. package/src/plugins/defaults/surface-completion-nudge/hooks/stop.ts +2 -2
  105. package/src/plugins/defaults/task-progress-nudge/hooks/post-tool-use.ts +2 -2
  106. package/src/plugins/defaults/title-generate/hooks/stop.ts +2 -2
  107. package/src/plugins/defaults/title-generate/hooks/user-prompt-submit.ts +2 -2
  108. package/src/plugins/defaults/tool-error/hooks/post-tool-use.ts +2 -2
  109. package/src/plugins/defaults/tool-result-truncate/hooks/post-tool-use.ts +2 -2
  110. package/src/plugins/external-plugin-loader.ts +2 -2
  111. package/src/plugins/mtime-cache.ts +5 -8
  112. package/src/plugins/pipeline.ts +2 -2
  113. package/src/plugins/registry.ts +5 -5
  114. package/src/plugins/types.ts +7 -7
  115. package/src/prompts/persona-resolver.ts +43 -11
  116. package/src/runtime/__tests__/guardian-vellum-migration.test.ts +30 -27
  117. package/src/runtime/__tests__/local-principal-trust.test.ts +17 -18
  118. package/src/runtime/__tests__/trust-verdict-consumer.test.ts +114 -168
  119. package/src/runtime/access-request-helper.ts +1 -2
  120. package/src/runtime/actor-trust-resolver.ts +44 -15
  121. package/src/runtime/anchored-guardian.test.ts +7 -54
  122. package/src/runtime/anchored-guardian.ts +4 -53
  123. package/src/runtime/guardian-vellum-migration.ts +18 -16
  124. package/src/runtime/invite-redemption-service.ts +25 -10
  125. package/src/runtime/local-actor-identity.test.ts +108 -0
  126. package/src/runtime/local-actor-identity.ts +27 -20
  127. package/src/runtime/routes/__tests__/contact-routes.test.ts +95 -2
  128. package/src/runtime/routes/__tests__/global-search-routes.test.ts +0 -2
  129. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +2 -1
  130. package/src/runtime/routes/contact-routes.ts +18 -11
  131. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +0 -10
  132. package/src/runtime/routes/inbound-stages/background-dispatch.ts +4 -8
  133. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +10 -0
  134. package/src/runtime/routes/settings-routes.ts +8 -3
  135. package/src/runtime/trust-verdict-consumer.ts +28 -41
  136. package/src/tools/types.ts +114 -23
  137. package/src/workspace/migrations/031-drop-user-md.ts +232 -148
package/openapi.yaml CHANGED
@@ -4915,20 +4915,13 @@ paths:
4915
4915
  - address
4916
4916
  - isPrimary
4917
4917
  - externalUserId
4918
- - status
4919
- - policy
4920
- - verifiedAt
4921
- - verifiedVia
4922
4918
  - lastSeenAt
4923
4919
  - interactionCount
4924
4920
  - lastInteraction
4925
- - revokedReason
4926
- - blockedReason
4927
4921
  additionalProperties: false
4928
4922
  required:
4929
4923
  - id
4930
4924
  - displayName
4931
- - role
4932
4925
  - interactionCount
4933
4926
  - createdAt
4934
4927
  - updatedAt
@@ -5076,20 +5069,13 @@ paths:
5076
5069
  - address
5077
5070
  - isPrimary
5078
5071
  - externalUserId
5079
- - status
5080
- - policy
5081
- - verifiedAt
5082
- - verifiedVia
5083
5072
  - lastSeenAt
5084
5073
  - interactionCount
5085
5074
  - lastInteraction
5086
- - revokedReason
5087
- - blockedReason
5088
5075
  additionalProperties: false
5089
5076
  required:
5090
5077
  - id
5091
5078
  - displayName
5092
- - role
5093
5079
  - interactionCount
5094
5080
  - createdAt
5095
5081
  - updatedAt
@@ -5206,20 +5192,13 @@ paths:
5206
5192
  - address
5207
5193
  - isPrimary
5208
5194
  - externalUserId
5209
- - status
5210
- - policy
5211
- - verifiedAt
5212
- - verifiedVia
5213
5195
  - lastSeenAt
5214
5196
  - interactionCount
5215
5197
  - lastInteraction
5216
- - revokedReason
5217
- - blockedReason
5218
5198
  additionalProperties: false
5219
5199
  required:
5220
5200
  - id
5221
5201
  - displayName
5222
- - role
5223
5202
  - interactionCount
5224
5203
  - createdAt
5225
5204
  - updatedAt
@@ -5581,20 +5560,13 @@ paths:
5581
5560
  - address
5582
5561
  - isPrimary
5583
5562
  - externalUserId
5584
- - status
5585
- - policy
5586
- - verifiedAt
5587
- - verifiedVia
5588
5563
  - lastSeenAt
5589
5564
  - interactionCount
5590
5565
  - lastInteraction
5591
- - revokedReason
5592
- - blockedReason
5593
5566
  additionalProperties: false
5594
5567
  required:
5595
5568
  - id
5596
5569
  - displayName
5597
- - role
5598
5570
  - interactionCount
5599
5571
  - createdAt
5600
5572
  - updatedAt
@@ -5776,20 +5748,13 @@ paths:
5776
5748
  - address
5777
5749
  - isPrimary
5778
5750
  - externalUserId
5779
- - status
5780
- - policy
5781
- - verifiedAt
5782
- - verifiedVia
5783
5751
  - lastSeenAt
5784
5752
  - interactionCount
5785
5753
  - lastInteraction
5786
- - revokedReason
5787
- - blockedReason
5788
5754
  additionalProperties: false
5789
5755
  required:
5790
5756
  - id
5791
5757
  - displayName
5792
- - role
5793
5758
  - interactionCount
5794
5759
  - createdAt
5795
5760
  - updatedAt
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/assistant",
3
- "version": "0.10.2-dev.202606251104.36cd100",
3
+ "version": "0.10.2-dev.202606251348.a66ca6e",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -7,12 +7,18 @@
7
7
  * and are discovered through the same path as every other channel.
8
8
  *
9
9
  * This suite verifies that address-based lookup returns the correct
10
- * `memberRecord` with the right channel/status so relay-setup-router
10
+ * `memberRecord` with the right ACL status so relay-setup-router
11
11
  * can emit the appropriate outcome (e.g. `unverified_caller`).
12
12
  */
13
13
 
14
14
  import { beforeEach, describe, expect, mock, test } from "bun:test";
15
15
 
16
+ import type {
17
+ ChannelPolicy,
18
+ ChannelStatus,
19
+ ContactRole,
20
+ } from "../contacts/types.js";
21
+
16
22
  // ── Logger mock (suppress output) ───────────────────────────────────────────
17
23
  mock.module("../util/logger.js", () => ({
18
24
  getLogger: () =>
@@ -23,13 +29,21 @@ mock.module("../util/logger.js", () => ({
23
29
  let _byAddress: ReturnType<
24
30
  (typeof import("../contacts/contact-store.js"))["findContactByAddress"]
25
31
  > = null;
26
- let _guardian: ReturnType<
27
- (typeof import("../contacts/contact-store.js"))["findGuardianForChannel"]
28
- > = null;
32
+ // ACL view is carried on memberRecord, sourced from the local ACL columns via
33
+ // getLocalMemberAcl. Stub it per test instead of seeding the DB.
34
+ let _acl: { status: ChannelStatus; policy: ChannelPolicy; role: ContactRole } =
35
+ { status: "unverified", policy: "allow", role: "contact" };
29
36
 
30
37
  mock.module("../contacts/contact-store.js", () => ({
31
38
  findContactByAddress: (_type: string, _addr: string) => _byAddress,
32
- findGuardianForChannel: (_channel: string) => _guardian,
39
+ getLocalMemberAcl: (_channelId: string) => _acl,
40
+ }));
41
+
42
+ // Guardian resolution now reads the gateway delivery cache; these suites only
43
+ // exercise the member/address path, so the cache peek stays empty.
44
+ mock.module("../contacts/guardian-delivery-reader.js", () => ({
45
+ peekCachedGuardianDelivery: () => undefined,
46
+ guardianForChannel: () => undefined,
33
47
  }));
34
48
 
35
49
  // ── Real import after mocks ───────────────────────────────────────────────────
@@ -45,11 +59,12 @@ function makeContact(
45
59
  status: "unverified" | "active" = "unverified",
46
60
  ): ContactWithChannels {
47
61
  const channelId = "ch-test";
62
+ // ACL lives on memberRecord (carrier), sourced from getLocalMemberAcl — set
63
+ // the stub here so the resolver classifies trust off this status/role.
64
+ _acl = { status, policy: "allow", role };
48
65
  return {
49
66
  id: "contact-test",
50
67
  displayName: "Patrick Test",
51
- role,
52
- principalId: null,
53
68
  notes: null,
54
69
  lastInteraction: null,
55
70
  interactionCount: 0,
@@ -65,16 +80,10 @@ function makeContact(
65
80
  address: PHONE,
66
81
  externalChatId: null,
67
82
  isPrimary: true,
68
- status,
69
- policy: "allow",
70
- verifiedAt: null,
71
- verifiedVia: null,
72
- revokedReason: null,
73
- blockedReason: null,
83
+ inviteId: null,
84
+ lastSeenAt: null,
74
85
  interactionCount: 0,
75
86
  lastInteraction: null,
76
- lastSeenAt: null,
77
- inviteId: null,
78
87
  createdAt: 0,
79
88
  updatedAt: 0,
80
89
  },
@@ -87,7 +96,7 @@ function makeContact(
87
96
  describe("resolveActorTrust — address fallback", () => {
88
97
  beforeEach(() => {
89
98
  _byAddress = null;
90
- _guardian = null;
99
+ _acl = { status: "unverified", policy: "allow", role: "contact" };
91
100
  });
92
101
 
93
102
  test("finds unverified channel via address when externalUserId is null", () => {
@@ -103,7 +112,7 @@ describe("resolveActorTrust — address fallback", () => {
103
112
 
104
113
  expect(result.memberRecord).not.toBeNull();
105
114
  expect(result.memberRecord?.contact.displayName).toBe("Patrick Test");
106
- expect(result.memberRecord?.channel.status).toBe("unverified");
115
+ expect(result.memberRecord?.status).toBe("unverified");
107
116
  // trustClass is 'unverified_contact' for a member whose channel is
108
117
  // pending or unverified — known to the guardian but not yet verified.
109
118
  expect(result.trustClass).toBe("unverified_contact");
@@ -119,7 +128,7 @@ describe("resolveActorTrust — address fallback", () => {
119
128
  actorExternalId: PHONE,
120
129
  });
121
130
 
122
- expect(result.memberRecord?.channel.status).toBe("active");
131
+ expect(result.memberRecord?.status).toBe("active");
123
132
  expect(result.memberRecord?.channel.address).toBe(PHONE);
124
133
  });
125
134
 
@@ -150,7 +159,7 @@ describe("resolveActorTrust — address fallback", () => {
150
159
  });
151
160
 
152
161
  expect(result.memberRecord).not.toBeNull();
153
- expect(result.memberRecord?.channel.status).toBe("active");
162
+ expect(result.memberRecord?.status).toBe("active");
154
163
  expect(result.trustClass).toBe("trusted_contact");
155
164
  });
156
165
 
@@ -158,8 +167,8 @@ describe("resolveActorTrust — address fallback", () => {
158
167
  // Mirrors the unverified branch but for `pending` status (e.g. a phone
159
168
  // contact registered by name-capture awaiting the DTMF challenge).
160
169
  const contact = makeContact("contact", "unverified");
161
- // Override status to "pending" — makeContact only accepts unverified/active
162
- contact.channels[0]!.status = "pending";
170
+ // Override ACL status to "pending" — makeContact only accepts unverified/active.
171
+ _acl = { ..._acl, status: "pending" };
163
172
  _byAddress = contact;
164
173
 
165
174
  const result = resolveActorTrust({
@@ -169,15 +178,15 @@ describe("resolveActorTrust — address fallback", () => {
169
178
  actorExternalId: PHONE,
170
179
  });
171
180
 
172
- expect(result.memberRecord?.channel.status).toBe("pending");
181
+ expect(result.memberRecord?.status).toBe("pending");
173
182
  expect(result.trustClass).toBe("unverified_contact");
174
183
  });
175
184
 
176
185
  test("blocked-status member is classified as unknown (not unverified_contact)", () => {
177
186
  // Hard-deny statuses (blocked, revoked) stay `unknown` — admission-layer
178
- // re-checks channel.status and emits the hard-deny reasons.
187
+ // re-checks channel status and emits the hard-deny reasons.
179
188
  const contact = makeContact("contact", "unverified");
180
- contact.channels[0]!.status = "blocked";
189
+ _acl = { ..._acl, status: "blocked" };
181
190
  _byAddress = contact;
182
191
 
183
192
  const result = resolveActorTrust({
@@ -187,13 +196,13 @@ describe("resolveActorTrust — address fallback", () => {
187
196
  actorExternalId: PHONE,
188
197
  });
189
198
 
190
- expect(result.memberRecord?.channel.status).toBe("blocked");
199
+ expect(result.memberRecord?.status).toBe("blocked");
191
200
  expect(result.trustClass).toBe("unknown");
192
201
  });
193
202
 
194
203
  test("revoked-status member is classified as unknown", () => {
195
204
  const contact = makeContact("contact", "unverified");
196
- contact.channels[0]!.status = "revoked";
205
+ _acl = { ..._acl, status: "revoked" };
197
206
  _byAddress = contact;
198
207
 
199
208
  const result = resolveActorTrust({
@@ -203,7 +212,7 @@ describe("resolveActorTrust — address fallback", () => {
203
212
  actorExternalId: PHONE,
204
213
  });
205
214
 
206
- expect(result.memberRecord?.channel.status).toBe("revoked");
215
+ expect(result.memberRecord?.status).toBe("revoked");
207
216
  expect(result.trustClass).toBe("unknown");
208
217
  });
209
218
  });
@@ -69,7 +69,6 @@ mock.module("../daemon/approval-generators.js", () => ({
69
69
  createApprovalConversationGenerator: () => _testApprovalConversationGenerator,
70
70
  }));
71
71
 
72
- import { upsertContact } from "../contacts/contact-store.js";
73
72
  import type { Conversation } from "../daemon/conversation.js";
74
73
  import {
75
74
  createCanonicalGuardianDelivery,
@@ -86,7 +85,10 @@ import * as gatewayClient from "../runtime/gateway-client.js";
86
85
  import * as pendingInteractions from "../runtime/pending-interactions.js";
87
86
  import { _setTestPollMaxWait } from "../runtime/routes/channel-route-shared.js";
88
87
  import { resetDbForTesting } from "./db-test-helpers.js";
89
- import { handleChannelInbound } from "./helpers/channel-test-adapter.js";
88
+ import {
89
+ handleChannelInbound,
90
+ seedContactChannel,
91
+ } from "./helpers/channel-test-adapter.js";
90
92
  import { createGuardianBinding } from "./helpers/create-guardian-binding.js";
91
93
 
92
94
  await initializeDb();
@@ -212,22 +214,19 @@ function makeInboundRequest(overrides: Record<string, unknown> = {}): Request {
212
214
  const noopProcessMessage = mock(async () => ({ messageId: "msg-1" }));
213
215
 
214
216
  function ensureTestContact(): void {
215
- upsertContact({
217
+ seedContactChannel({
218
+ sourceChannel: "telegram",
219
+ externalUserId: "telegram-user-default",
216
220
  displayName: "Test User",
217
- channels: [
218
- {
219
- type: "telegram",
220
- address: "telegram-user-default",
221
- status: "active",
222
- policy: "allow",
223
- },
224
- {
225
- type: "slack",
226
- address: "slack-user-default",
227
- status: "active",
228
- policy: "allow",
229
- },
230
- ],
221
+ status: "active",
222
+ policy: "allow",
223
+ });
224
+ seedContactChannel({
225
+ sourceChannel: "slack",
226
+ externalUserId: "slack-user-default",
227
+ displayName: "Test User",
228
+ status: "active",
229
+ policy: "allow",
231
230
  });
232
231
  }
233
232
 
@@ -1926,16 +1925,12 @@ describe("trusted-contact self-approval blocked before guardian approval row exi
1926
1925
  guardianDeliveryChatId: "guardian-tc-selfapproval-chat",
1927
1926
  guardianPrincipalId: "guardian-tc-selfapproval",
1928
1927
  });
1929
- upsertContact({
1928
+ seedContactChannel({
1929
+ sourceChannel: "telegram",
1930
+ externalUserId: "tc-selfapproval-user",
1930
1931
  displayName: "TC Self-Approval User",
1931
- channels: [
1932
- {
1933
- type: "telegram",
1934
- address: "tc-selfapproval-user",
1935
- status: "active",
1936
- policy: "allow",
1937
- },
1938
- ],
1932
+ status: "active",
1933
+ policy: "allow",
1939
1934
  });
1940
1935
  });
1941
1936
 
@@ -65,45 +65,77 @@ mock.module("../runtime/assistant-event-hub.js", () => ({
65
65
 
66
66
  // Gateway relay mock — the revoke path relays the ACL downgrade over IPC and
67
67
  // validates the response; return a well-formed mark_channel_revoked result.
68
+ // The gateway owns the revoke and dual-writes the local assistant row to
69
+ // "revoked"; mirror that dual-write here so guardian-resolution reads under
70
+ // test observe the downgrade (the assistant-side teardown is now a no-op shim).
68
71
  mock.module("../ipc/gateway-client.js", () => ({
69
72
  ipcCallPersistent: async (
70
- _method: string,
73
+ method: string,
71
74
  params?: Record<string, unknown>,
72
- ) => ({
73
- ok: true,
74
- didWrite: true,
75
- channel: {
76
- id: (params?.contactChannelId as string) ?? "ch1",
77
- contactId: "c1",
78
- type: "phone",
79
- address: "addr",
80
- status: "revoked",
81
- revokedReason: (params?.reason as string) ?? null,
82
- },
83
- }),
75
+ ) => {
76
+ if (method === "mark_channel_revoked") {
77
+ const { getDb } = await import("../memory/db-connection.js");
78
+ const { contactChannels } = await import("../memory/schema.js");
79
+ const { eq } = await import("drizzle-orm");
80
+ const channelId = params?.contactChannelId as string | undefined;
81
+ if (channelId) {
82
+ getDb()
83
+ .update(contactChannels)
84
+ .set({ status: "revoked" })
85
+ .where(eq(contactChannels.id, channelId))
86
+ .run();
87
+ }
88
+ }
89
+ return {
90
+ ok: true,
91
+ didWrite: true,
92
+ channel: {
93
+ id: (params?.contactChannelId as string) ?? "ch1",
94
+ contactId: "c1",
95
+ type: "phone",
96
+ address: "addr",
97
+ status: "revoked",
98
+ revokedReason: (params?.reason as string) ?? null,
99
+ },
100
+ };
101
+ },
84
102
  }));
85
103
 
86
104
  // Guardian-delivery reader mock — the inbound challenge guard reads guardian
87
105
  // existence from the gateway. Derive the list from the local binding state so
88
106
  // the gateway-backed presence guard mirrors the DB the rest of the test sets up.
89
107
  const resolveGuardianList = async (input?: { channelTypes?: string[] }) => {
90
- const { findGuardianForChannel } = await import(
91
- "../contacts/contact-store.js"
92
- );
108
+ const { getDb } = await import("../memory/db-connection.js");
109
+ const { contacts, contactChannels } = await import("../memory/schema.js");
110
+ const { and, eq } = await import("drizzle-orm");
93
111
  const channels = input?.channelTypes ?? [];
94
112
  return channels
95
113
  .map((channelType) => {
96
- const found = findGuardianForChannel(channelType);
97
- if (!found) return null;
114
+ const row = getDb()
115
+ .select({ contact: contacts, channel: contactChannels })
116
+ .from(contacts)
117
+ .innerJoin(
118
+ contactChannels,
119
+ eq(contacts.id, contactChannels.contactId),
120
+ )
121
+ .where(
122
+ and(
123
+ eq(contacts.role, "guardian"),
124
+ eq(contactChannels.type, channelType),
125
+ eq(contactChannels.status, "active"),
126
+ ),
127
+ )
128
+ .get();
129
+ if (!row) return null;
98
130
  return {
99
131
  channelType,
100
- contactId: found.contact.id,
101
- principalId: found.contact.principalId ?? null,
102
- displayName: found.contact.displayName ?? null,
103
- address: found.channel.address,
104
- externalChatId: found.channel.externalChatId ?? null,
132
+ contactId: row.contact.id,
133
+ principalId: row.contact.principalId ?? null,
134
+ displayName: row.contact.displayName ?? null,
135
+ address: row.channel.address,
136
+ externalChatId: row.channel.externalChatId ?? null,
105
137
  status: "active",
106
- verifiedAt: found.channel.verifiedAt ?? null,
138
+ verifiedAt: row.channel.verifiedAt ?? null,
107
139
  };
108
140
  })
109
141
  .filter((g) => g !== null);
@@ -147,6 +179,7 @@ import {
147
179
  } from "../memory/guardian-rate-limits.js";
148
180
  import {
149
181
  channelVerificationSessions,
182
+ contactChannels,
150
183
  conversations,
151
184
  } from "../memory/schema.js";
152
185
  import {
@@ -192,6 +225,19 @@ function resetTables(): void {
192
225
  mockBotUsername = "test_bot";
193
226
  }
194
227
 
228
+ /**
229
+ * Revoke a guardian channel's local ACL state directly. The production revoke
230
+ * is gateway-owned (relayed via mark_channel_revoked); this stamps the local
231
+ * mirror so the guardian-resolution reads still under test see the downgrade.
232
+ */
233
+ function revokeGuardianChannelLocally(channelType: string): void {
234
+ getDb()
235
+ .update(contactChannels)
236
+ .set({ status: "revoked" })
237
+ .where(eq(contactChannels.type, channelType))
238
+ .run();
239
+ }
240
+
195
241
  // ═══════════════════════════════════════════════════════════════════════════
196
242
  // 2. Verification Challenge Lifecycle (Store)
197
243
  // ═══════════════════════════════════════════════════════════════════════════
@@ -558,7 +604,7 @@ describe("guardian identity check", () => {
558
604
  guardianDeliveryChatId: "chat-42",
559
605
  });
560
606
 
561
- serviceRevokeBinding("asst-1", "telegram");
607
+ revokeGuardianChannelLocally("telegram");
562
608
 
563
609
  expect(await isGuardian("asst-1", "telegram", "user-42")).toBe(false);
564
610
  });
@@ -595,7 +641,7 @@ describe("guardian identity check", () => {
595
641
  expect(await isGuardian("asst-1", "telegram", "phone-user-1")).toBe(false);
596
642
  });
597
643
 
598
- test("serviceRevokeBinding revokes the active binding", async () => {
644
+ test("guardian binding read reflects a gateway-owned revoke", async () => {
599
645
  createGuardianBinding({
600
646
  channel: "telegram",
601
647
  guardianExternalUserId: "user-42",
@@ -603,8 +649,10 @@ describe("guardian identity check", () => {
603
649
  guardianDeliveryChatId: "chat-42",
604
650
  });
605
651
 
606
- const result = serviceRevokeBinding("asst-1", "telegram");
607
- expect(result).toBe(true);
652
+ // The revoke is gateway-owned; serviceRevokeBinding's local teardown is a
653
+ // no-op shim. Stamp the local downgrade and assert the read reflects it.
654
+ serviceRevokeBinding("asst-1", "telegram");
655
+ revokeGuardianChannelLocally("telegram");
608
656
  expect(await getGuardianBinding("asst-1", "telegram")).toBeNull();
609
657
  });
610
658
  });
@@ -958,7 +1006,7 @@ describe("channel-scoped guardian resolution", () => {
958
1006
  guardianDeliveryChatId: "chat-beta",
959
1007
  });
960
1008
 
961
- serviceRevokeBinding("self", "telegram");
1009
+ revokeGuardianChannelLocally("telegram");
962
1010
 
963
1011
  expect(await getGuardianBinding("self", "telegram")).toBeNull();
964
1012
  expect(await getGuardianBinding("self", "phone")).not.toBeNull();
@@ -1472,8 +1520,10 @@ describe("voice guardian identity and revocation", () => {
1472
1520
  guardianDeliveryChatId: "voice-chat-1",
1473
1521
  });
1474
1522
 
1475
- const result = serviceRevokeBinding("asst-1", "phone");
1476
- expect(result).toBe(true);
1523
+ // The revoke is gateway-owned; serviceRevokeBinding's local teardown is a
1524
+ // no-op shim. Stamp the local downgrade and assert the read reflects it.
1525
+ serviceRevokeBinding("asst-1", "phone");
1526
+ revokeGuardianChannelLocally("phone");
1477
1527
  expect(await getGuardianBinding("asst-1", "phone")).toBeNull();
1478
1528
  });
1479
1529
 
@@ -1491,7 +1541,7 @@ describe("voice guardian identity and revocation", () => {
1491
1541
  guardianDeliveryChatId: "tg-chat-1",
1492
1542
  });
1493
1543
 
1494
- serviceRevokeBinding("asst-1", "phone");
1544
+ revokeGuardianChannelLocally("phone");
1495
1545
 
1496
1546
  expect(await getGuardianBinding("asst-1", "phone")).toBeNull();
1497
1547
  expect(await getGuardianBinding("asst-1", "telegram")).not.toBeNull();
@@ -59,7 +59,6 @@ mock.module("../daemon/disk-pressure-guard.js", () => ({
59
59
  diskPressureStatusSequence?.shift() ?? diskPressureStatus,
60
60
  }));
61
61
 
62
- import { upsertContact } from "../contacts/contact-store.js";
63
62
  import { getDb } from "../memory/db-connection.js";
64
63
  import { initializeDb } from "../memory/db-init.js";
65
64
  import * as deliveryCrud from "../memory/delivery-crud.js";
@@ -71,6 +70,7 @@ import {
71
70
  import { sweepFailedEvents } from "../runtime/channel-retry-sweep.js";
72
71
  import {
73
72
  handleChannelInbound,
73
+ seedContactChannel,
74
74
  setAdapterProcessMessage,
75
75
  } from "./helpers/channel-test-adapter.js";
76
76
  import { createGuardianBinding } from "./helpers/create-guardian-binding.js";
@@ -90,16 +90,12 @@ function resetTables(): void {
90
90
  }
91
91
 
92
92
  function seedTrustedContact(policy: "allow" | "escalate" = "allow"): void {
93
- upsertContact({
93
+ seedContactChannel({
94
+ sourceChannel: "telegram",
95
+ externalUserId: "telegram-user-1",
94
96
  displayName: "Example User",
95
- channels: [
96
- {
97
- type: "telegram",
98
- address: "telegram-user-1",
99
- status: "active",
100
- policy,
101
- },
102
- ],
97
+ status: "active",
98
+ policy,
103
99
  });
104
100
  }
105
101
 
@@ -236,16 +232,12 @@ describe("channel inbound disk pressure gate", () => {
236
232
  });
237
233
 
238
234
  test("blocks non-guardian Slack reactions silently (no reply) before persistence while locked", async () => {
239
- upsertContact({
235
+ seedContactChannel({
236
+ sourceChannel: "slack",
237
+ externalUserId: "slack-user-1",
240
238
  displayName: "Example Slack User",
241
- channels: [
242
- {
243
- type: "slack",
244
- address: "slack-user-1",
245
- status: "active",
246
- policy: "allow",
247
- },
248
- ],
239
+ status: "active",
240
+ policy: "allow",
249
241
  });
250
242
  const processMessage = mock(async () => {
251
243
  throw new Error("processMessage should not run");