@vellumai/assistant 0.4.29 → 0.4.31

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 (237) hide show
  1. package/ARCHITECTURE.md +39 -37
  2. package/Dockerfile +14 -8
  3. package/README.md +7 -8
  4. package/docs/architecture/memory.md +28 -29
  5. package/docs/runbook-trusted-contacts.md +76 -43
  6. package/package.json +1 -1
  7. package/scripts/ipc/check-swift-decoder-drift.ts +2 -3
  8. package/scripts/test.sh +1 -1
  9. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +4 -37
  10. package/src/__tests__/actor-token-service.test.ts +4 -3
  11. package/src/__tests__/app-executors.test.ts +7 -17
  12. package/src/__tests__/assistant-feature-flags-integration.test.ts +18 -10
  13. package/src/__tests__/browser-skill-endstate.test.ts +10 -1
  14. package/src/__tests__/bundled-skill-retrieval-guard.test.ts +1 -0
  15. package/src/__tests__/channel-approval-routes.test.ts +44 -44
  16. package/src/__tests__/channel-approval.test.ts +8 -0
  17. package/src/__tests__/channel-approvals.test.ts +39 -1
  18. package/src/__tests__/channel-guardian.test.ts +15 -5
  19. package/src/__tests__/channel-reply-delivery.test.ts +31 -0
  20. package/src/__tests__/config-schema.test.ts +0 -9
  21. package/src/__tests__/conflict-policy.test.ts +76 -0
  22. package/src/__tests__/conflict-store.test.ts +14 -20
  23. package/src/__tests__/contacts-tools.test.ts +8 -61
  24. package/src/__tests__/contradiction-checker.test.ts +5 -1
  25. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +9 -0
  26. package/src/__tests__/gateway-only-guard.test.ts +1 -0
  27. package/src/__tests__/gemini-image-service.test.ts +2 -2
  28. package/src/__tests__/guardian-decision-primitive-canonical.test.ts +5 -3
  29. package/src/__tests__/guardian-grant-minting.test.ts +6 -6
  30. package/src/__tests__/guardian-routing-invariants.test.ts +40 -15
  31. package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +4 -6
  32. package/src/__tests__/inbound-invite-redemption.test.ts +1 -1
  33. package/src/__tests__/integrations-cli.test.ts +3 -27
  34. package/src/__tests__/intent-routing.test.ts +3 -0
  35. package/src/__tests__/invite-redemption-service.test.ts +1 -1
  36. package/src/__tests__/{ingress-routes-http.test.ts → invite-routes-http.test.ts} +40 -320
  37. package/src/__tests__/ipc-snapshot.test.ts +4 -31
  38. package/src/__tests__/memory-lifecycle-e2e.test.ts +11 -10
  39. package/src/__tests__/nl-approval-parser.test.ts +305 -0
  40. package/src/__tests__/oauth-provider-profiles.test.ts +34 -0
  41. package/src/__tests__/provider-error-scenarios.test.ts +68 -0
  42. package/src/__tests__/registry.test.ts +0 -10
  43. package/src/__tests__/relay-server.test.ts +1 -1
  44. package/src/__tests__/retry-after-extraction.test.ts +111 -0
  45. package/src/__tests__/script-proxy-profile-template-fallback.test.ts +127 -0
  46. package/src/__tests__/script-proxy-session-runtime.test.ts +6 -1
  47. package/src/__tests__/session-agent-loop.test.ts +0 -2
  48. package/src/__tests__/session-conflict-gate.test.ts +243 -388
  49. package/src/__tests__/session-media-retry.test.ts +147 -0
  50. package/src/__tests__/session-profile-injection.test.ts +0 -2
  51. package/src/__tests__/session-runtime-assembly.test.ts +2 -3
  52. package/src/__tests__/session-skill-tools.test.ts +0 -49
  53. package/src/__tests__/session-workspace-cache-state.test.ts +0 -1
  54. package/src/__tests__/session-workspace-injection.test.ts +0 -1
  55. package/src/__tests__/session-workspace-tool-tracking.test.ts +0 -1
  56. package/src/__tests__/skill-feature-flags-integration.test.ts +9 -5
  57. package/src/__tests__/skill-feature-flags.test.ts +18 -12
  58. package/src/__tests__/skill-load-feature-flag.test.ts +4 -3
  59. package/src/__tests__/slack-block-formatting.test.ts +100 -0
  60. package/src/__tests__/slack-inbound-verification.test.ts +346 -0
  61. package/src/__tests__/slack-reaction-approvals.test.ts +77 -0
  62. package/src/__tests__/slack-skill.test.ts +3 -2
  63. package/src/__tests__/starter-task-flow.test.ts +0 -1
  64. package/src/__tests__/tool-grant-request-escalation.test.ts +2 -1
  65. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +2 -1
  66. package/src/__tests__/trusted-contact-verification.test.ts +3 -1
  67. package/src/__tests__/voice-invite-redemption.test.ts +1 -1
  68. package/src/amazon/client.ts +7 -24
  69. package/src/approvals/guardian-decision-primitive.ts +11 -7
  70. package/src/approvals/guardian-request-resolvers.ts +5 -3
  71. package/src/calls/relay-server.ts +44 -11
  72. package/src/channels/config.ts +1 -1
  73. package/src/cli/integrations.ts +10 -66
  74. package/src/config/bundled-skills/app-builder/SKILL.md +193 -1500
  75. package/src/config/bundled-skills/app-builder/TOOLS.json +70 -18
  76. package/src/config/bundled-skills/browser/TOOLS.json +59 -2
  77. package/src/config/bundled-skills/chatgpt-import/TOOLS.json +4 -0
  78. package/src/config/bundled-skills/computer-use/TOOLS.json +50 -2
  79. package/src/config/bundled-skills/contacts/SKILL.md +49 -53
  80. package/src/config/bundled-skills/contacts/TOOLS.json +26 -22
  81. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +40 -62
  82. package/src/config/bundled-skills/contacts/tools/contact-search.ts +17 -43
  83. package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +18 -57
  84. package/src/config/bundled-skills/document/TOOLS.json +8 -0
  85. package/src/config/bundled-skills/email-setup/SKILL.md +10 -7
  86. package/src/config/bundled-skills/followups/TOOLS.json +12 -0
  87. package/src/config/bundled-skills/google-calendar/TOOLS.json +124 -26
  88. package/src/config/bundled-skills/guardian-verify-setup/SKILL.md +54 -21
  89. package/src/config/bundled-skills/image-studio/TOOLS.json +12 -2
  90. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +14 -8
  91. package/src/config/bundled-skills/knowledge-graph/TOOLS.json +13 -3
  92. package/src/config/bundled-skills/media-processing/SKILL.md +1 -1
  93. package/src/config/bundled-skills/media-processing/TOOLS.json +28 -0
  94. package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +26 -6
  95. package/src/config/bundled-skills/messaging/TOOLS.json +228 -182
  96. package/src/config/bundled-skills/notifications/SKILL.md +3 -2
  97. package/src/config/bundled-skills/notifications/TOOLS.json +7 -13
  98. package/src/config/bundled-skills/phone-calls/TOOLS.json +13 -1
  99. package/src/config/bundled-skills/playbooks/TOOLS.json +16 -0
  100. package/src/config/bundled-skills/reminder/TOOLS.json +15 -2
  101. package/src/config/bundled-skills/schedule/SKILL.md +33 -15
  102. package/src/config/bundled-skills/schedule/TOOLS.json +17 -1
  103. package/src/config/bundled-skills/slack/SKILL.md +30 -1
  104. package/src/config/bundled-skills/slack/TOOLS.json +89 -2
  105. package/src/config/bundled-skills/slack/tools/slack-channel-permissions.ts +146 -0
  106. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +120 -0
  107. package/src/config/bundled-skills/slack-app-setup/SKILL.md +200 -0
  108. package/src/config/bundled-skills/subagent/TOOLS.json +22 -2
  109. package/src/config/bundled-skills/tasks/TOOLS.json +86 -14
  110. package/src/config/bundled-skills/transcribe/TOOLS.json +4 -0
  111. package/src/config/bundled-skills/watcher/TOOLS.json +20 -0
  112. package/src/config/bundled-tool-registry.ts +2 -5
  113. package/src/config/channel-permission-profiles.ts +155 -0
  114. package/src/config/env.ts +4 -1
  115. package/src/config/memory-schema.ts +0 -10
  116. package/src/config/system-prompt.ts +6 -0
  117. package/src/contacts/contact-store.ts +221 -56
  118. package/src/contacts/contacts-write.ts +14 -3
  119. package/src/contacts/types.ts +35 -4
  120. package/src/daemon/assistant-attachments.ts +23 -3
  121. package/src/daemon/guardian-verification-intent.ts +7 -4
  122. package/src/daemon/handlers/apps.ts +1 -2
  123. package/src/daemon/handlers/config-heartbeat.ts +1 -2
  124. package/src/daemon/handlers/config-inbox.ts +16 -134
  125. package/src/daemon/handlers/contacts.ts +2 -2
  126. package/src/daemon/handlers/guardian-actions.ts +21 -88
  127. package/src/daemon/handlers/sessions.ts +2 -2
  128. package/src/daemon/ipc-contract/apps.ts +0 -1
  129. package/src/daemon/ipc-contract/contacts.ts +2 -2
  130. package/src/daemon/ipc-contract/inbox.ts +7 -66
  131. package/src/daemon/ipc-contract/sessions.ts +1 -0
  132. package/src/daemon/ipc-contract/surfaces.ts +0 -1
  133. package/src/daemon/ipc-contract-inventory.json +2 -4
  134. package/src/daemon/lifecycle.ts +14 -2
  135. package/src/daemon/session-agent-loop-handlers.ts +9 -0
  136. package/src/daemon/session-agent-loop.ts +2 -45
  137. package/src/daemon/session-attachments.ts +5 -1
  138. package/src/daemon/session-conflict-gate.ts +21 -82
  139. package/src/daemon/session-error.ts +18 -0
  140. package/src/daemon/session-lifecycle.ts +4 -5
  141. package/src/daemon/session-media-retry.ts +15 -1
  142. package/src/daemon/session-memory.ts +7 -52
  143. package/src/daemon/session-process.ts +3 -1
  144. package/src/daemon/session-runtime-assembly.ts +18 -35
  145. package/src/daemon/session-surfaces.ts +0 -1
  146. package/src/daemon/session-tool-setup.ts +7 -4
  147. package/src/events/domain-events.ts +2 -1
  148. package/src/heartbeat/heartbeat-service.ts +5 -1
  149. package/src/home-base/prebuilt/seed.ts +0 -1
  150. package/src/influencer/client.ts +7 -24
  151. package/src/media/gemini-image-service.ts +48 -3
  152. package/src/memory/app-store.ts +0 -4
  153. package/src/memory/conflict-intent.ts +3 -6
  154. package/src/memory/conflict-policy.ts +34 -0
  155. package/src/memory/conflict-store.ts +10 -18
  156. package/src/memory/contradiction-checker.ts +2 -2
  157. package/src/memory/conversation-attention-store.ts +3 -1
  158. package/src/memory/db-init.ts +8 -0
  159. package/src/memory/job-handlers/conflict.ts +0 -7
  160. package/src/memory/migrations/133-assistant-contact-metadata.ts +21 -0
  161. package/src/memory/migrations/134-contacts-notes-column.ts +51 -0
  162. package/src/memory/migrations/135-backfill-contact-interaction-stats.ts +31 -0
  163. package/src/memory/migrations/index.ts +3 -0
  164. package/src/memory/schema.ts +12 -17
  165. package/src/memory/slack-thread-store.ts +187 -0
  166. package/src/messaging/index.ts +0 -1
  167. package/src/messaging/providers/slack/client.ts +84 -26
  168. package/src/messaging/providers/slack/types.ts +4 -0
  169. package/src/messaging/types.ts +0 -38
  170. package/src/notifications/adapters/slack.ts +90 -0
  171. package/src/notifications/destination-resolver.ts +42 -1
  172. package/src/notifications/emit-signal.ts +17 -1
  173. package/src/oauth/provider-profiles.ts +22 -0
  174. package/src/providers/anthropic/client.ts +3 -0
  175. package/src/providers/openai/client.ts +3 -0
  176. package/src/providers/retry.ts +9 -1
  177. package/src/runtime/actor-trust-resolver.ts +8 -0
  178. package/src/runtime/auth/require-bound-guardian.ts +44 -0
  179. package/src/runtime/auth/route-policy.ts +4 -8
  180. package/src/runtime/channel-approval-types.ts +18 -0
  181. package/src/runtime/channel-approvals.ts +8 -0
  182. package/src/runtime/channel-invite-transport.ts +1 -1
  183. package/src/runtime/channel-reply-delivery.ts +62 -3
  184. package/src/runtime/gateway-client.ts +36 -2
  185. package/src/runtime/gateway-internal-client.ts +86 -0
  186. package/src/runtime/guardian-action-service.ts +128 -0
  187. package/src/runtime/guardian-outbound-actions.ts +3 -3
  188. package/src/runtime/guardian-reply-router.ts +4 -4
  189. package/src/runtime/guardian-verification-templates.ts +16 -1
  190. package/src/runtime/http-server.ts +29 -46
  191. package/src/runtime/invite-redemption-service.ts +1 -1
  192. package/src/runtime/{ingress-service.ts → invite-service.ts} +5 -157
  193. package/src/runtime/nl-approval-parser.ts +138 -0
  194. package/src/runtime/routes/approval-routes.ts +1 -40
  195. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +6 -3
  196. package/src/runtime/routes/channel-route-shared.ts +35 -1
  197. package/src/runtime/routes/contact-routes.ts +494 -47
  198. package/src/runtime/routes/conversation-routes.ts +2 -1
  199. package/src/runtime/routes/global-search-routes.ts +2 -2
  200. package/src/runtime/routes/guardian-action-routes.ts +19 -111
  201. package/src/runtime/routes/guardian-approval-interception.ts +78 -1
  202. package/src/runtime/routes/guardian-bootstrap-routes.ts +6 -1
  203. package/src/runtime/routes/inbound-message-handler.ts +40 -12
  204. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +227 -1
  205. package/src/runtime/routes/inbound-stages/background-dispatch.ts +108 -0
  206. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +2 -1
  207. package/src/runtime/routes/{ingress-routes.ts → invite-routes.ts} +10 -110
  208. package/src/runtime/routes/migration-routes.ts +17 -17
  209. package/src/runtime/slack-block-formatting.ts +176 -0
  210. package/src/schedule/scheduler.ts +11 -2
  211. package/src/tools/apps/executors.ts +16 -15
  212. package/src/tools/calls/call-end.ts +1 -1
  213. package/src/tools/computer-use/definitions.ts +16 -0
  214. package/src/tools/credentials/vault.ts +86 -2
  215. package/src/tools/network/script-proxy/session-manager.ts +28 -3
  216. package/src/tools/permission-checker.ts +18 -0
  217. package/src/tools/terminal/shell.ts +15 -5
  218. package/src/tools/tool-approval-handler.ts +48 -4
  219. package/src/tools/types.ts +38 -1
  220. package/src/util/errors.ts +5 -1
  221. package/src/util/retry.ts +21 -0
  222. package/src/watcher/providers/slack.ts +33 -3
  223. package/src/workspace/git-service.ts +6 -4
  224. package/src/__tests__/get-weather.test.ts +0 -393
  225. package/src/__tests__/weather-skill-regression.test.ts +0 -276
  226. package/src/autonomy/autonomy-resolver.ts +0 -62
  227. package/src/autonomy/autonomy-store.ts +0 -138
  228. package/src/autonomy/disposition-mapper.ts +0 -31
  229. package/src/autonomy/index.ts +0 -11
  230. package/src/autonomy/types.ts +0 -43
  231. package/src/config/bundled-skills/weather/SKILL.md +0 -38
  232. package/src/config/bundled-skills/weather/TOOLS.json +0 -32
  233. package/src/config/bundled-skills/weather/icon.svg +0 -24
  234. package/src/config/bundled-skills/weather/tools/get-weather.ts +0 -12
  235. package/src/messaging/triage-engine.ts +0 -344
  236. package/src/tools/weather/service.ts +0 -712
  237. /package/src/memory/{ingress-invite-store.ts → invite-store.ts} +0 -0
