@vellumai/assistant 0.3.15 → 0.3.18

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 (306) hide show
  1. package/ARCHITECTURE.md +211 -12
  2. package/Dockerfile +1 -1
  3. package/README.md +11 -5
  4. package/docs/architecture/http-token-refresh.md +274 -0
  5. package/docs/architecture/memory.md +5 -4
  6. package/docs/architecture/scheduling.md +4 -88
  7. package/docs/runbook-trusted-contacts.md +283 -0
  8. package/docs/trusted-contact-access.md +247 -0
  9. package/package.json +1 -1
  10. package/scripts/ipc/check-swift-decoder-drift.ts +2 -0
  11. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +2 -6
  12. package/src/__tests__/access-request-decision.test.ts +328 -0
  13. package/src/__tests__/asset-materialize-tool.test.ts +7 -7
  14. package/src/__tests__/asset-search-tool.test.ts +15 -15
  15. package/src/__tests__/attachments-store.test.ts +13 -13
  16. package/src/__tests__/call-controller.test.ts +150 -4
  17. package/src/__tests__/call-conversation-messages.test.ts +2 -2
  18. package/src/__tests__/call-pointer-messages.test.ts +28 -0
  19. package/src/__tests__/call-start-guardian-guard.test.ts +93 -0
  20. package/src/__tests__/channel-approval-routes.test.ts +108 -12
  21. package/src/__tests__/channel-guardian.test.ts +19 -15
  22. package/src/__tests__/checker.test.ts +103 -48
  23. package/src/__tests__/computer-use-skill-manifest-regression.test.ts +2 -2
  24. package/src/__tests__/config-watcher.test.ts +356 -0
  25. package/src/__tests__/conversation-pairing.test.ts +127 -27
  26. package/src/__tests__/conversation-store.test.ts +36 -36
  27. package/src/__tests__/date-context.test.ts +179 -1
  28. package/src/__tests__/db-migration-rollback.test.ts +4 -7
  29. package/src/__tests__/deterministic-verification-control-plane.test.ts +5 -5
  30. package/src/__tests__/emit-signal-routing-intent.test.ts +179 -0
  31. package/src/__tests__/gateway-only-guard.test.ts +188 -0
  32. package/src/__tests__/guardian-action-conversation-turn.test.ts +451 -0
  33. package/src/__tests__/guardian-action-copy-generator.test.ts +197 -0
  34. package/src/__tests__/guardian-action-followup-executor.test.ts +379 -0
  35. package/src/__tests__/guardian-action-followup-store.test.ts +376 -0
  36. package/src/__tests__/guardian-action-late-reply.test.ts +425 -0
  37. package/src/__tests__/guardian-action-no-hardcoded-copy.test.ts +71 -0
  38. package/src/__tests__/guardian-action-store.test.ts +182 -0
  39. package/src/__tests__/guardian-action-sweep.test.ts +9 -9
  40. package/src/__tests__/guardian-dispatch.test.ts +120 -0
  41. package/src/__tests__/guardian-outbound-http.test.ts +194 -2
  42. package/src/__tests__/guardian-verification-intent-routing.test.ts +179 -0
  43. package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +141 -0
  44. package/src/__tests__/handlers-telegram-config.test.ts +6 -6
  45. package/src/__tests__/hooks-runner.test.ts +13 -4
  46. package/src/__tests__/ingress-routes-http.test.ts +443 -0
  47. package/src/__tests__/intent-routing.test.ts +14 -0
  48. package/src/__tests__/ipc-snapshot.test.ts +23 -5
  49. package/src/__tests__/media-reuse-story.e2e.test.ts +7 -7
  50. package/src/__tests__/memory-regressions.test.ts +16 -12
  51. package/src/__tests__/non-member-access-request.test.ts +281 -0
  52. package/src/__tests__/notification-broadcaster.test.ts +115 -4
  53. package/src/__tests__/notification-decision-strategy.test.ts +138 -1
  54. package/src/__tests__/notification-deep-link.test.ts +44 -1
  55. package/src/__tests__/notification-guardian-path.test.ts +157 -0
  56. package/src/__tests__/notification-routing-intent.test.ts +11 -1
  57. package/src/__tests__/notification-thread-candidate-validation.test.ts +215 -0
  58. package/src/__tests__/notification-thread-candidates.test.ts +166 -0
  59. package/src/__tests__/recording-intent.test.ts +1 -0
  60. package/src/__tests__/recording-state-machine.test.ts +328 -17
  61. package/src/__tests__/registry.test.ts +17 -8
  62. package/src/__tests__/relay-server.test.ts +105 -0
  63. package/src/__tests__/reminder.test.ts +13 -0
  64. package/src/__tests__/runtime-attachment-metadata.test.ts +4 -4
  65. package/src/__tests__/scheduler-recurrence.test.ts +50 -0
  66. package/src/__tests__/server-history-render.test.ts +8 -8
  67. package/src/__tests__/session-agent-loop.test.ts +1 -0
  68. package/src/__tests__/session-runtime-assembly.test.ts +49 -0
  69. package/src/__tests__/session-skill-tools.test.ts +1 -0
  70. package/src/__tests__/skill-projection.benchmark.test.ts +11 -3
  71. package/src/__tests__/slack-channel-config.test.ts +230 -0
  72. package/src/__tests__/subagent-manager-notify.test.ts +4 -4
  73. package/src/__tests__/swarm-session-integration.test.ts +2 -2
  74. package/src/__tests__/system-prompt.test.ts +43 -0
  75. package/src/__tests__/task-management-tools.test.ts +3 -3
  76. package/src/__tests__/task-tools.test.ts +3 -3
  77. package/src/__tests__/trust-store.test.ts +38 -22
  78. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +489 -0
  79. package/src/__tests__/trusted-contact-multichannel.test.ts +405 -0
  80. package/src/__tests__/trusted-contact-verification.test.ts +360 -0
  81. package/src/__tests__/update-bulletin-format.test.ts +119 -0
  82. package/src/__tests__/update-bulletin-state.test.ts +129 -0
  83. package/src/__tests__/update-bulletin.test.ts +323 -0
  84. package/src/__tests__/update-template-contract.test.ts +24 -0
  85. package/src/__tests__/voice-session-bridge.test.ts +109 -9
  86. package/src/agent/loop.ts +2 -2
  87. package/src/amazon/client.ts +2 -3
  88. package/src/calls/call-controller.ts +241 -39
  89. package/src/calls/call-conversation-messages.ts +2 -2
  90. package/src/calls/call-domain.ts +10 -3
  91. package/src/calls/call-pointer-messages.ts +17 -5
  92. package/src/calls/guardian-action-sweep.ts +77 -36
  93. package/src/calls/guardian-dispatch.ts +8 -0
  94. package/src/calls/relay-server.ts +51 -12
  95. package/src/calls/twilio-routes.ts +3 -1
  96. package/src/calls/types.ts +1 -1
  97. package/src/calls/voice-session-bridge.ts +8 -6
  98. package/src/cli/core-commands.ts +43 -3
  99. package/src/cli/map.ts +8 -5
  100. package/src/config/bundled-skills/phone-calls/SKILL.md +16 -1
  101. package/src/config/bundled-skills/tasks/SKILL.md +1 -1
  102. package/src/config/bundled-skills/tasks/TOOLS.json +4 -4
  103. package/src/config/bundled-skills/time-based-actions/SKILL.md +11 -1
  104. package/src/config/computer-use-prompt.ts +1 -0
  105. package/src/config/core-schema.ts +16 -0
  106. package/src/config/env-registry.ts +1 -0
  107. package/src/config/env.ts +16 -1
  108. package/src/config/memory-schema.ts +5 -0
  109. package/src/config/schema.ts +4 -0
  110. package/src/config/system-prompt.ts +69 -2
  111. package/src/config/templates/BOOTSTRAP.md +1 -1
  112. package/src/config/templates/IDENTITY.md +8 -4
  113. package/src/config/templates/SOUL.md +14 -0
  114. package/src/config/templates/UPDATES.md +15 -0
  115. package/src/config/templates/USER.md +5 -1
  116. package/src/config/types.ts +1 -0
  117. package/src/config/update-bulletin-format.ts +54 -0
  118. package/src/config/update-bulletin-state.ts +49 -0
  119. package/src/config/update-bulletin-template-path.ts +6 -0
  120. package/src/config/update-bulletin.ts +97 -0
  121. package/src/config/vellum-skills/catalog.json +6 -0
  122. package/src/config/vellum-skills/chatgpt-import/tools/chatgpt-import.ts +1 -1
  123. package/src/config/vellum-skills/guardian-verify-setup/SKILL.md +44 -10
  124. package/src/config/vellum-skills/telegram-setup/SKILL.md +4 -4
  125. package/src/config/vellum-skills/trusted-contacts/SKILL.md +147 -0
  126. package/src/config/vellum-skills/twilio-setup/SKILL.md +2 -2
  127. package/src/context/window-manager.ts +43 -3
  128. package/src/daemon/config-watcher.ts +4 -2
  129. package/src/daemon/connection-policy.ts +21 -1
  130. package/src/daemon/daemon-control.ts +219 -8
  131. package/src/daemon/date-context.ts +174 -1
  132. package/src/daemon/guardian-action-generators.ts +175 -0
  133. package/src/daemon/guardian-verification-intent.ts +120 -0
  134. package/src/daemon/handlers/apps.ts +1 -3
  135. package/src/daemon/handlers/config-channels.ts +2 -2
  136. package/src/daemon/handlers/config-heartbeat.ts +1 -1
  137. package/src/daemon/handlers/config-inbox.ts +55 -159
  138. package/src/daemon/handlers/config-ingress.ts +1 -1
  139. package/src/daemon/handlers/config-integrations.ts +1 -1
  140. package/src/daemon/handlers/config-platform.ts +1 -1
  141. package/src/daemon/handlers/config-scheduling.ts +2 -2
  142. package/src/daemon/handlers/config-slack-channel.ts +190 -0
  143. package/src/daemon/handlers/config-telegram.ts +1 -1
  144. package/src/daemon/handlers/config-twilio.ts +1 -1
  145. package/src/daemon/handlers/config-voice.ts +100 -0
  146. package/src/daemon/handlers/config.ts +3 -0
  147. package/src/daemon/handlers/identity.ts +45 -25
  148. package/src/daemon/handlers/misc.ts +83 -5
  149. package/src/daemon/handlers/navigate-settings.ts +27 -0
  150. package/src/daemon/handlers/recording.ts +270 -144
  151. package/src/daemon/handlers/sessions.ts +100 -17
  152. package/src/daemon/handlers/subagents.ts +3 -3
  153. package/src/daemon/handlers/work-items.ts +10 -7
  154. package/src/daemon/ipc-contract/integrations.ts +9 -1
  155. package/src/daemon/ipc-contract/messages.ts +4 -0
  156. package/src/daemon/ipc-contract/sessions.ts +1 -1
  157. package/src/daemon/ipc-contract/settings.ts +26 -0
  158. package/src/daemon/ipc-contract/shared.ts +2 -0
  159. package/src/daemon/ipc-contract/work-items.ts +1 -7
  160. package/src/daemon/ipc-contract/workspace.ts +12 -1
  161. package/src/daemon/ipc-contract-inventory.json +6 -1
  162. package/src/daemon/ipc-contract.ts +5 -1
  163. package/src/daemon/lifecycle.ts +314 -266
  164. package/src/daemon/recording-intent.ts +0 -41
  165. package/src/daemon/response-tier.ts +2 -2
  166. package/src/daemon/server.ts +31 -9
  167. package/src/daemon/session-agent-loop-handlers.ts +34 -9
  168. package/src/daemon/session-agent-loop.ts +15 -8
  169. package/src/daemon/session-history.ts +3 -2
  170. package/src/daemon/session-media-retry.ts +3 -0
  171. package/src/daemon/session-messaging.ts +38 -4
  172. package/src/daemon/session-notifiers.ts +2 -2
  173. package/src/daemon/session-process.ts +546 -59
  174. package/src/daemon/session-queue-manager.ts +2 -0
  175. package/src/daemon/session-runtime-assembly.ts +39 -0
  176. package/src/daemon/session-skill-tools.ts +13 -4
  177. package/src/daemon/session-tool-setup.ts +5 -6
  178. package/src/daemon/session.ts +19 -8
  179. package/src/daemon/tls-certs.ts +60 -13
  180. package/src/daemon/tool-side-effects.ts +13 -5
  181. package/src/gallery/default-gallery.ts +32 -9
  182. package/src/influencer/client.ts +2 -1
  183. package/src/memory/channel-delivery-store.ts +35 -567
  184. package/src/memory/channel-guardian-store.ts +63 -1317
  185. package/src/memory/conflict-store.ts +4 -4
  186. package/src/memory/conversation-attention-store.ts +0 -3
  187. package/src/memory/conversation-crud.ts +668 -0
  188. package/src/memory/conversation-queries.ts +361 -0
  189. package/src/memory/conversation-store.ts +44 -983
  190. package/src/memory/db-connection.ts +3 -0
  191. package/src/memory/db-init.ts +33 -0
  192. package/src/memory/delivery-channels.ts +175 -0
  193. package/src/memory/delivery-crud.ts +211 -0
  194. package/src/memory/delivery-status.ts +199 -0
  195. package/src/memory/embedding-backend.ts +70 -4
  196. package/src/memory/embedding-local.ts +12 -2
  197. package/src/memory/entity-extractor.ts +3 -8
  198. package/src/memory/fts-reconciler.ts +136 -0
  199. package/src/memory/guardian-action-store.ts +418 -5
  200. package/src/memory/guardian-approvals.ts +569 -0
  201. package/src/memory/guardian-bindings.ts +130 -0
  202. package/src/memory/guardian-rate-limits.ts +196 -0
  203. package/src/memory/guardian-verification.ts +521 -0
  204. package/src/memory/job-handlers/index-maintenance.ts +2 -1
  205. package/src/memory/job-utils.ts +8 -5
  206. package/src/memory/jobs-store.ts +66 -6
  207. package/src/memory/jobs-worker.ts +23 -1
  208. package/src/memory/migrations/030-guardian-action-followup.ts +21 -0
  209. package/src/memory/migrations/030-guardian-verification-purpose.ts +17 -0
  210. package/src/memory/migrations/031-conversations-thread-type-index.ts +5 -0
  211. package/src/memory/migrations/032-guardian-delivery-conversation-index.ts +15 -0
  212. package/src/memory/migrations/032-notification-delivery-thread-decision.ts +20 -0
  213. package/src/memory/migrations/100-core-tables.ts +1 -1
  214. package/src/memory/migrations/101-watchers-and-logs.ts +4 -0
  215. package/src/memory/migrations/108-tasks-and-work-items.ts +1 -1
  216. package/src/memory/migrations/112-assistant-inbox.ts +1 -1
  217. package/src/memory/migrations/113-late-migrations.ts +1 -1
  218. package/src/memory/migrations/116-messages-fts.ts +13 -0
  219. package/src/memory/migrations/119-schema-indexes-and-columns.ts +37 -0
  220. package/src/memory/migrations/120-fk-cascade-rebuilds.ts +161 -0
  221. package/src/memory/migrations/index.ts +10 -3
  222. package/src/memory/migrations/validate-migration-state.ts +114 -15
  223. package/src/memory/qdrant-circuit-breaker.ts +105 -0
  224. package/src/memory/retriever.ts +46 -13
  225. package/src/memory/schema-migration.ts +4 -0
  226. package/src/memory/schema.ts +31 -8
  227. package/src/memory/search/semantic.ts +8 -90
  228. package/src/notifications/README.md +159 -18
  229. package/src/notifications/broadcaster.ts +69 -33
  230. package/src/notifications/conversation-pairing.ts +99 -21
  231. package/src/notifications/decision-engine.ts +176 -8
  232. package/src/notifications/deliveries-store.ts +39 -8
  233. package/src/notifications/emit-signal.ts +1 -0
  234. package/src/notifications/preferences-store.ts +7 -7
  235. package/src/notifications/thread-candidates.ts +269 -0
  236. package/src/notifications/types.ts +19 -0
  237. package/src/permissions/checker.ts +1 -16
  238. package/src/permissions/defaults.ts +25 -5
  239. package/src/permissions/prompter.ts +17 -0
  240. package/src/permissions/trust-store.ts +2 -0
  241. package/src/providers/failover.ts +19 -0
  242. package/src/providers/registry.ts +46 -1
  243. package/src/runtime/approval-message-composer.ts +1 -1
  244. package/src/runtime/channel-guardian-service.ts +15 -3
  245. package/src/runtime/channel-retry-sweep.ts +7 -2
  246. package/src/runtime/guardian-action-conversation-turn.ts +85 -0
  247. package/src/runtime/guardian-action-followup-executor.ts +301 -0
  248. package/src/runtime/guardian-action-message-composer.ts +245 -0
  249. package/src/runtime/guardian-outbound-actions.ts +26 -6
  250. package/src/runtime/guardian-verification-templates.ts +15 -9
  251. package/src/runtime/http-errors.ts +93 -0
  252. package/src/runtime/http-server.ts +133 -44
  253. package/src/runtime/http-types.ts +53 -0
  254. package/src/runtime/ingress-service.ts +237 -0
  255. package/src/runtime/middleware/error-handler.ts +4 -3
  256. package/src/runtime/middleware/rate-limiter.ts +160 -0
  257. package/src/runtime/middleware/request-logger.ts +71 -0
  258. package/src/runtime/middleware/twilio-validation.ts +7 -6
  259. package/src/runtime/pending-interactions.ts +12 -0
  260. package/src/runtime/routes/access-request-decision.ts +215 -0
  261. package/src/runtime/routes/app-routes.ts +25 -18
  262. package/src/runtime/routes/approval-routes.ts +18 -47
  263. package/src/runtime/routes/attachment-routes.ts +15 -41
  264. package/src/runtime/routes/call-routes.ts +20 -20
  265. package/src/runtime/routes/channel-delivery-routes.ts +6 -5
  266. package/src/runtime/routes/contact-routes.ts +4 -9
  267. package/src/runtime/routes/conversation-attention-routes.ts +2 -1
  268. package/src/runtime/routes/conversation-routes.ts +26 -57
  269. package/src/runtime/routes/debug-routes.ts +71 -0
  270. package/src/runtime/routes/events-routes.ts +3 -2
  271. package/src/runtime/routes/guardian-approval-interception.ts +221 -0
  272. package/src/runtime/routes/identity-routes.ts +14 -10
  273. package/src/runtime/routes/inbound-conversation.ts +3 -2
  274. package/src/runtime/routes/inbound-message-handler.ts +527 -62
  275. package/src/runtime/routes/ingress-routes.ts +174 -0
  276. package/src/runtime/routes/integration-routes.ts +78 -16
  277. package/src/runtime/routes/pairing-routes.ts +11 -10
  278. package/src/runtime/routes/secret-routes.ts +10 -18
  279. package/src/runtime/verification-rate-limiter.ts +83 -0
  280. package/src/schedule/schedule-store.ts +13 -1
  281. package/src/schedule/scheduler.ts +1 -1
  282. package/src/security/secret-ingress.ts +5 -2
  283. package/src/security/secret-scanner.ts +72 -6
  284. package/src/subagent/manager.ts +6 -4
  285. package/src/swarm/plan-validator.ts +4 -1
  286. package/src/tasks/task-runner.ts +3 -1
  287. package/src/tools/browser/api-map.ts +9 -6
  288. package/src/tools/calls/call-start.ts +20 -0
  289. package/src/tools/executor.ts +50 -568
  290. package/src/tools/permission-checker.ts +271 -0
  291. package/src/tools/registry.ts +14 -6
  292. package/src/tools/reminder/reminder-store.ts +7 -7
  293. package/src/tools/reminder/reminder.ts +6 -3
  294. package/src/tools/secret-detection-handler.ts +301 -0
  295. package/src/tools/subagent/message.ts +1 -1
  296. package/src/tools/system/voice-config.ts +62 -0
  297. package/src/tools/tasks/index.ts +3 -3
  298. package/src/tools/tasks/work-item-list.ts +3 -3
  299. package/src/tools/tasks/work-item-update.ts +4 -5
  300. package/src/tools/tool-approval-handler.ts +192 -0
  301. package/src/tools/tool-manifest.ts +2 -0
  302. package/src/version.ts +29 -2
  303. package/src/watcher/watcher-store.ts +9 -9
  304. package/src/work-items/work-item-runner.ts +9 -6
  305. /package/src/memory/migrations/{026-embeddings-nullable-vector-json.ts → 026a-embeddings-nullable-vector-json.ts} +0 -0
  306. /package/src/memory/migrations/{027-guardian-bootstrap-token.ts → 027a-guardian-bootstrap-token.ts} +0 -0
