@vellumai/assistant 0.4.29 → 0.4.30

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 (174) hide show
  1. package/ARCHITECTURE.md +39 -37
  2. package/README.md +5 -6
  3. package/docs/runbook-trusted-contacts.md +79 -43
  4. package/package.json +1 -1
  5. package/scripts/ipc/check-swift-decoder-drift.ts +2 -3
  6. package/scripts/test.sh +1 -1
  7. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +4 -37
  8. package/src/__tests__/actor-token-service.test.ts +4 -3
  9. package/src/__tests__/app-executors.test.ts +7 -17
  10. package/src/__tests__/assistant-feature-flags-integration.test.ts +18 -10
  11. package/src/__tests__/browser-skill-endstate.test.ts +10 -1
  12. package/src/__tests__/bundled-skill-retrieval-guard.test.ts +1 -0
  13. package/src/__tests__/channel-approval-routes.test.ts +44 -44
  14. package/src/__tests__/channel-approval.test.ts +8 -0
  15. package/src/__tests__/channel-approvals.test.ts +39 -1
  16. package/src/__tests__/channel-guardian.test.ts +15 -5
  17. package/src/__tests__/channel-reply-delivery.test.ts +31 -0
  18. package/src/__tests__/commit-message-enrichment-service.test.ts +4 -0
  19. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +9 -0
  20. package/src/__tests__/gateway-only-guard.test.ts +1 -0
  21. package/src/__tests__/gemini-image-service.test.ts +2 -2
  22. package/src/__tests__/guardian-grant-minting.test.ts +6 -6
  23. package/src/__tests__/guardian-routing-invariants.test.ts +34 -11
  24. package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +4 -6
  25. package/src/__tests__/inbound-invite-redemption.test.ts +1 -1
  26. package/src/__tests__/integrations-cli.test.ts +3 -27
  27. package/src/__tests__/intent-routing.test.ts +3 -0
  28. package/src/__tests__/invite-redemption-service.test.ts +1 -1
  29. package/src/__tests__/{ingress-routes-http.test.ts → invite-routes-http.test.ts} +40 -320
  30. package/src/__tests__/ipc-snapshot.test.ts +4 -31
  31. package/src/__tests__/nl-approval-parser.test.ts +305 -0
  32. package/src/__tests__/oauth-provider-profiles.test.ts +34 -0
  33. package/src/__tests__/provider-error-scenarios.test.ts +68 -0
  34. package/src/__tests__/relay-server.test.ts +1 -1
  35. package/src/__tests__/retry-after-extraction.test.ts +111 -0
  36. package/src/__tests__/script-proxy-profile-template-fallback.test.ts +127 -0
  37. package/src/__tests__/session-media-retry.test.ts +147 -0
  38. package/src/__tests__/skill-feature-flags-integration.test.ts +9 -5
  39. package/src/__tests__/skill-feature-flags.test.ts +18 -12
  40. package/src/__tests__/skill-load-feature-flag.test.ts +4 -3
  41. package/src/__tests__/slack-block-formatting.test.ts +100 -0
  42. package/src/__tests__/slack-inbound-verification.test.ts +346 -0
  43. package/src/__tests__/slack-reaction-approvals.test.ts +77 -0
  44. package/src/__tests__/slack-skill.test.ts +3 -2
  45. package/src/__tests__/starter-task-flow.test.ts +0 -1
  46. package/src/__tests__/trusted-contact-verification.test.ts +3 -1
  47. package/src/__tests__/voice-invite-redemption.test.ts +1 -1
  48. package/src/amazon/client.ts +7 -24
  49. package/src/calls/relay-server.ts +39 -11
  50. package/src/channels/config.ts +1 -1
  51. package/src/cli/integrations.ts +10 -66
  52. package/src/config/bundled-skills/app-builder/SKILL.md +193 -1500
  53. package/src/config/bundled-skills/app-builder/TOOLS.json +70 -18
  54. package/src/config/bundled-skills/browser/TOOLS.json +59 -2
  55. package/src/config/bundled-skills/chatgpt-import/TOOLS.json +4 -0
  56. package/src/config/bundled-skills/computer-use/TOOLS.json +50 -2
  57. package/src/config/bundled-skills/contacts/SKILL.md +42 -35
  58. package/src/config/bundled-skills/contacts/TOOLS.json +22 -2
  59. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +38 -58
  60. package/src/config/bundled-skills/contacts/tools/contact-search.ts +11 -31
  61. package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +19 -37
  62. package/src/config/bundled-skills/document/TOOLS.json +8 -0
  63. package/src/config/bundled-skills/email-setup/SKILL.md +10 -7
  64. package/src/config/bundled-skills/followups/TOOLS.json +12 -0
  65. package/src/config/bundled-skills/google-calendar/TOOLS.json +124 -26
  66. package/src/config/bundled-skills/guardian-verify-setup/SKILL.md +54 -21
  67. package/src/config/bundled-skills/image-studio/TOOLS.json +12 -2
  68. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +14 -8
  69. package/src/config/bundled-skills/knowledge-graph/TOOLS.json +13 -3
  70. package/src/config/bundled-skills/media-processing/SKILL.md +1 -1
  71. package/src/config/bundled-skills/media-processing/TOOLS.json +28 -0
  72. package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +26 -6
  73. package/src/config/bundled-skills/messaging/TOOLS.json +228 -182
  74. package/src/config/bundled-skills/notifications/SKILL.md +3 -2
  75. package/src/config/bundled-skills/notifications/TOOLS.json +7 -13
  76. package/src/config/bundled-skills/phone-calls/TOOLS.json +13 -1
  77. package/src/config/bundled-skills/playbooks/TOOLS.json +16 -0
  78. package/src/config/bundled-skills/reminder/TOOLS.json +15 -2
  79. package/src/config/bundled-skills/schedule/SKILL.md +33 -15
  80. package/src/config/bundled-skills/schedule/TOOLS.json +17 -1
  81. package/src/config/bundled-skills/slack/SKILL.md +30 -1
  82. package/src/config/bundled-skills/slack/TOOLS.json +89 -2
  83. package/src/config/bundled-skills/slack/tools/slack-channel-permissions.ts +146 -0
  84. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +120 -0
  85. package/src/config/bundled-skills/slack-app-setup/SKILL.md +200 -0
  86. package/src/config/bundled-skills/subagent/TOOLS.json +22 -2
  87. package/src/config/bundled-skills/tasks/TOOLS.json +86 -14
  88. package/src/config/bundled-skills/transcribe/TOOLS.json +4 -0
  89. package/src/config/bundled-skills/watcher/TOOLS.json +20 -0
  90. package/src/config/bundled-skills/weather/TOOLS.json +4 -0
  91. package/src/config/bundled-tool-registry.ts +2 -0
  92. package/src/config/channel-permission-profiles.ts +155 -0
  93. package/src/config/env.ts +4 -1
  94. package/src/contacts/contact-store.ts +195 -4
  95. package/src/contacts/types.ts +26 -0
  96. package/src/daemon/assistant-attachments.ts +23 -3
  97. package/src/daemon/guardian-verification-intent.ts +7 -4
  98. package/src/daemon/handlers/apps.ts +1 -2
  99. package/src/daemon/handlers/config-inbox.ts +16 -134
  100. package/src/daemon/handlers/guardian-actions.ts +20 -87
  101. package/src/daemon/handlers/sessions.ts +0 -1
  102. package/src/daemon/ipc-contract/apps.ts +0 -1
  103. package/src/daemon/ipc-contract/inbox.ts +7 -66
  104. package/src/daemon/ipc-contract/sessions.ts +1 -0
  105. package/src/daemon/ipc-contract/surfaces.ts +0 -1
  106. package/src/daemon/ipc-contract-inventory.json +2 -4
  107. package/src/daemon/lifecycle.ts +14 -2
  108. package/src/daemon/session-agent-loop-handlers.ts +9 -0
  109. package/src/daemon/session-agent-loop.ts +1 -0
  110. package/src/daemon/session-attachments.ts +5 -1
  111. package/src/daemon/session-error.ts +18 -0
  112. package/src/daemon/session-lifecycle.ts +4 -5
  113. package/src/daemon/session-media-retry.ts +15 -1
  114. package/src/daemon/session-surfaces.ts +0 -1
  115. package/src/daemon/session-tool-setup.ts +7 -4
  116. package/src/events/domain-events.ts +2 -1
  117. package/src/home-base/prebuilt/seed.ts +0 -1
  118. package/src/influencer/client.ts +7 -24
  119. package/src/media/gemini-image-service.ts +48 -3
  120. package/src/memory/app-store.ts +0 -4
  121. package/src/memory/conversation-attention-store.ts +3 -1
  122. package/src/memory/db-init.ts +4 -0
  123. package/src/memory/migrations/133-assistant-contact-metadata.ts +21 -0
  124. package/src/memory/migrations/index.ts +1 -0
  125. package/src/memory/schema.ts +12 -0
  126. package/src/memory/slack-thread-store.ts +187 -0
  127. package/src/messaging/providers/slack/client.ts +84 -26
  128. package/src/messaging/providers/slack/types.ts +4 -0
  129. package/src/notifications/adapters/slack.ts +90 -0
  130. package/src/notifications/destination-resolver.ts +42 -1
  131. package/src/notifications/emit-signal.ts +17 -1
  132. package/src/oauth/provider-profiles.ts +22 -0
  133. package/src/providers/anthropic/client.ts +3 -0
  134. package/src/providers/openai/client.ts +3 -0
  135. package/src/providers/retry.ts +9 -1
  136. package/src/runtime/actor-trust-resolver.ts +8 -0
  137. package/src/runtime/auth/require-bound-guardian.ts +44 -0
  138. package/src/runtime/auth/route-policy.ts +4 -8
  139. package/src/runtime/channel-approval-types.ts +18 -0
  140. package/src/runtime/channel-approvals.ts +8 -0
  141. package/src/runtime/channel-invite-transport.ts +1 -1
  142. package/src/runtime/channel-reply-delivery.ts +62 -3
  143. package/src/runtime/gateway-client.ts +36 -2
  144. package/src/runtime/gateway-internal-client.ts +86 -0
  145. package/src/runtime/guardian-action-service.ts +127 -0
  146. package/src/runtime/guardian-verification-templates.ts +16 -1
  147. package/src/runtime/http-server.ts +20 -49
  148. package/src/runtime/invite-redemption-service.ts +1 -1
  149. package/src/runtime/{ingress-service.ts → invite-service.ts} +5 -157
  150. package/src/runtime/nl-approval-parser.ts +138 -0
  151. package/src/runtime/routes/approval-routes.ts +1 -40
  152. package/src/runtime/routes/channel-route-shared.ts +35 -1
  153. package/src/runtime/routes/contact-routes.ts +196 -28
  154. package/src/runtime/routes/guardian-action-routes.ts +19 -111
  155. package/src/runtime/routes/guardian-approval-interception.ts +76 -0
  156. package/src/runtime/routes/inbound-message-handler.ts +40 -12
  157. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +222 -0
  158. package/src/runtime/routes/inbound-stages/background-dispatch.ts +108 -0
  159. package/src/runtime/routes/{ingress-routes.ts → invite-routes.ts} +10 -110
  160. package/src/runtime/slack-block-formatting.ts +176 -0
  161. package/src/schedule/scheduler.ts +11 -2
  162. package/src/tools/apps/executors.ts +16 -15
  163. package/src/tools/calls/call-end.ts +1 -1
  164. package/src/tools/computer-use/definitions.ts +16 -0
  165. package/src/tools/credentials/vault.ts +86 -2
  166. package/src/tools/network/script-proxy/session-manager.ts +28 -3
  167. package/src/tools/permission-checker.ts +18 -0
  168. package/src/tools/terminal/shell.ts +15 -5
  169. package/src/tools/tool-approval-handler.ts +48 -4
  170. package/src/tools/types.ts +38 -1
  171. package/src/util/errors.ts +5 -1
  172. package/src/util/retry.ts +21 -0
  173. package/src/watcher/providers/slack.ts +33 -3
  174. /package/src/memory/{ingress-invite-store.ts → invite-store.ts} +0 -0