@@ -33,7 +33,6 @@ import {
33
33
  getPendingConflictByPair,
34
34
  listPendingConflictDetails,
35
35
  listPendingConflicts,
36
- markConflictAsked,
37
36
  resolveConflict,
38
37
  } from "../memory/conflict-store.js";
39
38
  import { getDb, initializeDb, resetDb } from "../memory/db.js";
@@ -60,6 +59,10 @@ function resetTables() {
60
59
  function insertItemPair(
61
60
  suffix: string,
62
61
  scopeId = "default",
62
+ opts?: {
63
+ existingVerificationState?: string;
64
+ candidateVerificationState?: string;
65
+ },
63
66
  ): { existingItemId: string; candidateItemId: string } {
64
67
  const db = getDb();
65
68
  const now = Date.now();
@@ -76,7 +79,8 @@ function insertItemPair(
76
79
  confidence: 0.8,
77
80
  importance: 0.5,
78
81
  fingerprint: `fp-existing-${suffix}`,
79
- verificationState: "assistant_inferred",
82
+ verificationState:
83
+ opts?.existingVerificationState ?? "assistant_inferred",
80
84
  scopeId,
81
85
  firstSeenAt: now,
82
86
  lastSeenAt: now,
@@ -90,7 +94,8 @@ function insertItemPair(
90
94
  confidence: 0.8,
91
95
  importance: 0.5,
92
96
  fingerprint: `fp-candidate-${suffix}`,
93
- verificationState: "assistant_inferred",
97
+ verificationState:
98
+ opts?.candidateVerificationState ?? "assistant_inferred",
94
99
  scopeId,
95
100
  firstSeenAt: now,
96
101
  lastSeenAt: now,
@@ -218,24 +223,11 @@ describe("conflict-store", () => {
218
223
  expect(pendingDefault[0].status).toBe("pending_clarification");
219
224
  });
220
225
 
221
- test("markConflictAsked updates lastAskedAt", () => {
222
- const pair = insertItemPair("asked");
223
- const conflict = createOrUpdatePendingConflict({
224
- scopeId: "default",
225
- existingItemId: pair.existingItemId,
226
- candidateItemId: pair.candidateItemId,
227
- relationship: "ambiguous_contradiction",
226
+ test("listPendingConflictDetails joins current statements and verification states", () => {
227
+ const pair = insertItemPair("details", "workspace-a", {
228
+ existingVerificationState: "user_confirmed",
229
+ candidateVerificationState: "assistant_inferred",
228
230
  });
229
-
230
- const askedAt = 1_734_000_000_000;
231
- expect(markConflictAsked(conflict.id, askedAt)).toBe(true);
232
- const updated = getConflictById(conflict.id);
233
- expect(updated?.lastAskedAt).toBe(askedAt);
234
- expect(updated?.updatedAt).toBe(askedAt);
235
- });
236
-
237
- test("listPendingConflictDetails joins current statements", () => {
238
- const pair = insertItemPair("details", "workspace-a");
239
231
  createOrUpdatePendingConflict({
240
232
  scopeId: "workspace-a",
241
233
  existingItemId: pair.existingItemId,
@@ -250,6 +242,8 @@ describe("conflict-store", () => {
250
242
  expect(details[0].candidateStatement).toBe("Candidate statement details");
251
243
  expect(details[0].existingKind).toBe("fact");
252
244
  expect(details[0].candidateKind).toBe("fact");
245
+ expect(details[0].existingVerificationState).toBe("user_confirmed");
246
+ expect(details[0].candidateVerificationState).toBe("assistant_inferred");
253
247
  });
254
248
 
255
249
  test("applyConflictResolution keeps candidate and resolves conflict row", () => {
@@ -140,17 +140,14 @@ describe("contact_upsert tool", () => {
140
140
  expect(result.isError).toBe(false);
141
141
  expect(result.content).toContain("Created contact");
142
142
  expect(result.content).toContain("Alice");
143
- expect(result.content).toContain("Importance: 0.50");
144
143
  });
145
144
 
146
145
  test("creates a contact with all fields", async () => {
147
146
  const result = await executeContactUpsert(
148
147
  {
149
148
  display_name: "Bob",
150
- relationship: "colleague",
151
- importance: 0.8,
152
- response_expectation: "within_hours",
153
- preferred_tone: "professional",
149
+ notes:
150
+ "Colleague at Acme Corp, prefers professional tone, responds within hours",
154
151
  channels: [
155
152
  { type: "email", address: "bob@example.com", is_primary: true },
156
153
  { type: "slack", address: "@bob" },
@@ -161,10 +158,7 @@ describe("contact_upsert tool", () => {
161
158
 
162
159
  expect(result.isError).toBe(false);
163
160
  expect(result.content).toContain("Bob");
164
- expect(result.content).toContain("colleague");
165
- expect(result.content).toContain("0.80");
166
- expect(result.content).toContain("within_hours");
167
- expect(result.content).toContain("professional");
161
+ expect(result.content).toContain("Notes: Colleague at Acme Corp");
168
162
  expect(result.content).toContain("email: bob@example.com");
169
163
  expect(result.content).toContain("slack: @bob");
170
164
  });
@@ -185,7 +179,7 @@ describe("contact_upsert tool", () => {
185
179
  {
186
180
  id: contactId,
187
181
  display_name: "Charlie Updated",
188
- importance: 0.9,
182
+ notes: "Updated notes for Charlie",
189
183
  },
190
184
  ctx,
191
185
  );
@@ -193,7 +187,7 @@ describe("contact_upsert tool", () => {
193
187
  expect(updateResult.isError).toBe(false);
194
188
  expect(updateResult.content).toContain("Updated contact");
195
189
  expect(updateResult.content).toContain("Charlie Updated");
196
- expect(updateResult.content).toContain("0.90");
190
+ expect(updateResult.content).toContain("Notes: Updated notes for Charlie");
197
191
  });
198
192
 
199
193
  test("auto-matches by channel address on create", async () => {
@@ -239,36 +233,6 @@ describe("contact_upsert tool", () => {
239
233
  expect(result.isError).toBe(true);
240
234
  expect(result.content).toContain("display_name is required");
241
235
  });
242
-
243
- test("rejects importance out of range", async () => {
244
- const result = await executeContactUpsert(
245
- {
246
- display_name: "Test",
247
- importance: 1.5,
248
- },
249
- ctx,
250
- );
251
-
252
- expect(result.isError).toBe(true);
253
- expect(result.content).toContain(
254
- "importance must be a number between 0 and 1",
255
- );
256
- });
257
-
258
- test("rejects negative importance", async () => {
259
- const result = await executeContactUpsert(
260
- {
261
- display_name: "Test",
262
- importance: -0.1,
263
- },
264
- ctx,
265
- );
266
-
267
- expect(result.isError).toBe(true);
268
- expect(result.content).toContain(
269
- "importance must be a number between 0 and 1",
270
- );
271
- });
272
236
  });
273
237
 
274
238
  // ── contact_search ──────────────────────────────────────────────────
@@ -305,23 +269,6 @@ describe("contact_search tool", () => {
305
269
  expect(result.content).toContain("Charlie");
306
270
  });
307
271
 
308
- test("searches by relationship", async () => {
309
- await executeContactUpsert(
310
- { display_name: "Diana", relationship: "friend" },
311
- ctx,
312
- );
313
- await executeContactUpsert(
314
- { display_name: "Eve", relationship: "colleague" },
315
- ctx,
316
- );
317
-
318
- const result = await executeContactSearch({ relationship: "friend" }, ctx);
319
-
320
- expect(result.isError).toBe(false);
321
- expect(result.content).toContain("Diana");
322
- expect(result.content).not.toContain("Eve");
323
- });
324
-
325
272
  test("returns no results message when nothing matches", async () => {
326
273
  await executeContactUpsert({ display_name: "Existing" }, ctx);
327
274
 
@@ -380,7 +327,7 @@ describe("contact_merge tool", () => {
380
327
  const r1 = await executeContactUpsert(
381
328
  {
382
329
  display_name: "Alice (Email)",
383
- importance: 0.7,
330
+ notes: "Prefers email",
384
331
  channels: [{ type: "email", address: "alice@example.com" }],
385
332
  },
386
333
  ctx,
@@ -388,7 +335,7 @@ describe("contact_merge tool", () => {
388
335
  const r2 = await executeContactUpsert(
389
336
  {
390
337
  display_name: "Alice (Slack)",
391
- importance: 0.9,
338
+ notes: "Active on Slack",
392
339
  channels: [{ type: "slack", address: "@alice" }],
393
340
  },
394
341
  ctx,
@@ -407,7 +354,7 @@ describe("contact_merge tool", () => {
407
354
 
408
355
  expect(result.isError).toBe(false);
409
356
  expect(result.content).toContain("Merged");
410
- expect(result.content).toContain("Importance: 0.90"); // takes higher importance
357
+ expect(result.content).toContain("Notes: Prefers email\nActive on Slack"); // concatenated notes
411
358
  expect(result.content).toContain("email: alice@example.com");
412
359
  expect(result.content).toContain("slack: @alice");
413
360
 
@@ -201,7 +201,11 @@ describe("checkContradictions", () => {
201
201
  expect(conflicts[0].existingItemId).toBe("item-existing-ambiguous");
202
202
  expect(conflicts[0].candidateItemId).toBe("item-candidate-ambiguous");
203
203
  expect(conflicts[0].relationship).toBe("ambiguous_contradiction");
204
- expect(conflicts[0].clarificationQuestion).toContain(
204
+ expect(conflicts[0].clarificationQuestion).toContain("Pending conflict:");
205
+ expect(conflicts[0].clarificationQuestion).not.toContain(
206
+ "I have conflicting notes",
207
+ );
208
+ expect(conflicts[0].clarificationQuestion).not.toContain(
205
209
  "Which one is correct?",
206
210
  );
207
211
  });
@@ -46,6 +46,15 @@ mock.module("../util/logger.js", () => ({
46
46
  truncateForLog: (v: string) => v,
47
47
  }));
48
48
 
49
+ mock.module("../config/loader.js", () => ({
50
+ getConfig: () => ({
51
+ sandbox: { enabled: false, backend: "native" },
52
+ assistantFeatureFlagValues: {
53
+ "feature_flags.browser.enabled": true,
54
+ },
55
+ }),
56
+ }));
57
+
49
58
  const { buildSystemPrompt } = await import("../config/system-prompt.js");
50
59
 
51
60
  describe("Dynamic Skill Authoring Workflow prompt section", () => {
@@ -32,6 +32,7 @@ const ALLOWLIST = new Set([
32
32
 
33
33
  // --- Documentation and comments that mention the port for explanatory purposes ---
34
34
  "AGENTS.md", // documents the gateway-only rule itself
35
+ "assistant/docs/runbook-trusted-contacts.md", // operator runbook targeting runtime-only /v1/contacts endpoints
35
36
  "assistant/src/runtime/middleware/twilio-validation.ts", // comment explaining proxy URL rewriting
36
37
  ]);
37
38
 
@@ -137,8 +137,8 @@ describe("generateImage", () => {
137
137
  .contents as Array<Record<string, unknown>>;
138
138
  const parts = contents[0].parts as Array<Record<string, unknown>>;
139
139
 
140
- // First part is the text prompt
141
- expect(parts[0]).toEqual({ text: "remove background" });
140
+ // First part is the text prompt (with appended title instruction)
141
+ expect((parts[0] as { text: string }).text).toContain("remove background");
142
142
  // Second part is the source image
143
143
  expect(parts[1]).toEqual({
144
144
  inlineData: { mimeType: "image/jpeg", data: "srcdata" },
@@ -71,7 +71,8 @@ const TEST_PRINCIPAL_ID = "test-principal-id";
71
71
 
72
72
  function guardianActor(overrides: Partial<ActorContext> = {}): ActorContext {
73
73
  return {
74
- externalUserId: "guardian-1",
74
+ actorPrincipalId: TEST_PRINCIPAL_ID,
75
+ actorExternalUserId: "guardian-1",
75
76
  channel: "telegram",
76
77
  guardianPrincipalId: TEST_PRINCIPAL_ID,
77
78
  ...overrides,
@@ -80,7 +81,8 @@ function guardianActor(overrides: Partial<ActorContext> = {}): ActorContext {
80
81
 
81
82
  function trustedActor(overrides: Partial<ActorContext> = {}): ActorContext {
82
83
  return {
83
- externalUserId: undefined,
84
+ actorPrincipalId: TEST_PRINCIPAL_ID,
85
+ actorExternalUserId: undefined,
84
86
  channel: "desktop",
85
87
  guardianPrincipalId: TEST_PRINCIPAL_ID,
86
88
  ...overrides,
@@ -254,7 +256,7 @@ describe("applyCanonicalGuardianDecision", () => {
254
256
 
255
257
  expect(result.applied).toBe(true);
256
258
  if (!result.applied) return;
257
- // No grant minted because trusted actor has no externalUserId
259
+ // No grant minted because trusted actor has no actorExternalUserId
258
260
  expect(result.grantMinted).toBe(false);
259
261
  });
260
262
 
@@ -189,9 +189,9 @@ describe("guardian grant minting on tool-approval decisions", () => {
189
189
 
190
190
  beforeEach(() => {
191
191
  resetTables();
192
- deliverSpy = spyOn(gatewayClient, "deliverChannelReply").mockResolvedValue(
193
- undefined,
194
- );
192
+ deliverSpy = spyOn(gatewayClient, "deliverChannelReply").mockResolvedValue({
193
+ ok: true,
194
+ });
195
195
  composeSpy = spyOn(
196
196
  approvalMessageComposer,
197
197
  "composeApprovalMessageGenerative",
@@ -593,9 +593,9 @@ describe("approval interception trust-class regression coverage", () => {
593
593
 
594
594
  beforeEach(() => {
595
595
  resetTables();
596
- deliverSpy = spyOn(gatewayClient, "deliverChannelReply").mockResolvedValue(
597
- undefined,
598
- );
596
+ deliverSpy = spyOn(gatewayClient, "deliverChannelReply").mockResolvedValue({
597
+ ok: true,
598
+ });
599
599
  composeSpy = spyOn(
600
600
  approvalMessageComposer,
601
601
  "composeApprovalMessageGenerative",
@@ -94,7 +94,8 @@ const TEST_PRINCIPAL_ID = "test-principal-id";
94
94
 
95
95
  function guardianActor(overrides: Partial<ActorContext> = {}): ActorContext {
96
96
  return {
97
- externalUserId: "guardian-1",
97
+ actorPrincipalId: TEST_PRINCIPAL_ID,
98
+ actorExternalUserId: "guardian-1",
98
99
  channel: "telegram",
99
100
  guardianPrincipalId: TEST_PRINCIPAL_ID,
100
101
  ...overrides,
@@ -103,7 +104,8 @@ function guardianActor(overrides: Partial<ActorContext> = {}): ActorContext {
103
104
 
104
105
  function trustedActor(overrides: Partial<ActorContext> = {}): ActorContext {
105
106
  return {
106
- externalUserId: undefined,
107
+ actorPrincipalId: TEST_PRINCIPAL_ID,
108
+ actorExternalUserId: undefined,
107
109
  channel: "desktop",
108
110
  guardianPrincipalId: TEST_PRINCIPAL_ID,
109
111
  ...overrides,
@@ -169,22 +171,45 @@ function registerPendingToolApprovalInteraction(
169
171
  describe("routing invariant: all decision paths reference applyCanonicalGuardianDecision", () => {
170
172
  const srcRoot = resolve(__dirname, "..");
171
173
 
172
- // The files that constitute decision entrypoints. Each must import
173
- // `applyCanonicalGuardianDecision` from the guardian-decision-primitive.
174
- const DECISION_ENTRYPOINTS = [
174
+ // The files that constitute decision entrypoints. Each must reference
175
+ // `applyCanonicalGuardianDecision` (directly) or `processGuardianDecision`
176
+ // (shared wrapper that calls applyCanonicalGuardianDecision internally).
177
+ const DECISION_ENTRYPOINTS: Array<{
178
+ path: string;
179
+ symbols: string[];
180
+ }> = [
175
181
  // Inbound channel router (Telegram/SMS/WhatsApp)
176
- "runtime/guardian-reply-router.ts",
177
- // HTTP API route handler (desktop and API clients)
178
- "runtime/routes/guardian-action-routes.ts",
179
- // IPC handler (desktop socket clients)
180
- "daemon/handlers/guardian-actions.ts",
182
+ {
183
+ path: "runtime/guardian-reply-router.ts",
184
+ symbols: ["applyCanonicalGuardianDecision"],
185
+ },
186
+ // HTTP API route handler (desktop and API clients) — uses processGuardianDecision
187
+ // which is a shared wrapper around applyCanonicalGuardianDecision
188
+ {
189
+ path: "runtime/routes/guardian-action-routes.ts",
190
+ symbols: ["processGuardianDecision"],
191
+ },
192
+ // IPC handler (desktop socket clients) — uses processGuardianDecision
193
+ // which is a shared wrapper around applyCanonicalGuardianDecision
194
+ {
195
+ path: "daemon/handlers/guardian-actions.ts",
196
+ symbols: ["processGuardianDecision"],
197
+ },
198
+ // Shared service where processGuardianDecision is defined — must route
199
+ // through the canonical primitive to complete the chain:
200
+ // entrypoint → processGuardianDecision → applyCanonicalGuardianDecision
201
+ {
202
+ path: "runtime/guardian-action-service.ts",
203
+ symbols: ["applyCanonicalGuardianDecision"],
204
+ },
181
205
  ];
182
206
 
183
- for (const relPath of DECISION_ENTRYPOINTS) {
184
- test(`${relPath} imports applyCanonicalGuardianDecision`, () => {
207
+ for (const { path: relPath, symbols } of DECISION_ENTRYPOINTS) {
208
+ test(`${relPath} imports ${symbols.join(" or ")}`, () => {
185
209
  const fullPath = join(srcRoot, relPath);
186
210
  const source = readFileSync(fullPath, "utf-8");
187
- expect(source).toContain("applyCanonicalGuardianDecision");
211
+ const found = symbols.some((s) => source.includes(s));
212
+ expect(found).toBe(true);
188
213
  });
189
214
  }
190
215
 
@@ -1189,13 +1214,13 @@ describe("routing invariant: destination hints do not bypass tool_approval princ
1189
1214
  });
1190
1215
 
1191
1216
  // No pendingRequestIds passed — identity-based fallback uses
1192
- // actor.externalUserId which does not match any request's
1217
+ // actor.actorExternalUserId which does not match any request's
1193
1218
  // guardianExternalUserId (since it's null).
1194
1219
  const result = await routeGuardianReply(
1195
1220
  replyCtx({
1196
1221
  messageText: "approve",
1197
1222
  channel: "telegram",
1198
- actor: guardianActor({ externalUserId: "guardian-tg-user" }),
1223
+ actor: guardianActor({ actorExternalUserId: "guardian-tg-user" }),
1199
1224
  conversationId: "conv-guardian-chat",
1200
1225
  // pendingRequestIds: undefined — no delivery hints
1201
1226
  approvalConversationGenerator: undefined,
@@ -107,14 +107,12 @@ describe("guardian-verify-setup skill — voice auto-followup", () => {
107
107
  ?.split("## Step 6")[0] ?? "";
108
108
  // Must mention rebind guard concept
109
109
  expect(pollingSection).toContain("Rebind guard");
110
- // Must instruct not to trust the first bound: true in a rebind flow
110
+ // Must instruct not to trust bound: true alone in a rebind flow
111
111
  expect(pollingSection).toContain(
112
- "do NOT treat the first `bound: true` poll result as success",
112
+ "do NOT treat `bound: true` alone as success",
113
113
  );
114
- // Must reference bound_at timestamp comparison as the primary mechanism
115
- expect(pollingSection).toContain("bound_at");
116
- // Must have a fallback for when bound_at is unavailable
117
- expect(pollingSection).toContain("second poll onward");
114
+ // Must reference verificationSessionId as the mechanism to detect fresh binding
115
+ expect(pollingSection).toContain("verificationSessionId");
118
116
  // Must clarify non-rebind flows are unaffected
119
117
  expect(pollingSection).toContain("Non-rebind flows");
120
118
  });
@@ -90,7 +90,7 @@ mock.module("../runtime/approval-message-composer.js", () => ({
90
90
  import { findContactChannel } from "../contacts/contact-store.js";
91
91
  import { upsertMember } from "../contacts/contacts-write.js";
92
92
  import { getDb, initializeDb, resetDb } from "../memory/db.js";
93
- import { createInvite, revokeInvite } from "../memory/ingress-invite-store.js";
93
+ import { createInvite, revokeInvite } from "../memory/invite-store.js";
94
94
  import { handleChannelInbound } from "../runtime/routes/channel-routes.js";
95
95
 
96
96
  initializeDb();
@@ -108,7 +108,6 @@ describe("vellum integrations CLI", () => {
108
108
 
109
109
  afterEach(() => {
110
110
  delete process.env.GATEWAY_AUTH_TOKEN;
111
- delete process.env.INTERNAL_GATEWAY_BASE_URL;
112
111
  process.exitCode = 0;
113
112
  });
114
113
 
@@ -141,8 +140,8 @@ describe("vellum integrations CLI", () => {
141
140
  expect(mintCalls).toBe(1);
142
141
  });
143
142
 
144
- test("prefers INTERNAL_GATEWAY_BASE_URL when it is injected", async () => {
145
- process.env.INTERNAL_GATEWAY_BASE_URL = "http://gateway.internal:9900/";
143
+ test("uses configured gateway base for requests", async () => {
144
+ gatewayBase = "http://gateway.internal:9900";
146
145
  const result = await runCli(["--json", "twilio", "config"], {
147
146
  success: true,
148
147
  });
@@ -163,28 +162,6 @@ describe("vellum integrations CLI", () => {
163
162
  );
164
163
  });
165
164
 
166
- test("passes filters for ingress members (now calls /v1/contacts)", async () => {
167
- const result = await runCli(
168
- ["--json", "ingress", "members", "--role", "contact", "--limit", "20"],
169
- { ok: true, contacts: [] },
170
- );
171
- expect(result.exitCode).toBe(0);
172
- expect(result.fetchCalls[0]?.url).toBe(
173
- "http://gateway.test/v1/contacts?role=contact&limit=20",
174
- );
175
- });
176
-
177
- test("ingress members defaults role to contact", async () => {
178
- const result = await runCli(["--json", "ingress", "members"], {
179
- ok: true,
180
- contacts: [],
181
- });
182
- expect(result.exitCode).toBe(0);
183
- expect(result.fetchCalls[0]?.url).toBe(
184
- "http://gateway.test/v1/contacts?role=contact",
185
- );
186
- });
187
-
188
165
  test("reads ingress config without gateway fetch", async () => {
189
166
  rawConfig = {
190
167
  ingress: {
@@ -192,7 +169,6 @@ describe("vellum integrations CLI", () => {
192
169
  publicBaseUrl: "https://public.example.com",
193
170
  },
194
171
  };
195
- process.env.INTERNAL_GATEWAY_BASE_URL = "http://gateway.internal:9900/";
196
172
  const result = await runCli(["--json", "ingress", "config"], { ok: true });
197
173
 
198
174
  expect(result.exitCode).toBe(0);
@@ -201,7 +177,7 @@ describe("vellum integrations CLI", () => {
201
177
  success: true,
202
178
  enabled: true,
203
179
  publicBaseUrl: "https://public.example.com",
204
- localGatewayTarget: "http://gateway.internal:9900",
180
+ localGatewayTarget: "http://gateway.test",
205
181
  });
206
182
  });
207
183
 
@@ -50,6 +50,9 @@ mock.module("../config/loader.js", () => ({
50
50
  ui: {},
51
51
 
52
52
  sandbox: { enabled: false, backend: "local" },
53
+ assistantFeatureFlagValues: {
54
+ "feature_flags.guardian-verify-setup.enabled": true,
55
+ },
53
56
  }),
54
57
  }));
55
58
 
@@ -29,7 +29,7 @@ import { getSqlite, initializeDb, resetDb } from "../memory/db.js";
29
29
  import {
30
30
  createInvite,
31
31
  revokeInvite as revokeStoreFn,
32
- } from "../memory/ingress-invite-store.js";
32
+ } from "../memory/invite-store.js";
33
33
  import {
34
34
  type InviteRedemptionOutcome,
35
35
  redeemInvite,