@@ -881,7 +881,7 @@ describe('SMS channel approval decisions', () => {
881
881
  // ═══════════════════════════════════════════════════════════════════════════
882
882
 
883
883
  describe('SMS guardian verify intercept', () => {
884
- test('/guardian_verify command works with sourceChannel sms', async () => {
884
+ test('verification code reply works with sourceChannel sms', async () => {
885
885
  const { createVerificationChallenge } = await import('../runtime/channel-guardian-service.js');
886
886
  const { secret } = createVerificationChallenge('self', 'sms');
887
887
 
@@ -898,7 +898,7 @@ describe('SMS guardian verify intercept', () => {
898
898
  interface: 'sms',
899
899
  externalChatId: 'sms-chat-verify',
900
900
  externalMessageId: `msg-${Date.now()}-${Math.random()}`,
901
- content: `/guardian_verify ${secret}`,
901
+ content: secret,
902
902
  senderExternalUserId: 'sms-user-42',
903
903
  replyCallbackUrl: 'https://gateway.test/deliver',
904
904
  }),
@@ -921,7 +921,11 @@ describe('SMS guardian verify intercept', () => {
921
921
  deliverSpy.mockRestore();
922
922
  });
923
923
 
924
- test('/guardian_verify with invalid token returns failed via SMS', async () => {
924
+ test('invalid verification code returns failed via SMS', async () => {
925
+ const { createVerificationChallenge } = await import('../runtime/channel-guardian-service.js');
926
+ // Ensure there is a pending challenge so bare-code verification is intercepted.
927
+ createVerificationChallenge('self', 'sms');
928
+
925
929
  const deliverSpy = spyOn(gatewayClient, 'deliverChannelReply').mockResolvedValue(undefined);
926
930
 
927
931
  const req = new Request('http://localhost/channels/inbound', {
@@ -935,7 +939,7 @@ describe('SMS guardian verify intercept', () => {
935
939
  interface: 'sms',
936
940
  externalChatId: 'sms-chat-verify-fail',
937
941
  externalMessageId: `msg-${Date.now()}-${Math.random()}`,
938
- content: '/guardian_verify invalid-token-here',
942
+ content: '000000',
939
943
  senderExternalUserId: 'sms-user-43',
940
944
  replyCallbackUrl: 'https://gateway.test/deliver',
941
945
  }),
@@ -956,6 +960,51 @@ describe('SMS guardian verify intercept', () => {
956
960
 
957
961
  deliverSpy.mockRestore();
958
962
  });
963
+
964
+ test('64-char hex verification codes are intercepted when a pending challenge exists', async () => {
965
+ const { createHash, randomBytes } = await import('node:crypto');
966
+ const { createChallenge } = await import('../memory/channel-guardian-store.js');
967
+
968
+ const secret = randomBytes(32).toString('hex');
969
+ const challengeHash = createHash('sha256').update(secret).digest('hex');
970
+ createChallenge({
971
+ id: `challenge-hex-${Date.now()}`,
972
+ assistantId: 'self',
973
+ channel: 'sms',
974
+ challengeHash,
975
+ expiresAt: Date.now() + 600_000,
976
+ });
977
+
978
+ let processMessageCalled = false;
979
+ const processMessage = async () => {
980
+ processMessageCalled = true;
981
+ return { messageId: 'msg-hex-not-verify' };
982
+ };
983
+
984
+ const req = new Request('http://localhost/channels/inbound', {
985
+ method: 'POST',
986
+ headers: {
987
+ 'Content-Type': 'application/json',
988
+ 'X-Gateway-Origin': TEST_BEARER_TOKEN,
989
+ },
990
+ body: JSON.stringify({
991
+ sourceChannel: 'sms',
992
+ interface: 'sms',
993
+ externalChatId: 'sms-chat-hex-message',
994
+ externalMessageId: `msg-${Date.now()}-${Math.random()}`,
995
+ content: secret,
996
+ senderExternalUserId: 'sms-user-hex',
997
+ replyCallbackUrl: 'https://gateway.test/deliver',
998
+ }),
999
+ });
1000
+
1001
+ const res = await handleChannelInbound(req, processMessage, TEST_BEARER_TOKEN);
1002
+ const body = await res.json() as Record<string, unknown>;
1003
+
1004
+ expect(body.accepted).toBe(true);
1005
+ expect(body.guardianVerification).toBe('verified');
1006
+ expect(processMessageCalled).toBe(false);
1007
+ });
959
1008
  });
960
1009
 
961
1010
  // ═══════════════════════════════════════════════════════════════════════════
@@ -1242,14 +1291,14 @@ describe('deliver-once idempotency guard', () => {
1242
1291
  // ═══════════════════════════════════════════════════════════════════════════
1243
1292
 
1244
1293
  describe('assistant-scoped guardian verification via handleChannelInbound', () => {
1245
- test('/guardian_verify uses the threaded assistantId (default: self)', async () => {
1294
+ test('verification code uses the threaded assistantId (default: self)', async () => {
1246
1295
  const { createVerificationChallenge } = await import('../runtime/channel-guardian-service.js');
1247
1296
  const { secret } = createVerificationChallenge('self', 'telegram');
1248
1297
 
1249
1298
  const deliverSpy = spyOn(gatewayClient, 'deliverChannelReply').mockResolvedValue(undefined);
1250
1299
 
1251
1300
  const req = makeInboundRequest({
1252
- content: `/guardian_verify ${secret}`,
1301
+ content: secret,
1253
1302
  senderExternalUserId: 'user-default-asst',
1254
1303
  });
1255
1304
 
@@ -1262,7 +1311,7 @@ describe('assistant-scoped guardian verification via handleChannelInbound', () =
1262
1311
  deliverSpy.mockRestore();
1263
1312
  });
1264
1313
 
1265
- test('/guardian_verify with explicit assistantId resolves against that assistant', async () => {
1314
+ test('verification code with explicit assistantId resolves against that assistant', async () => {
1266
1315
  const { createVerificationChallenge } = await import('../runtime/channel-guardian-service.js');
1267
1316
  const { getGuardianBinding } = await import('../runtime/channel-guardian-service.js');
1268
1317
 
@@ -1271,7 +1320,7 @@ describe('assistant-scoped guardian verification via handleChannelInbound', () =
1271
1320
  const deliverSpy = spyOn(gatewayClient, 'deliverChannelReply').mockResolvedValue(undefined);
1272
1321
 
1273
1322
  const req = makeInboundRequest({
1274
- content: `/guardian_verify ${secret}`,
1323
+ content: secret,
1275
1324
  senderExternalUserId: 'user-for-asst-x',
1276
1325
  });
1277
1326
 
@@ -1288,7 +1337,7 @@ describe('assistant-scoped guardian verification via handleChannelInbound', () =
1288
1337
  deliverSpy.mockRestore();
1289
1338
  });
1290
1339
 
1291
- test('cross-assistant challenge verification fails', async () => {
1340
+ test('cross-assistant challenge code does not verify against a different assistant scope', async () => {
1292
1341
  const { createVerificationChallenge } = await import('../runtime/channel-guardian-service.js');
1293
1342
 
1294
1343
  const { secret } = createVerificationChallenge('asst-A-cross', 'telegram');
@@ -1296,7 +1345,7 @@ describe('assistant-scoped guardian verification via handleChannelInbound', () =
1296
1345
  const deliverSpy = spyOn(gatewayClient, 'deliverChannelReply').mockResolvedValue(undefined);
1297
1346
 
1298
1347
  const req = makeInboundRequest({
1299
- content: `/guardian_verify ${secret}`,
1348
+ content: secret,
1300
1349
  senderExternalUserId: 'user-cross-test',
1301
1350
  });
1302
1351
 
@@ -1304,7 +1353,7 @@ describe('assistant-scoped guardian verification via handleChannelInbound', () =
1304
1353
  const body = await res.json() as Record<string, unknown>;
1305
1354
 
1306
1355
  expect(body.accepted).toBe(true);
1307
- expect(body.guardianVerification).toBe('failed');
1356
+ expect(body.guardianVerification).toBeUndefined();
1308
1357
 
1309
1358
  deliverSpy.mockRestore();
1310
1359
  });
@@ -2506,7 +2555,15 @@ describe('non-decision status reply for different channels', () => {
2506
2555
  // ═══════════════════════════════════════════════════════════════════════════
2507
2556
 
2508
2557
  describe('background channel processing approval prompts', () => {
2509
- test('marks channel turns interactive and delivers approval prompt when confirmation is pending', async () => {
2558
+ test('marks guardian channel turns interactive and delivers approval prompt when confirmation is pending', async () => {
2559
+ // Set up a guardian binding so the sender is recognized as a guardian
2560
+ createBinding({
2561
+ assistantId: 'self',
2562
+ channel: 'telegram',
2563
+ guardianExternalUserId: 'telegram-user-default',
2564
+ guardianDeliveryChatId: 'chat-123',
2565
+ });
2566
+
2510
2567
  const deliverPromptSpy = spyOn(gatewayClient, 'deliverApprovalPrompt').mockResolvedValue(undefined);
2511
2568
  const processCalls: Array<{ options?: Record<string, unknown> }> = [];
2512
2569
 
@@ -2549,4 +2606,43 @@ describe('background channel processing approval prompts', () => {
2549
2606
 
2550
2607
  deliverPromptSpy.mockRestore();
2551
2608
  });
2609
+
2610
+ test('non-guardian channel turns are not interactive to prevent self-approval', async () => {
2611
+ // Set up a guardian binding for a DIFFERENT user so the sender is non-guardian
2612
+ createBinding({
2613
+ assistantId: 'self',
2614
+ channel: 'telegram',
2615
+ guardianExternalUserId: 'guardian-user-other',
2616
+ guardianDeliveryChatId: 'guardian-chat-other',
2617
+ });
2618
+
2619
+ const processCalls: Array<{ options?: Record<string, unknown> }> = [];
2620
+
2621
+ const processMessage = mock(async (
2622
+ _conversationId: string,
2623
+ _content: string,
2624
+ _attachmentIds?: string[],
2625
+ options?: Record<string, unknown>,
2626
+ ) => {
2627
+ processCalls.push({ options });
2628
+ await new Promise((resolve) => setTimeout(resolve, 50));
2629
+ return { messageId: 'msg-ng-1' };
2630
+ });
2631
+
2632
+ const req = makeInboundRequest({
2633
+ content: 'run something',
2634
+ sourceChannel: 'telegram',
2635
+ replyCallbackUrl: 'https://gateway.test/deliver/telegram',
2636
+ externalMessageId: 'msg-ng-1',
2637
+ });
2638
+
2639
+ const res = await handleChannelInbound(req, processMessage as unknown as typeof noopProcessMessage, 'token');
2640
+ const body = await res.json() as Record<string, unknown>;
2641
+ expect(body.accepted).toBe(true);
2642
+
2643
+ await new Promise((resolve) => setTimeout(resolve, 300));
2644
+
2645
+ expect(processCalls.length).toBeGreaterThan(0);
2646
+ expect(processCalls[0].options?.isInteractive).toBe(false);
2647
+ });
2552
2648
  });
@@ -426,7 +426,7 @@ describe('guardian service challenge validation', () => {
426
426
  );
427
427
 
428
428
  expect(result.success).toBe(true);
429
- if (result.success) {
429
+ if (result.success && result.verificationType === 'guardian') {
430
430
  expect(result.bindingId).toBeDefined();
431
431
  }
432
432
  });
@@ -528,7 +528,7 @@ describe('guardian service challenge validation', () => {
528
528
  );
529
529
 
530
530
  expect(result.success).toBe(true);
531
- if (result.success) {
531
+ if (result.success && result.verificationType === 'guardian') {
532
532
  expect(result.bindingId).toBeDefined();
533
533
  }
534
534
 
@@ -1616,7 +1616,7 @@ describe('voice guardian challenge validation', () => {
1616
1616
  );
1617
1617
 
1618
1618
  expect(result.success).toBe(true);
1619
- if (result.success) {
1619
+ if (result.success && result.verificationType === 'guardian') {
1620
1620
  expect(result.bindingId).toBeDefined();
1621
1621
  }
1622
1622
  });
@@ -2261,7 +2261,7 @@ describe('outbound verification sessions', () => {
2261
2261
  );
2262
2262
 
2263
2263
  expect(result.success).toBe(true);
2264
- if (result.success) {
2264
+ if (result.success && result.verificationType === 'guardian') {
2265
2265
  expect(result.bindingId).toBeDefined();
2266
2266
  }
2267
2267
  });
@@ -2740,7 +2740,12 @@ describe('outbound SMS verification', () => {
2740
2740
 
2741
2741
  expect(result.success).toBe(true);
2742
2742
  if (result.success) {
2743
- expect(result.bindingId).toBeDefined();
2743
+ // Guardian outbound sessions (no verificationPurpose override) create
2744
+ // guardian bindings on success
2745
+ expect(result.verificationType).toBe('guardian');
2746
+ if (result.verificationType === 'guardian') {
2747
+ expect(result.bindingId).toBeDefined();
2748
+ }
2744
2749
  }
2745
2750
  });
2746
2751
 
@@ -3166,7 +3171,7 @@ describe('outbound Telegram verification', () => {
3166
3171
  expect(result.success).toBe(false);
3167
3172
  });
3168
3173
 
3169
- test('backward compat: inbound-only /guardian_verify Telegram flow still works', () => {
3174
+ test('inbound-only Telegram verification flow still works with bare code', () => {
3170
3175
  // Create an inbound-only challenge (no outbound session, no expected identity)
3171
3176
  const challengeResult = createVerificationChallenge('self', 'telegram');
3172
3177
 
@@ -3274,23 +3279,22 @@ describe('outbound Telegram verification', () => {
3274
3279
  expect(revoked).toBeNull();
3275
3280
  });
3276
3281
 
3277
- test('telegram template includes verification code in message', () => {
3282
+ test('telegram template does not include verification code in message', () => {
3278
3283
  const msg = composeVerificationTelegram(
3279
3284
  GUARDIAN_VERIFY_TEMPLATE_KEYS.TELEGRAM_CHALLENGE_REQUEST,
3280
3285
  { code: 'abc123', expiresInMinutes: 10 },
3281
3286
  );
3282
- // Code must be visible in-channel for bootstrap flows where the app cannot display it
3283
- expect(msg).toContain('abc123');
3284
- expect(msg).toContain('/guardian_verify abc123');
3287
+ expect(msg).not.toContain('abc123');
3288
+ expect(msg).not.toContain('guardian_verify');
3285
3289
  });
3286
3290
 
3287
- test('telegram resend template includes code and (resent) suffix', () => {
3291
+ test('telegram resend template does not include code and includes (resent) suffix', () => {
3288
3292
  const msg = composeVerificationTelegram(
3289
3293
  GUARDIAN_VERIFY_TEMPLATE_KEYS.TELEGRAM_RESEND,
3290
3294
  { code: 'xyz789', expiresInMinutes: 5 },
3291
3295
  );
3292
- expect(msg).toContain('xyz789');
3293
- expect(msg).toContain('/guardian_verify xyz789');
3296
+ expect(msg).not.toContain('xyz789');
3297
+ expect(msg).not.toContain('guardian_verify');
3294
3298
  expect(msg).toContain('(resent)');
3295
3299
  });
3296
3300
 
@@ -3300,7 +3304,7 @@ describe('outbound Telegram verification', () => {
3300
3304
  { code: '999999', expiresInMinutes: 10, assistantName: 'MyBot' },
3301
3305
  );
3302
3306
  expect(msg).toContain('Vellum assistant');
3303
- expect(msg).toContain('999999');
3307
+ expect(msg).not.toContain('999999');
3304
3308
  });
3305
3309
 
3306
3310
  test('start_outbound for telegram with missing destination fails', () => {
@@ -3703,13 +3707,13 @@ describe('M1–M4 hardening coverage', () => {
3703
3707
  // ── M2: bootstrap sessions use high-entropy hex secrets ──
3704
3708
 
3705
3709
  test('bootstrap (pending_bootstrap) sessions use high-entropy hex secrets, identity-bound use 6-digit numeric', () => {
3706
- // Pending bootstrap: high-entropy hex (32 bytes = 64 hex chars)
3707
3710
  const bootstrapResult = createOutboundSession({
3708
3711
  assistantId: 'asst-entropy',
3709
3712
  channel: 'telegram',
3710
3713
  identityBindingStatus: 'pending_bootstrap',
3711
3714
  destinationAddress: '@testuser',
3712
3715
  });
3716
+ // Pending bootstrap: high-entropy hex (32 bytes = 64 hex chars)
3713
3717
  expect(bootstrapResult.secret.length).toBe(64);
3714
3718
  expect(bootstrapResult.secret).toMatch(/^[a-f0-9]{64}$/);
3715
3719
 
@@ -272,8 +272,8 @@ describe('Permission Checker', () => {
272
272
  expect(await classifyRisk('bash', { command: 'some_custom_tool' })).toBe(RiskLevel.Medium);
273
273
  });
274
274
 
275
- test('rm (without -r) is medium risk', async () => {
276
- expect(await classifyRisk('bash', { command: 'rm file.txt' })).toBe(RiskLevel.Medium);
275
+ test('rm (without -r) is high risk', async () => {
276
+ expect(await classifyRisk('bash', { command: 'rm file.txt' })).toBe(RiskLevel.High);
277
277
  });
278
278
 
279
279
  test('chmod is medium risk', async () => {
@@ -374,7 +374,7 @@ describe('Permission Checker', () => {
374
374
  expect(high.matchedRule?.id).toBe('default:allow-bash-global');
375
375
 
376
376
  // Medium risk
377
- const med = await check('bash', { command: 'rm file.txt' }, '/tmp');
377
+ const med = await check('bash', { command: 'curl https://example.com' }, '/tmp');
378
378
  expect(med.decision).toBe('allow');
379
379
  expect(med.matchedRule?.id).toBe('default:allow-bash-global');
380
380
 
@@ -391,7 +391,7 @@ describe('Permission Checker', () => {
391
391
  const high = await check('bash', { command: 'sudo rm -rf /' }, '/tmp');
392
392
  expect(high.decision).toBe('prompt');
393
393
 
394
- const med = await check('bash', { command: 'rm file.txt' }, '/tmp');
394
+ const med = await check('bash', { command: 'curl https://example.com' }, '/tmp');
395
395
  expect(med.decision).toBe('prompt');
396
396
 
397
397
  // Low risk still auto-allows via the normal risk-based fallback
@@ -409,17 +409,31 @@ describe('Permission Checker', () => {
409
409
  expect(result.decision).toBe('prompt');
410
410
  });
411
411
 
412
- test('host_bash medium risk with no matching rule → prompt', async () => {
412
+ test('host_bash rm is always high risk → prompt', async () => {
413
413
  const result = await check('host_bash', { command: 'rm file.txt' }, '/tmp');
414
414
  expect(result.decision).toBe('prompt');
415
+ expect(result.reason).toContain('High risk');
415
416
  });
416
417
 
417
- test('medium risk with matching trust rule allow', async () => {
418
+ test('plain rm (without -rf) is high risk and prompts despite default allow rule', async () => {
419
+ // Validates that ALL rm commands are escalated to High risk, not just rm -rf.
420
+ // The default allow rule for host_bash auto-approves Low/Medium risk but
421
+ // High risk always prompts.
422
+ const result = await check('host_bash', { command: 'rm single-file.txt' }, '/tmp');
423
+ expect(result.decision).toBe('prompt');
424
+ expect(result.reason).toContain('High risk');
425
+
426
+ // Also verify rm -rf still prompts
427
+ const rfResult = await check('host_bash', { command: 'rm -rf /tmp/dir' }, '/tmp');
428
+ expect(rfResult.decision).toBe('prompt');
429
+ expect(rfResult.reason).toContain('High risk');
430
+ });
431
+
432
+ test('rm is high risk even with matching trust rule → prompt', async () => {
418
433
  addRule('bash', 'rm *', '/tmp');
419
434
  const result = await check('bash', { command: 'rm file.txt' }, '/tmp');
420
- expect(result.decision).toBe('allow');
421
- expect(result.reason).toContain('Matched trust rule');
422
- expect(result.matchedRule).toBeDefined();
435
+ expect(result.decision).toBe('prompt');
436
+ expect(result.reason).toContain('High risk');
423
437
  });
424
438
 
425
439
  test('file_read → auto-allow', async () => {
@@ -489,11 +503,11 @@ describe('Permission Checker', () => {
489
503
  expect(result.matchedRule?.id).toBe('default:ask-host_file_edit-global');
490
504
  });
491
505
 
492
- test('host_bash prompts by default via host ask rule', async () => {
506
+ test('host_bash auto-allows low risk via default allow rule', async () => {
493
507
  const result = await check('host_bash', { command: 'ls' }, '/tmp');
494
- expect(result.decision).toBe('prompt');
495
- expect(result.reason).toContain('ask rule');
496
- expect(result.matchedRule?.id).toBe('default:ask-host_bash-global');
508
+ expect(result.decision).toBe('allow');
509
+ expect(result.reason).toContain('Matched trust rule');
510
+ expect(result.matchedRule?.id).toBe('default:allow-host_bash-global');
497
511
  });
498
512
 
499
513
  test('scaffold_managed_skill prompts by default via managed skill ask rule', async () => {
@@ -597,7 +611,7 @@ describe('Permission Checker', () => {
597
611
  });
598
612
 
599
613
  // Deny rule tests
600
- test('deny rule blocks medium-risk command', async () => {
614
+ test('deny rule blocks high-risk command', async () => {
601
615
  addRule('bash', 'rm *', '/tmp', 'deny');
602
616
  const result = await check('bash', { command: 'rm file.txt' }, '/tmp');
603
617
  expect(result.decision).toBe('deny');
@@ -764,16 +778,16 @@ describe('Permission Checker', () => {
764
778
 
765
779
  // Priority-based rule resolution
766
780
  test('higher-priority allow rule overrides lower-priority deny rule', async () => {
767
- addRule('bash', 'rm *', '/tmp', 'deny', 0);
768
- addRule('bash', 'rm *', '/tmp', 'allow', 100);
769
- const result = await check('bash', { command: 'rm file.txt' }, '/tmp');
781
+ addRule('bash', 'chmod *', '/tmp', 'deny', 0);
782
+ addRule('bash', 'chmod *', '/tmp', 'allow', 100);
783
+ const result = await check('bash', { command: 'chmod 644 file.txt' }, '/tmp');
770
784
  expect(result.decision).toBe('allow');
771
785
  });
772
786
 
773
787
  test('higher-priority deny rule overrides lower-priority allow rule', async () => {
774
- addRule('bash', 'rm *', '/tmp', 'allow', 0);
775
- addRule('bash', 'rm *', '/tmp', 'deny', 100);
776
- const result = await check('bash', { command: 'rm file.txt' }, '/tmp');
788
+ addRule('bash', 'chmod *', '/tmp', 'allow', 0);
789
+ addRule('bash', 'chmod *', '/tmp', 'deny', 100);
790
+ const result = await check('bash', { command: 'chmod 644 file.txt' }, '/tmp');
777
791
  expect(result.decision).toBe('deny');
778
792
  });
779
793
 
@@ -927,6 +941,30 @@ describe('Permission Checker', () => {
927
941
  expect(result.matchedRule!.id).toBe('default:allow-file_write-bootstrap');
928
942
  });
929
943
 
944
+ test('file_read of workspace UPDATES.md is auto-allowed', async () => {
945
+ const updatesPath = join(checkerTestDir, 'workspace', 'UPDATES.md');
946
+ const result = await check('file_read', { path: updatesPath }, '/tmp');
947
+ expect(result.decision).toBe('allow');
948
+ expect(result.matchedRule).toBeDefined();
949
+ expect(result.matchedRule!.id).toBe('default:allow-file_read-updates');
950
+ });
951
+
952
+ test('file_write of workspace UPDATES.md is auto-allowed', async () => {
953
+ const updatesPath = join(checkerTestDir, 'workspace', 'UPDATES.md');
954
+ const result = await check('file_write', { path: updatesPath }, '/tmp');
955
+ expect(result.decision).toBe('allow');
956
+ expect(result.matchedRule).toBeDefined();
957
+ expect(result.matchedRule!.id).toBe('default:allow-file_write-updates');
958
+ });
959
+
960
+ test('file_edit of workspace UPDATES.md is auto-allowed', async () => {
961
+ const updatesPath = join(checkerTestDir, 'workspace', 'UPDATES.md');
962
+ const result = await check('file_edit', { path: updatesPath }, '/tmp');
963
+ expect(result.decision).toBe('allow');
964
+ expect(result.matchedRule).toBeDefined();
965
+ expect(result.matchedRule!.id).toBe('default:allow-file_edit-updates');
966
+ });
967
+
930
968
  test('file_write of non-workspace file is not auto-allowed', async () => {
931
969
  const otherPath = join(checkerTestDir, 'workspace', 'OTHER.md');
932
970
  const result = await check('file_write', { path: otherPath }, '/tmp');
@@ -1441,13 +1479,14 @@ describe('Permission Checker', () => {
1441
1479
  expect(result.matchedRule?.id).toBe('default:allow-bash-global');
1442
1480
  });
1443
1481
 
1444
- test('host_bash with no user rule returns prompt in strict mode', async () => {
1482
+ test('host_bash auto-allows low risk in strict mode (default allow rule is a matching rule)', async () => {
1445
1483
  testConfig.permissions.mode = 'strict';
1446
1484
  const result = await check('host_bash', { command: 'ls' }, '/tmp');
1447
- expect(result.decision).toBe('prompt');
1485
+ expect(result.decision).toBe('allow');
1486
+ expect(result.matchedRule?.id).toBe('default:allow-host_bash-global');
1448
1487
  });
1449
1488
 
1450
- test('medium-risk host_bash with no matching rule returns prompt in strict mode', async () => {
1489
+ test('high-risk host_bash (rm) with no matching rule returns prompt in strict mode', async () => {
1451
1490
  testConfig.permissions.mode = 'strict';
1452
1491
  const result = await check('host_bash', { command: 'rm file.txt' }, '/tmp');
1453
1492
  expect(result.decision).toBe('prompt');
@@ -1544,8 +1583,8 @@ describe('Permission Checker', () => {
1544
1583
  });
1545
1584
 
1546
1585
  test('medium-risk tool with allow rule is NOT affected by allowHighRisk', async () => {
1547
- addRule('bash', 'rm *', '/tmp', 'allow', 100);
1548
- const result = await check('bash', { command: 'rm file.txt' }, '/tmp');
1586
+ addRule('bash', 'chmod *', '/tmp', 'allow', 100);
1587
+ const result = await check('bash', { command: 'chmod 644 file.txt' }, '/tmp');
1549
1588
  expect(result.decision).toBe('allow');
1550
1589
  expect(result.reason).toContain('Matched trust rule');
1551
1590
  // No mention of high-risk in the reason
@@ -1615,8 +1654,8 @@ describe('Permission Checker', () => {
1615
1654
 
1616
1655
  test('strict mode: medium-risk with matching allow rule auto-allows', async () => {
1617
1656
  testConfig.permissions.mode = 'strict';
1618
- addRule('bash', 'rm *', '/tmp', 'allow');
1619
- const result = await check('bash', { command: 'rm file.txt' }, '/tmp');
1657
+ addRule('bash', 'chmod *', '/tmp', 'allow');
1658
+ const result = await check('bash', { command: 'chmod 644 file.txt' }, '/tmp');
1620
1659
  expect(result.decision).toBe('allow');
1621
1660
  expect(result.reason).toContain('Matched trust rule');
1622
1661
  });
@@ -2392,10 +2431,11 @@ describe('Permission Checker', () => {
2392
2431
  expect(result.matchedRule?.id).toBe('default:allow-bash-global');
2393
2432
  });
2394
2433
 
2395
- test('low-risk host_bash with no user rule prompts in strict mode', async () => {
2434
+ test('low-risk host_bash auto-allows in strict mode (default allow rule is a matching rule)', async () => {
2396
2435
  testConfig.permissions.mode = 'strict';
2397
2436
  const result = await check('host_bash', { command: 'echo hello' }, '/tmp');
2398
- expect(result.decision).toBe('prompt');
2437
+ expect(result.decision).toBe('allow');
2438
+ expect(result.matchedRule?.id).toBe('default:allow-host_bash-global');
2399
2439
  });
2400
2440
 
2401
2441
  test('low-risk file_read with no rule prompts in strict mode', async () => {
@@ -2457,10 +2497,10 @@ describe('Permission Checker', () => {
2457
2497
  // target-scoped. ───────────────────────────────────────────────
2458
2498
 
2459
2499
  describe('Invariant 4: host execution approvals are explicit and target-scoped', () => {
2460
- test('host_bash prompts by default (no implicit allow)', async () => {
2500
+ test('host_bash auto-allows low risk via default allow rule', async () => {
2461
2501
  const result = await check('host_bash', { command: 'ls' }, '/tmp');
2462
- expect(result.decision).toBe('prompt');
2463
- expect(result.matchedRule?.id).toBe('default:ask-host_bash-global');
2502
+ expect(result.decision).toBe('allow');
2503
+ expect(result.matchedRule?.id).toBe('default:allow-host_bash-global');
2464
2504
  });
2465
2505
 
2466
2506
  test('host_file_read prompts by default (no implicit allow)', async () => {
@@ -2507,11 +2547,11 @@ describe('Permission Checker', () => {
2507
2547
  expect(matchResult.matchedRule?.id).toBe('inv4-target-scoped');
2508
2548
 
2509
2549
  // Different target — the target-scoped rule should NOT match;
2510
- // falls back to the default host_bash ask rule (prompt)
2550
+ // falls back to the default host_bash allow rule (auto-allows medium risk)
2511
2551
  const noMatchResult = await check('host_bash', { command: 'run script.js' }, '/tmp', {
2512
2552
  executionTarget: '/usr/local/bin/bun',
2513
2553
  });
2514
- expect(noMatchResult.decision).toBe('prompt');
2554
+ expect(noMatchResult.decision).toBe('allow');
2515
2555
  expect(noMatchResult.matchedRule?.id).not.toBe('inv4-target-scoped');
2516
2556
  });
2517
2557
  });
@@ -2581,7 +2621,7 @@ describe('Permission Checker', () => {
2581
2621
  test('wildcard allow rule matches any command in legacy mode', async () => {
2582
2622
  testConfig.permissions.mode = 'legacy';
2583
2623
  addRule('bash', '*', 'everywhere');
2584
- const result = await check('bash', { command: 'rm file.txt' }, '/tmp');
2624
+ const result = await check('bash', { command: 'chmod 644 file.txt' }, '/tmp');
2585
2625
  expect(result.decision).toBe('allow');
2586
2626
  expect(result.matchedRule).toBeDefined();
2587
2627
  });
@@ -2589,7 +2629,7 @@ describe('Permission Checker', () => {
2589
2629
  test('wildcard allow rule matches any command in strict mode', async () => {
2590
2630
  testConfig.permissions.mode = 'strict';
2591
2631
  addRule('bash', '*', 'everywhere');
2592
- const result = await check('bash', { command: 'rm file.txt' }, '/tmp');
2632
+ const result = await check('bash', { command: 'chmod 644 file.txt' }, '/tmp');
2593
2633
  expect(result.decision).toBe('allow');
2594
2634
  expect(result.matchedRule).toBeDefined();
2595
2635
  });
@@ -2700,12 +2740,27 @@ describe('Permission Checker', () => {
2700
2740
  );
2701
2741
 
2702
2742
  test('getDefaultRuleTemplates has no extra rules when extraDirs is empty', () => {
2703
- // Default testConfig has no skills property → getConfig returns default
2704
- // with extraDirs: []
2705
2743
  const templates = getDefaultRuleTemplates();
2706
2744
  const extraRules = templates.filter((t) => t.id.includes('extra-'));
2707
2745
  expect(extraRules.length).toBe(0);
2708
2746
  });
2747
+
2748
+ test('getDefaultRuleTemplates tolerates partial config mocks', () => {
2749
+ const originalSkills = testConfig.skills;
2750
+ const originalSandbox = testConfig.sandbox;
2751
+ try {
2752
+ testConfig.skills = {} as any;
2753
+ testConfig.sandbox = {} as any;
2754
+
2755
+ const templates = getDefaultRuleTemplates();
2756
+ expect(Array.isArray(templates)).toBe(true);
2757
+ expect(templates.some((t) => t.id.includes('extra-'))).toBe(false);
2758
+ expect(templates.some((t) => t.id === 'default:allow-bash-global')).toBe(true);
2759
+ } finally {
2760
+ testConfig.skills = originalSkills;
2761
+ testConfig.sandbox = originalSandbox;
2762
+ }
2763
+ });
2709
2764
  });
2710
2765
 
2711
2766
  // ── backslash normalization gated to Windows (PR 3558 follow-up) ──
@@ -2928,8 +2983,8 @@ describe('bash network_mode=proxied force prompt', () => {
2928
2983
  });
2929
2984
 
2930
2985
  test('non-proxied bash with trust rule follows normal flow', async () => {
2931
- addRule('bash', 'rm *', '/tmp');
2932
- const result = await check('bash', { command: 'rm file.txt' }, '/tmp');
2986
+ addRule('bash', 'chmod *', '/tmp');
2987
+ const result = await check('bash', { command: 'chmod 644 file.txt' }, '/tmp');
2933
2988
  expect(result.decision).toBe('allow');
2934
2989
  expect(result.reason).not.toContain('Proxied network mode');
2935
2990
  });
@@ -3221,10 +3276,10 @@ describe('workspace mode — auto-allow workspace-scoped operations', () => {
3221
3276
  expect(result.reason).toContain('ask rule');
3222
3277
  });
3223
3278
 
3224
- test('host_bash → prompt (default ask rule matches)', async () => {
3279
+ test('host_bash → allow (default allow rule matches)', async () => {
3225
3280
  const result = await check('host_bash', { command: 'ls' }, workspaceDir);
3226
- expect(result.decision).toBe('prompt');
3227
- expect(result.reason).toContain('ask rule');
3281
+ expect(result.decision).toBe('allow');
3282
+ expect(result.reason).toContain('Matched trust rule');
3228
3283
  });
3229
3284
 
3230
3285
  // ── explicit rules still take precedence in workspace mode ──
@@ -3404,20 +3459,20 @@ describe('integration regressions (PR 11)', () => {
3404
3459
  });
3405
3460
 
3406
3461
  test('raw legacy rule still works alongside new action key system', async () => {
3407
- // Use medium-risk commands (rm) so they aren't auto-allowed by low-risk classification.
3462
+ // Use medium-risk commands (chmod) so they aren't auto-allowed by low-risk classification.
3408
3463
  // Disable sandbox so the catch-all "**" rule doesn't interfere.
3409
3464
  testConfig.sandbox.enabled = false;
3410
3465
  try { rmSync(join(checkerTestDir, 'protected', 'trust.json')); } catch { /* may not exist */ }
3411
3466
  clearCache();
3412
3467
  try {
3413
- addRule('bash', 'rm file.txt', 'everywhere');
3468
+ addRule('bash', 'chmod 644 file.txt', 'everywhere');
3414
3469
 
3415
3470
  // Exact match still works
3416
- const r1 = await check('bash', { command: 'rm file.txt' }, '/tmp');
3471
+ const r1 = await check('bash', { command: 'chmod 644 file.txt' }, '/tmp');
3417
3472
  expect(r1.decision).toBe('allow');
3418
3473
 
3419
- // Different rm argument should not match this exact raw rule
3420
- const r2 = await check('bash', { command: 'rm other.txt' }, '/tmp');
3474
+ // Different chmod argument should not match this exact raw rule
3475
+ const r2 = await check('bash', { command: 'chmod 755 other.txt' }, '/tmp');
3421
3476
  expect(r2.decision).not.toBe('allow');
3422
3477
  } finally {
3423
3478
  testConfig.sandbox.enabled = true;
@@ -77,8 +77,8 @@ describe('computer-use skill manifest regression', () => {
77
77
  await initializeTools();
78
78
 
79
79
  // The 12 computer_use_* action tools must NOT be in the global registry
80
- // after initializeTools(). If they were, registerSkillTools() would throw
81
- // a "collides with core tool" error when the computer-use skill is activated.
80
+ // after initializeTools(). If they were, registerSkillTools() would skip
81
+ // them as core tool collisions when the computer-use skill is activated.
82
82
  for (const name of COMPUTER_USE_TOOL_NAMES) {
83
83
  expect(getTool(name)).toBeUndefined();
84
84
  }