@@ -146,16 +146,20 @@ describe("buildSystemPrompt assistant feature flag filtering", () => {
146
146
 
147
147
  currentConfig = {
148
148
  sandbox: { enabled: false, backend: "native" },
149
- assistantFeatureFlagValues: { [DECLARED_FLAG_KEY]: false },
149
+ assistantFeatureFlagValues: {
150
+ [DECLARED_FLAG_KEY]: false,
151
+ "feature_flags.twitter.enabled": true,
152
+ },
150
153
  };
151
154
 
152
155
  const result = buildSystemPrompt();
153
156
 
157
+ // twitter is explicitly enabled, declared flagged skill is explicitly off
154
158
  expect(result).toContain('id="twitter"');
155
159
  expect(result).not.toContain(`id="${DECLARED_SKILL_ID}"`);
156
160
  });
157
161
 
158
- test("all skills visible when no flag overrides set", () => {
162
+ test("declared skills hidden when no flag overrides set (registry defaults to false)", () => {
159
163
  createSkillOnDisk(
160
164
  DECLARED_SKILL_ID,
161
165
  "Hatch New Assistant",
@@ -169,8 +173,9 @@ describe("buildSystemPrompt assistant feature flag filtering", () => {
169
173
 
170
174
  const result = buildSystemPrompt();
171
175
 
172
- expect(result).toContain(`id="${DECLARED_SKILL_ID}"`);
173
- expect(result).toContain('id="twitter"');
176
+ // Both skills are declared in the registry with defaultEnabled: false
177
+ expect(result).not.toContain(`id="${DECLARED_SKILL_ID}"`);
178
+ expect(result).not.toContain('id="twitter"');
174
179
  });
175
180
 
176
181
  test("flagged-off skills hidden when all flags are OFF", () => {
@@ -227,7 +232,7 @@ describe("buildSystemPrompt assistant feature flag filtering", () => {
227
232
  expect(result).not.toContain('id="browser"');
228
233
  });
229
234
 
230
- test("undeclared flags with no persisted override default to enabled", () => {
235
+ test("declared flags with no persisted override use registry default", () => {
231
236
  createSkillOnDisk("browser", "Browser", "Web browsing automation");
232
237
 
233
238
  currentConfig = {
@@ -236,7 +241,8 @@ describe("buildSystemPrompt assistant feature flag filtering", () => {
236
241
 
237
242
  const result = buildSystemPrompt();
238
243
 
239
- expect(result).toContain('id="browser"');
244
+ // browser is declared in the registry with defaultEnabled: false
245
+ expect(result).not.toContain('id="browser"');
240
246
  });
241
247
  });
242
248
 
@@ -265,10 +271,12 @@ describe("isAssistantFeatureFlagEnabled", () => {
265
271
 
266
272
  test("missing persisted value falls back to defaults registry defaultEnabled", () => {
267
273
  // No explicit config at all — should fall back to defaults registry
268
- // which has defaultEnabled: true for hatch-new-assistant
274
+ // which has defaultEnabled: false for hatch-new-assistant
269
275
  const config = {} as any;
270
276
 
271
- expect(isAssistantFeatureFlagEnabled(DECLARED_FLAG_KEY, config)).toBe(true);
277
+ expect(isAssistantFeatureFlagEnabled(DECLARED_FLAG_KEY, config)).toBe(
278
+ false,
279
+ );
272
280
  });
273
281
 
274
282
  test("unknown flag defaults to true when no persisted override", () => {
@@ -302,9 +310,9 @@ describe("legacy isSkillFeatureEnabled backward compat", () => {
302
310
  expect(isSkillFeatureEnabled(DECLARED_SKILL_ID, config)).toBe(false);
303
311
  });
304
312
 
305
- test("enabled when no override set", () => {
313
+ test("disabled when no override set (registry default is false)", () => {
306
314
  const config = {} as any;
307
315
 
308
- expect(isSkillFeatureEnabled(DECLARED_SKILL_ID, config)).toBe(true);
316
+ expect(isSkillFeatureEnabled(DECLARED_SKILL_ID, config)).toBe(false);
309
317
  });
310
318
  });
@@ -4,7 +4,16 @@
4
4
  * Locks the final invariants from the BROWSER_SKILL plan so that future
5
5
  * changes cannot silently regress any of the migration guarantees.
6
6
  */
7
- import { afterAll, beforeAll, describe, expect, test } from "bun:test";
7
+ import { afterAll, beforeAll, describe, expect, mock, test } from "bun:test";
8
+
9
+ mock.module("../config/loader.js", () => ({
10
+ getConfig: () => ({
11
+ sandbox: { enabled: false, backend: "native" },
12
+ assistantFeatureFlagValues: {
13
+ "feature_flags.browser.enabled": true,
14
+ },
15
+ }),
16
+ }));
8
17
 
9
18
  import {
10
19
  projectSkillTools,
@@ -62,6 +62,7 @@ const GATEWAY_RETRIEVAL_BANLIST: Array<{
62
62
  bannedSnippets: [
63
63
  'curl -s "$INTERNAL_GATEWAY_BASE_URL/v1/ingress/members',
64
64
  'curl -s "$INTERNAL_GATEWAY_BASE_URL/v1/ingress/invites',
65
+ 'curl -s "$INTERNAL_GATEWAY_BASE_URL/v1/contacts/invites',
65
66
  'curl -s "$INTERNAL_GATEWAY_BASE_URL/v1/integrations/telegram/config',
66
67
  ],
67
68
  },
@@ -280,7 +280,7 @@ describe("inbound callback metadata triggers decision handling", () => {
280
280
  const deliverSpy = spyOn(
281
281
  gatewayClient,
282
282
  "deliverChannelReply",
283
- ).mockResolvedValue(undefined);
283
+ ).mockResolvedValue({ ok: true });
284
284
 
285
285
  // Establish the conversation to get a conversationId mapping
286
286
  const initReq = makeInboundRequest({ content: "init" });
@@ -321,7 +321,7 @@ describe("inbound callback metadata triggers decision handling", () => {
321
321
  const deliverSpy = spyOn(
322
322
  gatewayClient,
323
323
  "deliverChannelReply",
324
- ).mockResolvedValue(undefined);
324
+ ).mockResolvedValue({ ok: true });
325
325
 
326
326
  const initReq = makeInboundRequest({ content: "init" });
327
327
  await handleChannelInbound(initReq, noopProcessMessage);
@@ -374,7 +374,7 @@ describe("inbound text matching approval phrases triggers decision handling", ()
374
374
  const deliverSpy = spyOn(
375
375
  gatewayClient,
376
376
  "deliverChannelReply",
377
- ).mockResolvedValue(undefined);
377
+ ).mockResolvedValue({ ok: true });
378
378
 
379
379
  const initReq = makeInboundRequest({ content: "init" });
380
380
  await handleChannelInbound(initReq, noopProcessMessage);
@@ -407,7 +407,7 @@ describe("inbound text matching approval phrases triggers decision handling", ()
407
407
  const deliverSpy = spyOn(
408
408
  gatewayClient,
409
409
  "deliverChannelReply",
410
- ).mockResolvedValue(undefined);
410
+ ).mockResolvedValue({ ok: true });
411
411
 
412
412
  const initReq = makeInboundRequest({ content: "init" });
413
413
  await handleChannelInbound(initReq, noopProcessMessage);
@@ -456,7 +456,7 @@ describe("non-decision messages during pending approval (legacy fallback)", () =
456
456
  const replySpy = spyOn(
457
457
  gatewayClient,
458
458
  "deliverChannelReply",
459
- ).mockResolvedValue(undefined);
459
+ ).mockResolvedValue({ ok: true });
460
460
 
461
461
  const initReq = makeInboundRequest({ content: "init" });
462
462
  await handleChannelInbound(initReq, noopProcessMessage);
@@ -565,7 +565,7 @@ describe("empty content with callbackData bypasses validation", () => {
565
565
  const deliverSpy = spyOn(
566
566
  gatewayClient,
567
567
  "deliverChannelReply",
568
- ).mockResolvedValue(undefined);
568
+ ).mockResolvedValue({ ok: true });
569
569
 
570
570
  const req = makeInboundRequest({
571
571
  content: "",
@@ -603,7 +603,7 @@ describe("empty content with callbackData bypasses validation", () => {
603
603
  const deliverSpy = spyOn(
604
604
  gatewayClient,
605
605
  "deliverChannelReply",
606
- ).mockResolvedValue(undefined);
606
+ ).mockResolvedValue({ ok: true });
607
607
 
608
608
  // Send with no content field at all, just callbackData
609
609
  const reqBody = {
@@ -651,7 +651,7 @@ describe("callback requestId validation", () => {
651
651
  const deliverSpy = spyOn(
652
652
  gatewayClient,
653
653
  "deliverChannelReply",
654
- ).mockResolvedValue(undefined);
654
+ ).mockResolvedValue({ ok: true });
655
655
 
656
656
  const initReq = makeInboundRequest({ content: "init" });
657
657
  await handleChannelInbound(initReq, noopProcessMessage);
@@ -691,7 +691,7 @@ describe("callback requestId validation", () => {
691
691
  const deliverSpy = spyOn(
692
692
  gatewayClient,
693
693
  "deliverChannelReply",
694
- ).mockResolvedValue(undefined);
694
+ ).mockResolvedValue({ ok: true });
695
695
 
696
696
  const initReq = makeInboundRequest({ content: "init" });
697
697
  await handleChannelInbound(initReq, noopProcessMessage);
@@ -729,7 +729,7 @@ describe("callback requestId validation", () => {
729
729
  const deliverSpy = spyOn(
730
730
  gatewayClient,
731
731
  "deliverChannelReply",
732
- ).mockResolvedValue(undefined);
732
+ ).mockResolvedValue({ ok: true });
733
733
 
734
734
  const initReq = makeInboundRequest({ content: "init" });
735
735
  await handleChannelInbound(initReq, noopProcessMessage);
@@ -779,7 +779,7 @@ describe("no immediate reply after approval decision", () => {
779
779
  const deliverSpy = spyOn(
780
780
  gatewayClient,
781
781
  "deliverChannelReply",
782
- ).mockResolvedValue(undefined);
782
+ ).mockResolvedValue({ ok: true });
783
783
 
784
784
  const initReq = makeInboundRequest({ content: "init" });
785
785
  await handleChannelInbound(initReq, noopProcessMessage);
@@ -818,7 +818,7 @@ describe("no immediate reply after approval decision", () => {
818
818
  const deliverSpy = spyOn(
819
819
  gatewayClient,
820
820
  "deliverChannelReply",
821
- ).mockResolvedValue(undefined);
821
+ ).mockResolvedValue({ ok: true });
822
822
 
823
823
  const initReq = makeInboundRequest({ content: "init" });
824
824
  await handleChannelInbound(initReq, noopProcessMessage);
@@ -934,7 +934,7 @@ describe("SMS channel approval decisions", () => {
934
934
  const deliverSpy = spyOn(
935
935
  gatewayClient,
936
936
  "deliverChannelReply",
937
- ).mockResolvedValue(undefined);
937
+ ).mockResolvedValue({ ok: true });
938
938
 
939
939
  // Establish the conversation via SMS
940
940
  const initReq = makeSmsInboundRequest({ content: "init" });
@@ -968,7 +968,7 @@ describe("SMS channel approval decisions", () => {
968
968
  const deliverSpy = spyOn(
969
969
  gatewayClient,
970
970
  "deliverChannelReply",
971
- ).mockResolvedValue(undefined);
971
+ ).mockResolvedValue({ ok: true });
972
972
 
973
973
  const initReq = makeSmsInboundRequest({ content: "init" });
974
974
  await handleChannelInbound(initReq, noopProcessMessage);
@@ -1001,7 +1001,7 @@ describe("SMS channel approval decisions", () => {
1001
1001
  const deliverSpy = spyOn(
1002
1002
  gatewayClient,
1003
1003
  "deliverChannelReply",
1004
- ).mockResolvedValue(undefined);
1004
+ ).mockResolvedValue({ ok: true });
1005
1005
  const approvalSpy = spyOn(
1006
1006
  gatewayClient,
1007
1007
  "deliverApprovalPrompt",
@@ -1061,7 +1061,7 @@ describe("SMS guardian verify intercept", () => {
1061
1061
  const deliverSpy = spyOn(
1062
1062
  gatewayClient,
1063
1063
  "deliverChannelReply",
1064
- ).mockResolvedValue(undefined);
1064
+ ).mockResolvedValue({ ok: true });
1065
1065
 
1066
1066
  const req = new Request("http://localhost/channels/inbound", {
1067
1067
  method: "POST",
@@ -1105,7 +1105,7 @@ describe("SMS guardian verify intercept", () => {
1105
1105
  const deliverSpy = spyOn(
1106
1106
  gatewayClient,
1107
1107
  "deliverChannelReply",
1108
- ).mockResolvedValue(undefined);
1108
+ ).mockResolvedValue({ ok: true });
1109
1109
 
1110
1110
  const req = new Request("http://localhost/channels/inbound", {
1111
1111
  method: "POST",
@@ -1202,7 +1202,7 @@ describe("guardian decision scoping — multiple pending approvals", () => {
1202
1202
  const deliverSpy = spyOn(
1203
1203
  gatewayClient,
1204
1204
  "deliverChannelReply",
1205
- ).mockResolvedValue(undefined);
1205
+ ).mockResolvedValue({ ok: true });
1206
1206
 
1207
1207
  const olderConvId = "conv-scope-older";
1208
1208
  const newerConvId = "conv-scope-newer";
@@ -1287,7 +1287,7 @@ describe("ambiguous plain-text decision with multiple pending requests", () => {
1287
1287
  const deliverSpy = spyOn(
1288
1288
  gatewayClient,
1289
1289
  "deliverChannelReply",
1290
- ).mockResolvedValue(undefined);
1290
+ ).mockResolvedValue({ ok: true });
1291
1291
 
1292
1292
  const convA = "conv-ambig-a";
1293
1293
  const convB = "conv-ambig-b";
@@ -1376,7 +1376,7 @@ describe("expired guardian approval auto-denies via sweep", () => {
1376
1376
  const deliverSpy = spyOn(
1377
1377
  gatewayClient,
1378
1378
  "deliverChannelReply",
1379
- ).mockResolvedValue(undefined);
1379
+ ).mockResolvedValue({ ok: true });
1380
1380
 
1381
1381
  const convId = "conv-expiry-sweep";
1382
1382
  ensureConversation(convId);
@@ -1440,7 +1440,7 @@ describe("expired guardian approval auto-denies via sweep", () => {
1440
1440
  const deliverSpy = spyOn(
1441
1441
  gatewayClient,
1442
1442
  "deliverChannelReply",
1443
- ).mockResolvedValue(undefined);
1443
+ ).mockResolvedValue({ ok: true });
1444
1444
 
1445
1445
  const convId = "conv-not-expired";
1446
1446
  ensureConversation(convId);
@@ -1515,7 +1515,7 @@ describe("assistant-scoped guardian verification via handleChannelInbound", () =
1515
1515
  const deliverSpy = spyOn(
1516
1516
  gatewayClient,
1517
1517
  "deliverChannelReply",
1518
- ).mockResolvedValue(undefined);
1518
+ ).mockResolvedValue({ ok: true });
1519
1519
 
1520
1520
  const req = makeInboundRequest({
1521
1521
  content: secret,
@@ -1543,7 +1543,7 @@ describe("assistant-scoped guardian verification via handleChannelInbound", () =
1543
1543
  const deliverSpy = spyOn(
1544
1544
  gatewayClient,
1545
1545
  "deliverChannelReply",
1546
- ).mockResolvedValue(undefined);
1546
+ ).mockResolvedValue({ ok: true });
1547
1547
 
1548
1548
  const req = makeInboundRequest({
1549
1549
  content: secret,
@@ -1577,7 +1577,7 @@ describe("assistant-scoped guardian verification via handleChannelInbound", () =
1577
1577
  const deliverSpy = spyOn(
1578
1578
  gatewayClient,
1579
1579
  "deliverChannelReply",
1580
- ).mockResolvedValue(undefined);
1580
+ ).mockResolvedValue({ ok: true });
1581
1581
 
1582
1582
  const req = makeInboundRequest({
1583
1583
  content: secret,
@@ -1677,7 +1677,7 @@ describe("conversational approval engine — standard path", () => {
1677
1677
  const deliverSpy = spyOn(
1678
1678
  gatewayClient,
1679
1679
  "deliverChannelReply",
1680
- ).mockResolvedValue(undefined);
1680
+ ).mockResolvedValue({ ok: true });
1681
1681
 
1682
1682
  const initReq = makeInboundRequest({ content: "init" });
1683
1683
  await handleChannelInbound(initReq, noopProcessMessage);
@@ -1735,7 +1735,7 @@ describe("conversational approval engine — standard path", () => {
1735
1735
  const deliverSpy = spyOn(
1736
1736
  gatewayClient,
1737
1737
  "deliverChannelReply",
1738
- ).mockResolvedValue(undefined);
1738
+ ).mockResolvedValue({ ok: true });
1739
1739
 
1740
1740
  const initReq = makeInboundRequest({ content: "init" });
1741
1741
  await handleChannelInbound(initReq, noopProcessMessage);
@@ -1783,7 +1783,7 @@ describe("conversational approval engine — standard path", () => {
1783
1783
  const deliverSpy = spyOn(
1784
1784
  gatewayClient,
1785
1785
  "deliverChannelReply",
1786
- ).mockResolvedValue(undefined);
1786
+ ).mockResolvedValue({ ok: true });
1787
1787
 
1788
1788
  const initReq = makeInboundRequest({ content: "init" });
1789
1789
  await handleChannelInbound(initReq, noopProcessMessage);
@@ -1830,7 +1830,7 @@ describe("conversational approval engine — standard path", () => {
1830
1830
  const deliverSpy = spyOn(
1831
1831
  gatewayClient,
1832
1832
  "deliverChannelReply",
1833
- ).mockResolvedValue(undefined);
1833
+ ).mockResolvedValue({ ok: true });
1834
1834
 
1835
1835
  const initReq = makeInboundRequest({ content: "init" });
1836
1836
  await handleChannelInbound(initReq, noopProcessMessage);
@@ -1896,7 +1896,7 @@ describe("guardian conversational approval via conversation engine", () => {
1896
1896
  const deliverSpy = spyOn(
1897
1897
  gatewayClient,
1898
1898
  "deliverChannelReply",
1899
- ).mockResolvedValue(undefined);
1899
+ ).mockResolvedValue({ ok: true });
1900
1900
 
1901
1901
  const convId = "conv-guardian-clarify";
1902
1902
  ensureConversation(convId);
@@ -1978,7 +1978,7 @@ describe("guardian conversational approval via conversation engine", () => {
1978
1978
  const deliverSpy = spyOn(
1979
1979
  gatewayClient,
1980
1980
  "deliverChannelReply",
1981
- ).mockResolvedValue(undefined);
1981
+ ).mockResolvedValue({ ok: true });
1982
1982
 
1983
1983
  const convId = "conv-guardian-nlp";
1984
1984
  ensureConversation(convId);
@@ -2058,7 +2058,7 @@ describe("guardian conversational approval via conversation engine", () => {
2058
2058
  const deliverSpy = spyOn(
2059
2059
  gatewayClient,
2060
2060
  "deliverChannelReply",
2061
- ).mockResolvedValue(undefined);
2061
+ ).mockResolvedValue({ ok: true });
2062
2062
 
2063
2063
  const convId = "conv-guardian-downgrade";
2064
2064
  ensureConversation(convId);
@@ -2113,7 +2113,7 @@ describe("guardian conversational approval via conversation engine", () => {
2113
2113
  const deliverSpy = spyOn(
2114
2114
  gatewayClient,
2115
2115
  "deliverChannelReply",
2116
- ).mockResolvedValue(undefined);
2116
+ ).mockResolvedValue({ ok: true });
2117
2117
 
2118
2118
  const convA = "conv-multi-a";
2119
2119
  const convB = "conv-multi-b";
@@ -2218,7 +2218,7 @@ describe("keep_pending remains conversational — standard path", () => {
2218
2218
  const deliverSpy = spyOn(
2219
2219
  gatewayClient,
2220
2220
  "deliverChannelReply",
2221
- ).mockResolvedValue(undefined);
2221
+ ).mockResolvedValue({ ok: true });
2222
2222
 
2223
2223
  const initReq = makeInboundRequest({ content: "init" });
2224
2224
  await handleChannelInbound(initReq, noopProcessMessage);
@@ -2277,7 +2277,7 @@ describe("keep_pending remains conversational — guardian path", () => {
2277
2277
  const deliverSpy = spyOn(
2278
2278
  gatewayClient,
2279
2279
  "deliverChannelReply",
2280
- ).mockResolvedValue(undefined);
2280
+ ).mockResolvedValue({ ok: true });
2281
2281
 
2282
2282
  const convId = "conv-gfb-1";
2283
2283
  ensureConversation(convId);
@@ -2367,7 +2367,7 @@ describe("requester cancel of guardian-gated pending request", () => {
2367
2367
  const deliverSpy = spyOn(
2368
2368
  gatewayClient,
2369
2369
  "deliverChannelReply",
2370
- ).mockResolvedValue(undefined);
2370
+ ).mockResolvedValue({ ok: true });
2371
2371
 
2372
2372
  // Create requester conversation
2373
2373
  const initReq = makeInboundRequest({
@@ -2450,7 +2450,7 @@ describe("requester cancel of guardian-gated pending request", () => {
2450
2450
  const deliverSpy = spyOn(
2451
2451
  gatewayClient,
2452
2452
  "deliverChannelReply",
2453
- ).mockResolvedValue(undefined);
2453
+ ).mockResolvedValue({ ok: true });
2454
2454
 
2455
2455
  const initReq = makeInboundRequest({
2456
2456
  content: "init",
@@ -2526,7 +2526,7 @@ describe("requester cancel of guardian-gated pending request", () => {
2526
2526
  const deliverSpy = spyOn(
2527
2527
  gatewayClient,
2528
2528
  "deliverChannelReply",
2529
- ).mockResolvedValue(undefined);
2529
+ ).mockResolvedValue({ ok: true });
2530
2530
 
2531
2531
  const initReq = makeInboundRequest({
2532
2532
  content: "init",
@@ -2599,7 +2599,7 @@ describe("requester cancel of guardian-gated pending request", () => {
2599
2599
  const deliverSpy = spyOn(
2600
2600
  gatewayClient,
2601
2601
  "deliverChannelReply",
2602
- ).mockResolvedValue(undefined);
2602
+ ).mockResolvedValue({ ok: true });
2603
2603
 
2604
2604
  const initReq = makeInboundRequest({
2605
2605
  content: "init",
@@ -2669,7 +2669,7 @@ describe("engine decision race condition — standard path", () => {
2669
2669
  const deliverSpy = spyOn(
2670
2670
  gatewayClient,
2671
2671
  "deliverChannelReply",
2672
- ).mockResolvedValue(undefined);
2672
+ ).mockResolvedValue({ ok: true });
2673
2673
 
2674
2674
  const initReq = makeInboundRequest({ content: "init" });
2675
2675
  await handleChannelInbound(initReq, noopProcessMessage);
@@ -2737,7 +2737,7 @@ describe("engine decision race condition — guardian path", () => {
2737
2737
  const deliverSpy = spyOn(
2738
2738
  gatewayClient,
2739
2739
  "deliverChannelReply",
2740
- ).mockResolvedValue(undefined);
2740
+ ).mockResolvedValue({ ok: true });
2741
2741
 
2742
2742
  const convId = "conv-guardian-race";
2743
2743
  ensureConversation(convId);
@@ -2828,7 +2828,7 @@ describe("non-decision status reply for different channels", () => {
2828
2828
  const deliverSpy = spyOn(
2829
2829
  gatewayClient,
2830
2830
  "deliverChannelReply",
2831
- ).mockResolvedValue(undefined);
2831
+ ).mockResolvedValue({ ok: true });
2832
2832
 
2833
2833
  // Establish the conversation using sms (non-rich channel)
2834
2834
  const initReq = makeInboundRequest({
@@ -2875,7 +2875,7 @@ describe("non-decision status reply for different channels", () => {
2875
2875
  const replySpy = spyOn(
2876
2876
  gatewayClient,
2877
2877
  "deliverChannelReply",
2878
- ).mockResolvedValue(undefined);
2878
+ ).mockResolvedValue({ ok: true });
2879
2879
 
2880
2880
  // Establish the conversation using telegram (rich channel)
2881
2881
  const initReq = makeInboundRequest({
@@ -3318,7 +3318,7 @@ describe("trusted-contact self-approval blocked before guardian approval row exi
3318
3318
  const deliverSpy = spyOn(
3319
3319
  gatewayClient,
3320
3320
  "deliverChannelReply",
3321
- ).mockResolvedValue(undefined);
3321
+ ).mockResolvedValue({ ok: true });
3322
3322
 
3323
3323
  // Create the requester conversation (different user than guardian)
3324
3324
  const initReq = makeInboundRequest({
@@ -3387,7 +3387,7 @@ describe("trusted-contact self-approval blocked before guardian approval row exi
3387
3387
  const deliverSpy = spyOn(
3388
3388
  gatewayClient,
3389
3389
  "deliverChannelReply",
3390
- ).mockResolvedValue(undefined);
3390
+ ).mockResolvedValue({ ok: true });
3391
3391
 
3392
3392
  const initReq = makeInboundRequest({
3393
3393
  content: "init",
@@ -229,6 +229,14 @@ describe("parseCallbackData", () => {
229
229
  expect(result!.source).toBe("whatsapp_button");
230
230
  });
231
231
 
232
+ test("parses slack source channel", () => {
233
+ const result = parseCallbackData("apr:req-789:approve_once", "slack");
234
+ expect(result).not.toBeNull();
235
+ expect(result!.action).toBe("approve_once");
236
+ expect(result!.requestId).toBe("req-789");
237
+ expect(result!.source).toBe("slack_button");
238
+ });
239
+
232
240
  test("returns null for unknown action", () => {
233
241
  expect(parseCallbackData("apr:req-123:unknown_action")).toBeNull();
234
242
  });
@@ -234,6 +234,41 @@ describe("buildApprovalUIMetadata", () => {
234
234
  expect(metadata.requestId).toBe("req-abc");
235
235
  expect(metadata.actions).toEqual(prompt.actions);
236
236
  expect(metadata.plainTextFallback).toBe("Reply yes or no.");
237
+ expect(metadata.permissionDetails).toEqual({
238
+ toolName: "shell",
239
+ riskLevel: "low",
240
+ toolInput: { command: "ls" },
241
+ });
242
+ });
243
+
244
+ test("includes requesterIdentifier in permissionDetails when provided", () => {
245
+ const prompt: ChannelApprovalPrompt = {
246
+ promptText: "Allow deploy?",
247
+ actions: [
248
+ { id: "approve_once", label: "Approve once" },
249
+ { id: "reject", label: "Reject" },
250
+ ],
251
+ plainTextFallback: "Reply yes or no.",
252
+ };
253
+
254
+ const approvalInfo: PendingApprovalInfo = {
255
+ requestId: "req-guard",
256
+ toolName: "deploy",
257
+ input: { target: "prod" },
258
+ riskLevel: "high",
259
+ };
260
+
261
+ const metadata = buildApprovalUIMetadata(
262
+ prompt,
263
+ approvalInfo,
264
+ "alice@example.com",
265
+ );
266
+ expect(metadata.permissionDetails).toEqual({
267
+ toolName: "deploy",
268
+ riskLevel: "high",
269
+ toolInput: { target: "prod" },
270
+ requesterIdentifier: "alice@example.com",
271
+ });
237
272
  });
238
273
  });
239
274
 
@@ -478,8 +513,11 @@ describe("channelSupportsRichApprovalUI", () => {
478
513
  expect(channelSupportsRichApprovalUI("sms")).toBe(false);
479
514
  });
480
515
 
516
+ test("returns true for slack", () => {
517
+ expect(channelSupportsRichApprovalUI("slack")).toBe(true);
518
+ });
519
+
481
520
  test("returns false for unknown channels", () => {
482
- expect(channelSupportsRichApprovalUI("slack")).toBe(false);
483
521
  expect(channelSupportsRichApprovalUI("")).toBe(false);
484
522
  });
485
523
  });
@@ -394,7 +394,9 @@ describe("guardian service challenge validation", () => {
394
394
  );
395
395
 
396
396
  expect(result.success).toBe(true);
397
- expect(result.verificationType).toBe("guardian");
397
+ if (result.success) {
398
+ expect(result.verificationType).toBe("guardian");
399
+ }
398
400
  });
399
401
 
400
402
  test("validateAndConsumeChallenge does not create a guardian binding (caller responsibility)", () => {
@@ -497,7 +499,9 @@ describe("guardian service challenge validation", () => {
497
499
  );
498
500
 
499
501
  expect(result.success).toBe(true);
500
- expect(result.verificationType).toBe("guardian");
502
+ if (result.success) {
503
+ expect(result.verificationType).toBe("guardian");
504
+ }
501
505
 
502
506
  // validateAndConsumeChallenge no longer creates bindings — that is
503
507
  // now handled by the caller (verification-intercept / relay-server).
@@ -1757,7 +1761,9 @@ describe("voice guardian challenge validation", () => {
1757
1761
  );
1758
1762
 
1759
1763
  expect(result.success).toBe(true);
1760
- expect(result.verificationType).toBe("guardian");
1764
+ if (result.success) {
1765
+ expect(result.verificationType).toBe("guardian");
1766
+ }
1761
1767
  });
1762
1768
 
1763
1769
  test("validateAndConsumeChallenge does not create a guardian binding for voice (caller responsibility)", () => {
@@ -2492,7 +2498,9 @@ describe("outbound verification sessions", () => {
2492
2498
  );
2493
2499
 
2494
2500
  expect(result.success).toBe(true);
2495
- expect(result.verificationType).toBe("guardian");
2501
+ if (result.success) {
2502
+ expect(result.verificationType).toBe("guardian");
2503
+ }
2496
2504
  });
2497
2505
 
2498
2506
  // ── Session state transitions ──
@@ -3022,7 +3030,9 @@ describe("outbound SMS verification", () => {
3022
3030
  );
3023
3031
 
3024
3032
  expect(result.success).toBe(true);
3025
- expect(result.verificationType).toBe("guardian");
3033
+ if (result.success) {
3034
+ expect(result.verificationType).toBe("guardian");
3035
+ }
3026
3036
  });
3027
3037
 
3028
3038
  test("inbound SMS from wrong identity + correct code is rejected", () => {
@@ -56,6 +56,7 @@ mock.module("../runtime/gateway-client.js", () => ({
56
56
  throw new Error("Simulated delivery failure (502)");
57
57
  }
58
58
  deliveryCalls.push({ callbackUrl, payload, bearerToken });
59
+ return { ok: true };
59
60
  },
60
61
  }));
61
62
 
@@ -334,6 +335,36 @@ describe("channel-reply-delivery", () => {
334
335
  expect(deliveryCalls).toHaveLength(0);
335
336
  });
336
337
 
338
+ it("passes ephemeral and user through to each delivery call", async () => {
339
+ await deliverRenderedReplyViaCallback({
340
+ callbackUrl: "http://gateway/deliver/slack",
341
+ chatId: "C123",
342
+ textSegments: ["Part 1.", "Part 2."],
343
+ interSegmentDelayMs: 0,
344
+ ephemeral: true,
345
+ user: "U456",
346
+ });
347
+
348
+ expect(deliveryCalls).toHaveLength(2);
349
+ expect(deliveryCalls[0].payload.ephemeral).toBe(true);
350
+ expect(deliveryCalls[0].payload.user).toBe("U456");
351
+ expect(deliveryCalls[1].payload.ephemeral).toBe(true);
352
+ expect(deliveryCalls[1].payload.user).toBe("U456");
353
+ });
354
+
355
+ it("does not include ephemeral fields when not set", async () => {
356
+ await deliverRenderedReplyViaCallback({
357
+ callbackUrl: "http://gateway/deliver/slack",
358
+ chatId: "C123",
359
+ textSegments: ["Normal message."],
360
+ interSegmentDelayMs: 0,
361
+ });
362
+
363
+ expect(deliveryCalls).toHaveLength(1);
364
+ expect(deliveryCalls[0].payload.ephemeral).toBeUndefined();
365
+ expect(deliveryCalls[0].payload.user).toBeUndefined();
366
+ });
367
+
337
368
  it("passes startFromSegment through deliverReplyViaCallback options", async () => {
338
369
  conversationMessages.push(
339
370
  { id: "msg-u", role: "user", content: "hi" },
@@ -42,6 +42,10 @@ describe("CommitEnrichmentService", () => {
42
42
  beforeEach(() => {
43
43
  _resetGitServiceRegistry();
44
44
  _resetEnrichmentService();
45
+ // Previous tests' enrichment jobs may leave a stale index.lock if
46
+ // the git process exits but the lock file isn't flushed before the
47
+ // next test runs git operations in the shared testDir.
48
+ rmSync(join(testDir, ".git", "index.lock"), { force: true });
45
49
  });
46
50
 
47
51
  afterEach(async () => {