@vellumai/assistant 0.4.48 → 0.4.49

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 (252) hide show
  1. package/ARCHITECTURE.md +2 -2
  2. package/README.md +2 -23
  3. package/docs/architecture/integrations.md +45 -41
  4. package/docs/architecture/keychain-broker.md +3 -3
  5. package/docs/runbook-trusted-contacts.md +3 -8
  6. package/hook-templates/debug-prompt-logger/hook.json +1 -1
  7. package/hook-templates/debug-prompt-logger/run.sh +1 -3
  8. package/package.json +1 -1
  9. package/src/__tests__/actor-token-service.test.ts +0 -1
  10. package/src/__tests__/anthropic-provider.test.ts +156 -0
  11. package/src/__tests__/approval-cascade.test.ts +810 -0
  12. package/src/__tests__/approval-primitive.test.ts +0 -1
  13. package/src/__tests__/approval-routes-http.test.ts +2 -0
  14. package/src/__tests__/assistant-attachments.test.ts +12 -34
  15. package/src/__tests__/assistant-feature-flag-guardrails.test.ts +76 -0
  16. package/src/__tests__/assistant-feature-flags-integration.test.ts +0 -1
  17. package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +2 -2
  18. package/src/__tests__/channel-guardian.test.ts +0 -2
  19. package/src/__tests__/channel-readiness-routes.test.ts +15 -6
  20. package/src/__tests__/channel-readiness-service.test.ts +10 -9
  21. package/src/__tests__/checker.test.ts +9 -29
  22. package/src/__tests__/computer-use-skill-manifest-regression.test.ts +1 -1
  23. package/src/__tests__/computer-use-tools.test.ts +2 -19
  24. package/src/__tests__/config-watcher.test.ts +0 -1
  25. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +0 -1
  26. package/src/__tests__/context-image-dimensions.test.ts +332 -0
  27. package/src/__tests__/context-token-estimator.test.ts +196 -13
  28. package/src/__tests__/conversation-attention-store.test.ts +0 -1
  29. package/src/__tests__/conversation-attention-telegram.test.ts +0 -1
  30. package/src/__tests__/conversation-routes-guardian-reply.test.ts +144 -0
  31. package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
  32. package/src/__tests__/credential-metadata-store.test.ts +64 -73
  33. package/src/__tests__/credential-security-invariants.test.ts +13 -7
  34. package/src/__tests__/credential-vault-unit.test.ts +280 -49
  35. package/src/__tests__/credential-vault.test.ts +138 -16
  36. package/src/__tests__/credentials-cli.test.ts +71 -0
  37. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +0 -1
  38. package/src/__tests__/ephemeral-permissions.test.ts +3 -3
  39. package/src/__tests__/gateway-only-guard.test.ts +0 -1
  40. package/src/__tests__/guardian-action-grant-mint-consume.test.ts +0 -1
  41. package/src/__tests__/guardian-decision-primitive-canonical.test.ts +0 -1
  42. package/src/__tests__/guardian-routing-invariants.test.ts +0 -1
  43. package/src/__tests__/guardian-verification-voice-binding.test.ts +0 -1
  44. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +0 -39
  45. package/src/__tests__/heartbeat-service.test.ts +0 -1
  46. package/src/__tests__/host-cu-proxy.test.ts +629 -0
  47. package/src/__tests__/host-shell-tool.test.ts +27 -15
  48. package/src/__tests__/http-user-message-parity.test.ts +1 -0
  49. package/src/__tests__/ingress-url-consistency.test.ts +14 -21
  50. package/src/__tests__/integration-status.test.ts +32 -51
  51. package/src/__tests__/intent-routing.test.ts +0 -1
  52. package/src/__tests__/invite-routes-http.test.ts +10 -9
  53. package/src/__tests__/keychain-broker-client.test.ts +11 -43
  54. package/src/__tests__/notification-routing-intent.test.ts +0 -1
  55. package/src/__tests__/oauth-cli.test.ts +373 -14
  56. package/src/__tests__/oauth-provider-profiles.test.ts +9 -9
  57. package/src/__tests__/oauth-scope-policy.test.ts +4 -6
  58. package/src/__tests__/oauth-store.test.ts +756 -0
  59. package/src/__tests__/onboarding-starter-tasks.test.ts +0 -1
  60. package/src/__tests__/provider-error-scenarios.test.ts +0 -1
  61. package/src/__tests__/provider-streaming.benchmark.test.ts +0 -1
  62. package/src/__tests__/public-ingress-urls.test.ts +15 -21
  63. package/src/__tests__/recording-handler.test.ts +3 -4
  64. package/src/__tests__/registry.test.ts +2 -2
  65. package/src/__tests__/runtime-events-sse.test.ts +55 -7
  66. package/src/__tests__/schedule-store.test.ts +0 -1
  67. package/src/__tests__/scheduler-recurrence.test.ts +0 -1
  68. package/src/__tests__/scoped-approval-grants.test.ts +0 -1
  69. package/src/__tests__/scoped-grant-security-matrix.test.ts +0 -1
  70. package/src/__tests__/secret-ingress-handler.test.ts +0 -1
  71. package/src/__tests__/send-endpoint-busy.test.ts +21 -6
  72. package/src/__tests__/sequence-store.test.ts +0 -1
  73. package/src/__tests__/session-init.benchmark.test.ts +4 -5
  74. package/src/__tests__/skill-include-graph.test.ts +66 -0
  75. package/src/__tests__/skill-load-feature-flag.test.ts +0 -1
  76. package/src/__tests__/skill-load-tool.test.ts +149 -1
  77. package/src/__tests__/skill-projection-feature-flag.test.ts +0 -1
  78. package/src/__tests__/skills-uninstall.test.ts +1 -1
  79. package/src/__tests__/skills.test.ts +3 -3
  80. package/src/__tests__/slack-channel-config.test.ts +67 -3
  81. package/src/__tests__/slack-share-routes.test.ts +17 -19
  82. package/src/__tests__/system-prompt.test.ts +0 -1
  83. package/src/__tests__/telegram-invite-adapter.test.ts +18 -22
  84. package/src/__tests__/terminal-tools.test.ts +4 -3
  85. package/src/__tests__/test-support/computer-use-skill-harness.ts +3 -2
  86. package/src/__tests__/tool-approval-handler.test.ts +0 -1
  87. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +0 -1
  88. package/src/__tests__/tool-executor-lifecycle-events.test.ts +0 -1
  89. package/src/__tests__/tool-executor-shell-integration.test.ts +0 -1
  90. package/src/__tests__/tool-executor.test.ts +0 -1
  91. package/src/__tests__/tool-grant-request-escalation.test.ts +0 -1
  92. package/src/__tests__/trust-store-pattern-matches.test.ts +29 -0
  93. package/src/__tests__/trust-store.test.ts +1 -22
  94. package/src/__tests__/trusted-contact-approval-notifier.test.ts +0 -1
  95. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +0 -1
  96. package/src/__tests__/twilio-routes.test.ts +0 -16
  97. package/src/__tests__/verification-control-plane-policy.test.ts +0 -1
  98. package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -1
  99. package/src/agent/ax-tree-compaction.test.ts +235 -0
  100. package/src/agent/loop.ts +76 -130
  101. package/src/calls/call-domain.ts +1 -6
  102. package/src/calls/relay-server.ts +9 -13
  103. package/src/calls/twilio-config.ts +2 -7
  104. package/src/calls/twilio-routes.ts +1 -2
  105. package/src/calls/voice-ingress-preflight.ts +1 -1
  106. package/src/cli/commands/browser-relay.ts +18 -12
  107. package/src/cli/commands/completions.ts +0 -3
  108. package/src/cli/commands/credentials.ts +101 -15
  109. package/src/cli/commands/oauth/apps.ts +255 -0
  110. package/src/cli/commands/oauth/connections.ts +299 -0
  111. package/src/cli/commands/oauth/index.ts +52 -0
  112. package/src/cli/commands/oauth/providers.ts +242 -0
  113. package/src/cli/commands/skills.ts +4 -338
  114. package/src/cli/program.ts +1 -5
  115. package/src/cli/reference.ts +1 -3
  116. package/src/config/assistant-feature-flags.ts +0 -3
  117. package/src/config/bundled-skills/_shared/CLI_RETRIEVAL_PATTERN.md +1 -1
  118. package/src/config/bundled-skills/computer-use/SKILL.md +3 -6
  119. package/src/config/bundled-skills/computer-use/TOOLS.json +22 -4
  120. package/src/config/bundled-skills/google-calendar/calendar-client.ts +21 -16
  121. package/src/config/bundled-skills/messaging/tools/shared.ts +1 -4
  122. package/src/config/bundled-skills/settings/SKILL.md +1 -1
  123. package/src/config/bundled-skills/settings/TOOLS.json +2 -8
  124. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +5 -33
  125. package/src/config/env-registry.ts +14 -83
  126. package/src/config/env.ts +11 -50
  127. package/src/config/feature-flag-registry.json +16 -16
  128. package/src/config/loader.ts +0 -6
  129. package/src/config/schema.ts +3 -1
  130. package/src/config/skills.ts +21 -2
  131. package/src/context/image-dimensions.ts +229 -0
  132. package/src/context/token-estimator.ts +75 -12
  133. package/src/context/window-manager.ts +49 -10
  134. package/src/daemon/assistant-attachments.ts +1 -13
  135. package/src/daemon/handlers/config-ingress.ts +8 -33
  136. package/src/daemon/handlers/config-slack-channel.ts +49 -46
  137. package/src/daemon/handlers/config-telegram.ts +32 -16
  138. package/src/daemon/handlers/sessions.ts +10 -24
  139. package/src/daemon/handlers/shared.ts +0 -130
  140. package/src/daemon/host-cu-proxy.ts +401 -0
  141. package/src/daemon/lifecycle.ts +36 -68
  142. package/src/daemon/message-protocol.ts +3 -0
  143. package/src/daemon/message-types/computer-use.ts +2 -119
  144. package/src/daemon/message-types/host-cu.ts +19 -0
  145. package/src/daemon/message-types/messages.ts +3 -0
  146. package/src/daemon/server.ts +14 -21
  147. package/src/daemon/session-agent-loop-handlers.ts +2 -0
  148. package/src/daemon/session-attachments.ts +1 -2
  149. package/src/daemon/session-slash.ts +1 -1
  150. package/src/daemon/session-surfaces.ts +40 -28
  151. package/src/daemon/session-tool-setup.ts +2 -9
  152. package/src/daemon/session.ts +138 -15
  153. package/src/daemon/tool-side-effects.ts +2 -8
  154. package/src/daemon/watch-handler.ts +2 -2
  155. package/src/events/tool-metrics-listener.ts +2 -2
  156. package/src/hooks/manager.ts +1 -4
  157. package/src/inbound/public-ingress-urls.ts +7 -7
  158. package/src/logfire.ts +16 -5
  159. package/src/memory/conversation-key-store.ts +21 -0
  160. package/src/memory/db-init.ts +4 -0
  161. package/src/memory/migrations/149-oauth-tables.ts +60 -0
  162. package/src/memory/migrations/index.ts +1 -0
  163. package/src/memory/schema/index.ts +1 -0
  164. package/src/memory/schema/oauth.ts +65 -0
  165. package/src/messaging/provider.ts +4 -4
  166. package/src/messaging/providers/gmail/client.ts +82 -2
  167. package/src/messaging/providers/gmail/people-client.ts +10 -10
  168. package/src/messaging/providers/telegram-bot/adapter.ts +17 -17
  169. package/src/messaging/providers/whatsapp/adapter.ts +11 -8
  170. package/src/messaging/registry.ts +2 -32
  171. package/src/notifications/copy-composer.ts +0 -5
  172. package/src/notifications/signal.ts +4 -5
  173. package/src/oauth/byo-connection.test.ts +126 -25
  174. package/src/oauth/byo-connection.ts +22 -6
  175. package/src/oauth/connect-orchestrator.ts +113 -57
  176. package/src/oauth/connect-types.ts +17 -23
  177. package/src/oauth/connection-resolver.ts +35 -11
  178. package/src/oauth/connection.ts +1 -1
  179. package/src/oauth/manual-token-connection.ts +104 -0
  180. package/src/oauth/oauth-store.ts +496 -0
  181. package/src/oauth/platform-connection.test.ts +29 -0
  182. package/src/oauth/platform-connection.ts +6 -5
  183. package/src/oauth/provider-behaviors.ts +124 -0
  184. package/src/oauth/scope-policy.ts +9 -2
  185. package/src/oauth/seed-providers.ts +161 -0
  186. package/src/oauth/token-persistence.ts +74 -78
  187. package/src/permissions/checker.ts +3 -3
  188. package/src/permissions/defaults.ts +0 -1
  189. package/src/permissions/prompter.ts +10 -1
  190. package/src/permissions/trust-store.ts +13 -0
  191. package/src/prompts/__tests__/build-cli-reference-section.test.ts +3 -1
  192. package/src/prompts/system-prompt.ts +28 -40
  193. package/src/providers/anthropic/client.ts +133 -24
  194. package/src/providers/retry.ts +1 -27
  195. package/src/runtime/auth/route-policy.ts +0 -3
  196. package/src/runtime/channel-reply-delivery.ts +0 -40
  197. package/src/runtime/gateway-client.ts +0 -7
  198. package/src/runtime/http-server.ts +8 -6
  199. package/src/runtime/http-types.ts +2 -2
  200. package/src/runtime/middleware/twilio-validation.ts +1 -11
  201. package/src/runtime/pending-interactions.ts +14 -12
  202. package/src/runtime/routes/channel-delivery-routes.ts +0 -1
  203. package/src/runtime/routes/conversation-routes.ts +73 -19
  204. package/src/runtime/routes/events-routes.ts +21 -11
  205. package/src/runtime/routes/host-cu-routes.ts +97 -0
  206. package/src/runtime/routes/inbound-stages/background-dispatch.ts +12 -111
  207. package/src/runtime/routes/integrations/slack/share.ts +6 -7
  208. package/src/runtime/routes/log-export-routes.ts +126 -8
  209. package/src/runtime/routes/settings-routes.ts +55 -48
  210. package/src/runtime/routes/surface-action-routes.ts +1 -1
  211. package/src/runtime/routes/watch-routes.ts +128 -0
  212. package/src/schedule/integration-status.ts +10 -9
  213. package/src/security/credential-key.ts +0 -156
  214. package/src/security/keychain-broker-client.ts +5 -6
  215. package/src/security/oauth2.ts +1 -1
  216. package/src/security/token-manager.ts +119 -46
  217. package/src/skills/catalog-install.ts +358 -0
  218. package/src/skills/include-graph.ts +32 -0
  219. package/src/telegram/bot-username.ts +2 -3
  220. package/src/tools/browser/network-recorder.ts +1 -1
  221. package/src/tools/browser/network-recording-types.ts +1 -1
  222. package/src/tools/computer-use/definitions.ts +46 -11
  223. package/src/tools/computer-use/registry.ts +4 -5
  224. package/src/tools/credentials/broker.ts +1 -2
  225. package/src/tools/credentials/metadata-store.ts +17 -121
  226. package/src/tools/credentials/vault.ts +94 -167
  227. package/src/tools/registry.ts +2 -7
  228. package/src/tools/skills/load.ts +62 -3
  229. package/src/tools/watch/watch-state.ts +0 -12
  230. package/src/util/logger.ts +7 -41
  231. package/src/util/platform.ts +9 -28
  232. package/src/watcher/providers/google-calendar.ts +2 -1
  233. package/src/__tests__/computer-use-session-compaction.test.ts +0 -143
  234. package/src/__tests__/computer-use-session-lifecycle.test.ts +0 -322
  235. package/src/__tests__/computer-use-session-working-dir.test.ts +0 -166
  236. package/src/__tests__/computer-use-skill-baseline.test.ts +0 -78
  237. package/src/__tests__/computer-use-skill-endstate.test.ts +0 -105
  238. package/src/__tests__/computer-use-skill-lifecycle-cleanup.test.ts +0 -249
  239. package/src/__tests__/ride-shotgun-handler.test.ts +0 -452
  240. package/src/cli/commands/dev.ts +0 -129
  241. package/src/cli/commands/map.ts +0 -391
  242. package/src/cli/commands/oauth.ts +0 -77
  243. package/src/config/bundled-skills/computer-use/tools/computer-use-request-control.ts +0 -16
  244. package/src/daemon/computer-use-session.ts +0 -1026
  245. package/src/daemon/ride-shotgun-handler.ts +0 -569
  246. package/src/oauth/provider-base-urls.ts +0 -21
  247. package/src/oauth/provider-profiles.ts +0 -192
  248. package/src/prompts/computer-use-prompt.ts +0 -98
  249. package/src/runtime/routes/computer-use-routes.ts +0 -641
  250. package/src/runtime/telegram-streaming-delivery.test.ts +0 -729
  251. package/src/runtime/telegram-streaming-delivery.ts +0 -393
  252. package/src/tools/computer-use/request-computer-control.ts +0 -56
@@ -60,10 +60,16 @@ mock.module("../util/logger.js", () => ({
60
60
  new Proxy({} as Record<string, unknown>, {
61
61
  get: () => () => {},
62
62
  }),
63
- isDebug: () => false,
64
63
  truncateForLog: (s: unknown) => String(s),
65
64
  }));
66
65
 
66
+ // Mock autoInstallFromCatalog — default returns false (not found in catalog).
67
+ // Tests can override via `mockAutoInstall.mockImplementation(...)`.
68
+ const mockAutoInstall = mock((_skillId: string) => Promise.resolve(false));
69
+ mock.module("../skills/catalog-install.js", () => ({
70
+ autoInstallFromCatalog: (skillId: string) => mockAutoInstall(skillId),
71
+ }));
72
+
67
73
  await import("../tools/skills/load.js");
68
74
  const { getTool } = await import("../tools/registry.js");
69
75
 
@@ -145,6 +151,10 @@ async function executeSkillLoad(
145
151
  describe("skill_load tool", () => {
146
152
  beforeEach(() => {
147
153
  mkdirSync(join(TEST_DIR, "skills"), { recursive: true });
154
+ mockAutoInstall.mockReset();
155
+ mockAutoInstall.mockImplementation((_skillId: string) =>
156
+ Promise.resolve(false),
157
+ );
148
158
  });
149
159
 
150
160
  afterEach(() => {
@@ -850,4 +860,142 @@ describe("skill_load tool", () => {
850
860
  "Use `skill_execute` to call these tools.",
851
861
  );
852
862
  });
863
+
864
+ test("auto-installs missing includes from catalog", async () => {
865
+ // Parent includes "dep-a" which is not initially in the catalog
866
+ writeSkillWithIncludes(
867
+ "auto-parent",
868
+ "Auto Parent",
869
+ "Has auto-installable dep",
870
+ "Parent body",
871
+ ["dep-a"],
872
+ );
873
+ writeFileSync(join(TEST_DIR, "skills", "SKILLS.md"), "- auto-parent\n");
874
+
875
+ // Mock autoInstallFromCatalog to succeed and write the skill to disk
876
+ mockAutoInstall.mockImplementation((skillId: string) => {
877
+ if (skillId === "dep-a") {
878
+ writeSkill("dep-a", "Dep A", "A dependency", "Dep A body");
879
+ // Add to SKILLS.md so catalog reload finds it
880
+ writeFileSync(
881
+ join(TEST_DIR, "skills", "SKILLS.md"),
882
+ "- auto-parent\n- dep-a\n",
883
+ );
884
+ return Promise.resolve(true);
885
+ }
886
+ return Promise.resolve(false);
887
+ });
888
+
889
+ const result = await executeSkillLoad({ skill: "auto-parent" });
890
+ expect(result.isError).toBe(false);
891
+ expect(result.content).toContain("Skill: Auto Parent");
892
+ expect(result.content).toContain("<loaded_skill");
893
+ expect(mockAutoInstall).toHaveBeenCalledWith("dep-a");
894
+ });
895
+
896
+ test("auto-installs transitive missing includes across rounds", async () => {
897
+ // Skill A includes B, B includes C. Neither B nor C in initial catalog.
898
+ writeSkillWithIncludes("trans-a", "Trans A", "Top level", "Body A", [
899
+ "trans-b",
900
+ ]);
901
+ writeFileSync(join(TEST_DIR, "skills", "SKILLS.md"), "- trans-a\n");
902
+
903
+ let round = 0;
904
+ mockAutoInstall.mockImplementation((skillId: string) => {
905
+ if (skillId === "trans-b" && round === 0) {
906
+ // First round: install B (which includes C)
907
+ writeSkillWithIncludes("trans-b", "Trans B", "Mid level", "Body B", [
908
+ "trans-c",
909
+ ]);
910
+ writeFileSync(
911
+ join(TEST_DIR, "skills", "SKILLS.md"),
912
+ "- trans-a\n- trans-b\n",
913
+ );
914
+ round++;
915
+ return Promise.resolve(true);
916
+ }
917
+ if (skillId === "trans-c") {
918
+ // Second round: install C
919
+ writeSkill("trans-c", "Trans C", "Leaf", "Body C");
920
+ writeFileSync(
921
+ join(TEST_DIR, "skills", "SKILLS.md"),
922
+ "- trans-a\n- trans-b\n- trans-c\n",
923
+ );
924
+ return Promise.resolve(true);
925
+ }
926
+ return Promise.resolve(false);
927
+ });
928
+
929
+ const result = await executeSkillLoad({ skill: "trans-a" });
930
+ expect(result.isError).toBe(false);
931
+ expect(result.content).toContain("Skill: Trans A");
932
+ expect(result.content).toContain("<loaded_skill");
933
+ expect(mockAutoInstall).toHaveBeenCalledWith("trans-b");
934
+ expect(mockAutoInstall).toHaveBeenCalledWith("trans-c");
935
+ });
936
+
937
+ test("returns error when auto-install of missing include fails", async () => {
938
+ writeSkillWithIncludes(
939
+ "fail-parent",
940
+ "Fail Parent",
941
+ "Has failing dep",
942
+ "Body",
943
+ ["dep-x"],
944
+ );
945
+ writeFileSync(join(TEST_DIR, "skills", "SKILLS.md"), "- fail-parent\n");
946
+
947
+ // autoInstallFromCatalog throws an error
948
+ mockAutoInstall.mockImplementation((skillId: string) => {
949
+ if (skillId === "dep-x") {
950
+ return Promise.reject(new Error("Network error"));
951
+ }
952
+ return Promise.resolve(false);
953
+ });
954
+
955
+ const result = await executeSkillLoad({ skill: "fail-parent" });
956
+ expect(result.isError).toBe(true);
957
+ expect(result.content).toContain("dep-x");
958
+ expect(result.content).toContain("not found");
959
+ expect(result.content).not.toContain("<loaded_skill");
960
+ });
961
+
962
+ test("stops after MAX_INSTALL_ROUNDS", async () => {
963
+ // Pathological case: each install round reveals a new missing dep
964
+ writeSkillWithIncludes("loop-root", "Loop Root", "Infinite deps", "Body", [
965
+ "loop-dep-0",
966
+ ]);
967
+ writeFileSync(join(TEST_DIR, "skills", "SKILLS.md"), "- loop-root\n");
968
+
969
+ let installCount = 0;
970
+ mockAutoInstall.mockImplementation((skillId: string) => {
971
+ const id = skillId;
972
+ if (id.startsWith("loop-dep-")) {
973
+ installCount++;
974
+ const nextDepId = `loop-dep-${installCount}`;
975
+ // Install the requested dep, but it includes yet another missing dep
976
+ writeSkillWithIncludes(
977
+ id,
978
+ `Loop Dep ${installCount}`,
979
+ "Generated dep",
980
+ "Body",
981
+ [nextDepId],
982
+ );
983
+ // Update SKILLS.md to include all installed deps so far
984
+ const entries = ["- loop-root\n"];
985
+ for (let i = 0; i < installCount; i++) {
986
+ entries.push(`- loop-dep-${i}\n`);
987
+ }
988
+ writeFileSync(join(TEST_DIR, "skills", "SKILLS.md"), entries.join(""));
989
+ return Promise.resolve(true);
990
+ }
991
+ return Promise.resolve(false);
992
+ });
993
+
994
+ const result = await executeSkillLoad({ skill: "loop-root" });
995
+ // Should terminate with an error (the final dep is still missing)
996
+ expect(result.isError).toBe(true);
997
+ expect(result.content).toContain("not found");
998
+ // Should have terminated — installCount should be bounded by MAX_INSTALL_ROUNDS (5)
999
+ expect(installCount).toBeLessThanOrEqual(5);
1000
+ });
853
1001
  });
@@ -210,7 +210,6 @@ mock.module("../util/logger.js", () => ({
210
210
  debug: () => {},
211
211
  error: () => {},
212
212
  }),
213
- isDebug: () => false,
214
213
  }));
215
214
 
216
215
  // ---------------------------------------------------------------------------
@@ -9,7 +9,7 @@ import { tmpdir } from "node:os";
9
9
  import { join } from "node:path";
10
10
  import { afterEach, beforeEach, describe, expect, test } from "bun:test";
11
11
 
12
- import { uninstallSkillLocally } from "../cli/commands/skills.js";
12
+ import { uninstallSkillLocally } from "../skills/catalog-install.js";
13
13
 
14
14
  let tempDir: string;
15
15
  let originalBaseDataDir: string | undefined;
@@ -60,7 +60,6 @@ mock.module("../util/logger.js", () => ({
60
60
  ...realLogger,
61
61
  getLogger: () => noopLogger,
62
62
  getCliLogger: () => noopLogger,
63
- isDebug: () => false,
64
63
  truncateForLog: (v: string) => v,
65
64
  initLogger: () => {},
66
65
  pruneOldLogFiles: () => 0,
@@ -719,15 +718,16 @@ describe("bundled computer-use skill", () => {
719
718
  expect(cuSkill!.disableModelInvocation).toBe(true);
720
719
  });
721
720
 
722
- test("computer-use skill has a valid tool manifest with 12 tools", () => {
721
+ test("computer-use skill has a valid tool manifest with 11 tools", () => {
723
722
  const catalog = loadSkillCatalog();
724
723
  const cuSkill = catalog.find((s) => s.id === "computer-use");
725
724
  expect(cuSkill).toBeDefined();
726
725
  expect(cuSkill!.toolManifest).toBeDefined();
727
726
  expect(cuSkill!.toolManifest!.present).toBe(true);
728
727
  expect(cuSkill!.toolManifest!.valid).toBe(true);
729
- expect(cuSkill!.toolManifest!.toolCount).toBe(10);
728
+ expect(cuSkill!.toolManifest!.toolCount).toBe(11);
730
729
  expect(cuSkill!.toolManifest!.toolNames).toEqual([
730
+ "computer_use_observe",
731
731
  "computer_use_click",
732
732
  "computer_use_type_text",
733
733
  "computer_use_key",
@@ -75,13 +75,11 @@ mock.module("../util/logger.js", () => ({
75
75
  debug: () => {},
76
76
  trace: () => {},
77
77
  fatal: () => {},
78
- isDebug: () => false,
79
78
  child: () => ({
80
79
  info: () => {},
81
80
  warn: () => {},
82
81
  error: () => {},
83
82
  debug: () => {},
84
- isDebug: () => false,
85
83
  }),
86
84
  }),
87
85
  }));
@@ -116,6 +114,46 @@ mock.module("../security/secure-keys.js", () => {
116
114
  };
117
115
  });
118
116
 
117
+ // Mock oauth-store (getConnectionByProvider)
118
+ let oauthConnectionStore: Record<
119
+ string,
120
+ { id: string; status: string; accountInfo?: string | null }
121
+ > = {};
122
+
123
+ mock.module("../oauth/oauth-store.js", () => ({
124
+ getConnectionByProvider: (providerKey: string) =>
125
+ oauthConnectionStore[providerKey] ?? undefined,
126
+ createConnection: () => ({ id: "test-conn-id" }),
127
+ updateConnection: () => true,
128
+ deleteConnection: (id: string) => {
129
+ for (const [key, conn] of Object.entries(oauthConnectionStore)) {
130
+ if (conn.id === id) {
131
+ delete oauthConnectionStore[key];
132
+ return true;
133
+ }
134
+ }
135
+ return false;
136
+ },
137
+ upsertApp: async () => ({ id: "test-app-id" }),
138
+ }));
139
+
140
+ // Mock manual-token-connection
141
+ mock.module("../oauth/manual-token-connection.js", () => ({
142
+ ensureManualTokenConnection: async (
143
+ providerKey: string,
144
+ accountInfo?: string,
145
+ ) => {
146
+ oauthConnectionStore[providerKey] = {
147
+ id: `conn-${providerKey}`,
148
+ status: "active",
149
+ accountInfo: accountInfo ?? null,
150
+ };
151
+ },
152
+ removeManualTokenConnection: (providerKey: string) => {
153
+ delete oauthConnectionStore[providerKey];
154
+ },
155
+ }));
156
+
119
157
  // Mock credential metadata store
120
158
  let credentialMetadataStore: Array<{
121
159
  service: string;
@@ -187,6 +225,7 @@ describe("Slack channel config handler", () => {
187
225
  beforeEach(() => {
188
226
  secureKeyStore = {};
189
227
  credentialMetadataStore = [];
228
+ oauthConnectionStore = {};
190
229
  configStore = {};
191
230
  globalThis.fetch = originalFetch;
192
231
  });
@@ -199,7 +238,11 @@ describe("Slack channel config handler", () => {
199
238
  expect(result.connected).toBe(false);
200
239
  });
201
240
 
202
- test("GET returns connected: true when both tokens are set", () => {
241
+ test("GET returns connected: true when oauth_connection is active and both keys exist", () => {
242
+ oauthConnectionStore["slack_channel"] = {
243
+ id: "conn-slack",
244
+ status: "active",
245
+ };
203
246
  secureKeyStore[credentialKey("slack_channel", "bot_token")] = "xoxb-test";
204
247
  secureKeyStore[credentialKey("slack_channel", "app_token")] = "xapp-test";
205
248
 
@@ -210,8 +253,29 @@ describe("Slack channel config handler", () => {
210
253
  expect(result.connected).toBe(true);
211
254
  });
212
255
 
256
+ test("GET reports per-field token presence independently of connection row", () => {
257
+ // Only bot_token in keychain, no app_token, but connection row exists
258
+ oauthConnectionStore["slack_channel"] = {
259
+ id: "conn-slack",
260
+ status: "active",
261
+ };
262
+ secureKeyStore[credentialKey("slack_channel", "bot_token")] = "xoxb-test";
263
+
264
+ const result = getSlackChannelConfig();
265
+ expect(result.success).toBe(true);
266
+ expect(result.hasBotToken).toBe(true);
267
+ expect(result.hasAppToken).toBe(false);
268
+ // connected requires both keys AND connection row
269
+ expect(result.connected).toBe(false);
270
+ });
271
+
213
272
  test("GET returns metadata from config when available", () => {
273
+ oauthConnectionStore["slack_channel"] = {
274
+ id: "conn-slack",
275
+ status: "active",
276
+ };
214
277
  secureKeyStore[credentialKey("slack_channel", "bot_token")] = "xoxb-test";
278
+ secureKeyStore[credentialKey("slack_channel", "app_token")] = "xapp-test";
215
279
  configStore = {
216
280
  slack: {
217
281
  teamId: "T123",
@@ -1,7 +1,5 @@
1
1
  import { beforeEach, describe, expect, mock, test } from "bun:test";
2
2
 
3
- import { credentialKey } from "../security/credential-key.js";
4
-
5
3
  // ---------------------------------------------------------------------------
6
4
  // Mocks — must be declared before any imports that pull in mocked modules
7
5
  // ---------------------------------------------------------------------------
@@ -12,6 +10,12 @@ mock.module("../security/secure-keys.js", () => ({
12
10
  setSecureKeyAsync: async () => {},
13
11
  }));
14
12
 
13
+ let connectionByProvider: Record<string, unknown> = {};
14
+ mock.module("../oauth/oauth-store.js", () => ({
15
+ getConnectionByProvider: (key: string) =>
16
+ connectionByProvider[key] ?? undefined,
17
+ }));
18
+
15
19
  let listConversationsResult: unknown = { ok: true, channels: [] };
16
20
  let postMessageResult: unknown = {
17
21
  ok: true,
@@ -86,6 +90,7 @@ function makeRequest(body: unknown): Request {
86
90
 
87
91
  beforeEach(() => {
88
92
  secureKeyValues.clear();
93
+ connectionByProvider = {};
89
94
  listConversationsResult = { ok: true, channels: [] };
90
95
  userInfoResults = new Map();
91
96
  appStoreResult = null;
@@ -106,8 +111,9 @@ describe("handleListSlackChannels", () => {
106
111
  });
107
112
 
108
113
  test("returns channels sorted by type then name", async () => {
114
+ connectionByProvider["integration:slack"] = { id: "conn-slack-1" };
109
115
  secureKeyValues.set(
110
- credentialKey("integration:slack", "access_token"),
116
+ "oauth_connection/conn-slack-1/access_token",
111
117
  "xoxb-test",
112
118
  );
113
119
 
@@ -176,18 +182,6 @@ describe("handleListSlackChannels", () => {
176
182
  isPrivate: true,
177
183
  });
178
184
  });
179
-
180
- test("falls back to legacy bot token", async () => {
181
- secureKeyValues.set(
182
- credentialKey("slack_channel", "bot_token"),
183
- "xoxb-legacy",
184
- );
185
-
186
- listConversationsResult = { ok: true, channels: [] };
187
-
188
- const res = await handleListSlackChannels();
189
- expect(res.status).toBe(200);
190
- });
191
185
  });
192
186
 
193
187
  describe("handleShareToSlackChannel", () => {
@@ -198,8 +192,9 @@ describe("handleShareToSlackChannel", () => {
198
192
  });
199
193
 
200
194
  test("returns 400 for malformed JSON", async () => {
195
+ connectionByProvider["integration:slack"] = { id: "conn-slack-1" };
201
196
  secureKeyValues.set(
202
- credentialKey("integration:slack", "access_token"),
197
+ "oauth_connection/conn-slack-1/access_token",
203
198
  "xoxb-test",
204
199
  );
205
200
  const req = new Request("http://localhost/v1/slack/share", {
@@ -212,8 +207,9 @@ describe("handleShareToSlackChannel", () => {
212
207
  });
213
208
 
214
209
  test("returns 400 when missing required fields", async () => {
210
+ connectionByProvider["integration:slack"] = { id: "conn-slack-1" };
215
211
  secureKeyValues.set(
216
- credentialKey("integration:slack", "access_token"),
212
+ "oauth_connection/conn-slack-1/access_token",
217
213
  "xoxb-test",
218
214
  );
219
215
  const req = makeRequest({ appId: "app1" });
@@ -224,8 +220,9 @@ describe("handleShareToSlackChannel", () => {
224
220
  });
225
221
 
226
222
  test("returns 404 when app not found", async () => {
223
+ connectionByProvider["integration:slack"] = { id: "conn-slack-1" };
227
224
  secureKeyValues.set(
228
- credentialKey("integration:slack", "access_token"),
225
+ "oauth_connection/conn-slack-1/access_token",
229
226
  "xoxb-test",
230
227
  );
231
228
  appStoreResult = null;
@@ -235,8 +232,9 @@ describe("handleShareToSlackChannel", () => {
235
232
  });
236
233
 
237
234
  test("posts message and returns success", async () => {
235
+ connectionByProvider["integration:slack"] = { id: "conn-slack-1" };
238
236
  secureKeyValues.set(
239
- credentialKey("integration:slack", "access_token"),
237
+ "oauth_connection/conn-slack-1/access_token",
240
238
  "xoxb-test",
241
239
  );
242
240
  appStoreResult = {
@@ -53,7 +53,6 @@ mock.module("../util/logger.js", () => ({
53
53
  ...realLogger,
54
54
  getLogger: () => noopLogger,
55
55
  getCliLogger: () => noopLogger,
56
- isDebug: () => false,
57
56
  truncateForLog: (v: string) => v,
58
57
  initLogger: () => {},
59
58
  pruneOldLogFiles: () => 0,
@@ -7,44 +7,42 @@
7
7
  import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
8
8
 
9
9
  import type { ChannelId } from "../channels/types.js";
10
- import { telegramInviteAdapter } from "../runtime/channel-invite-transports/telegram.js";
11
10
 
12
11
  // Mock credential metadata so tests don't depend on local persisted state.
13
12
  mock.module("../tools/credentials/metadata-store.js", () => ({
14
13
  getCredentialMetadata: () => undefined,
15
14
  }));
16
15
 
16
+ // Mock getTelegramBotUsername — the env var fallback was removed so we
17
+ // control the return value directly via a mutable variable.
18
+ let mockBotUsername: string | undefined;
19
+ mock.module("../telegram/bot-username.js", () => ({
20
+ getTelegramBotUsername: () => mockBotUsername,
21
+ }));
22
+
23
+ import { telegramInviteAdapter } from "../runtime/channel-invite-transports/telegram.js";
24
+
17
25
  // ---------------------------------------------------------------------------
18
26
  // Helpers
19
27
  // ---------------------------------------------------------------------------
20
28
 
21
29
  const CHANNEL: ChannelId = "telegram" as ChannelId;
22
30
 
23
- function setEnv(key: string, value: string | undefined) {
24
- if (value === undefined) {
25
- delete process.env[key];
26
- } else {
27
- process.env[key] = value;
28
- }
29
- }
30
-
31
31
  // ---------------------------------------------------------------------------
32
32
  // buildShareLink
33
33
  // ---------------------------------------------------------------------------
34
34
 
35
35
  describe("telegramInviteAdapter.buildShareLink", () => {
36
- let originalBotUsername: string | undefined;
37
-
38
36
  beforeEach(() => {
39
- originalBotUsername = process.env.TELEGRAM_BOT_USERNAME;
37
+ mockBotUsername = undefined;
40
38
  });
41
39
 
42
40
  afterEach(() => {
43
- setEnv("TELEGRAM_BOT_USERNAME", originalBotUsername);
41
+ mockBotUsername = undefined;
44
42
  });
45
43
 
46
44
  test("builds a deep link with iv_ prefix", () => {
47
- setEnv("TELEGRAM_BOT_USERNAME", "TestBot");
45
+ mockBotUsername = "TestBot";
48
46
 
49
47
  const link = telegramInviteAdapter.buildShareLink!({
50
48
  rawToken: "abc123",
@@ -56,7 +54,7 @@ describe("telegramInviteAdapter.buildShareLink", () => {
56
54
  });
57
55
 
58
56
  test("throws when bot username is not configured", () => {
59
- setEnv("TELEGRAM_BOT_USERNAME", undefined);
57
+ mockBotUsername = undefined;
60
58
 
61
59
  expect(() =>
62
60
  telegramInviteAdapter.buildShareLink!({
@@ -138,25 +136,23 @@ describe("telegramInviteAdapter.extractInboundToken", () => {
138
136
  // ---------------------------------------------------------------------------
139
137
 
140
138
  describe("telegramInviteAdapter.resolveChannelHandle", () => {
141
- let originalBotUsername: string | undefined;
142
-
143
139
  beforeEach(() => {
144
- originalBotUsername = process.env.TELEGRAM_BOT_USERNAME;
140
+ mockBotUsername = undefined;
145
141
  });
146
142
 
147
143
  afterEach(() => {
148
- setEnv("TELEGRAM_BOT_USERNAME", originalBotUsername);
144
+ mockBotUsername = undefined;
149
145
  });
150
146
 
151
- test("returns @-prefixed bot username from env", () => {
152
- setEnv("TELEGRAM_BOT_USERNAME", "MyBot");
147
+ test("returns @-prefixed bot username from config", () => {
148
+ mockBotUsername = "MyBot";
153
149
 
154
150
  const handle = telegramInviteAdapter.resolveChannelHandle!();
155
151
  expect(handle).toBe("@MyBot");
156
152
  });
157
153
 
158
154
  test("returns undefined when bot username is not configured", () => {
159
- setEnv("TELEGRAM_BOT_USERNAME", undefined);
155
+ mockBotUsername = undefined;
160
156
 
161
157
  const handle = telegramInviteAdapter.resolveChannelHandle!();
162
158
  expect(handle).toBeUndefined();
@@ -457,10 +457,10 @@ describe("buildSanitizedEnv", () => {
457
457
  });
458
458
 
459
459
  test("injects INTERNAL_GATEWAY_BASE_URL from gateway config", () => {
460
- process.env.GATEWAY_INTERNAL_BASE_URL = "http://gateway.internal:9000/";
460
+ process.env.GATEWAY_PORT = "9000";
461
461
  const env = buildSanitizedEnv();
462
- expect(env.INTERNAL_GATEWAY_BASE_URL).toBe("http://gateway.internal:9000");
463
- delete process.env.GATEWAY_INTERNAL_BASE_URL;
462
+ expect(env.INTERNAL_GATEWAY_BASE_URL).toBe("http://127.0.0.1:9000");
463
+ delete process.env.GATEWAY_PORT;
464
464
  });
465
465
 
466
466
  test("result is a plain object with no prototype-inherited secrets", () => {
@@ -485,6 +485,7 @@ describe("buildSanitizedEnv", () => {
485
485
  "SSH_AGENT_PID",
486
486
  "GPG_TTY",
487
487
  "GNUPGHOME",
488
+ "VELLUM_DEV",
488
489
  "INTERNAL_GATEWAY_BASE_URL",
489
490
  "VELLUM_DATA_DIR",
490
491
  ];
@@ -2,8 +2,9 @@
2
2
  * Reusable constants and helpers for the computer-use skill migration test suite.
3
3
  */
4
4
 
5
- /** The 10 computer_use_* action tool names provided by the bundled computer-use skill. */
5
+ /** The 11 computer_use_* action tool names provided by the bundled computer-use skill. */
6
6
  export const COMPUTER_USE_TOOL_NAMES = [
7
+ "computer_use_observe",
7
8
  "computer_use_click",
8
9
  "computer_use_type_text",
9
10
  "computer_use_key",
@@ -20,7 +21,7 @@ export const COMPUTER_USE_TOOL_NAMES = [
20
21
  export const COMPUTER_USE_SKILL_ID = "computer-use";
21
22
 
22
23
  /** Number of computer_use_* tools. */
23
- export const COMPUTER_USE_TOOL_COUNT = COMPUTER_USE_TOOL_NAMES.length; // 10
24
+ export const COMPUTER_USE_TOOL_COUNT = COMPUTER_USE_TOOL_NAMES.length; // 11
24
25
 
25
26
  import { expect } from "bun:test";
26
27
 
@@ -21,7 +21,6 @@ mock.module("../util/logger.js", () => ({
21
21
  new Proxy({} as Record<string, unknown>, {
22
22
  get: () => () => {},
23
23
  }),
24
- isDebug: () => false,
25
24
  truncateForLog: (value: string) => value,
26
25
  }));
27
26
 
@@ -44,7 +44,6 @@ mock.module("../util/logger.js", () => ({
44
44
  new Proxy({} as Record<string, unknown>, {
45
45
  get: () => () => {},
46
46
  }),
47
- isDebug: () => false,
48
47
  }));
49
48
 
50
49
  // Allow toggling between no-rule and matched-rule paths
@@ -59,7 +59,6 @@ mock.module("../util/logger.js", () => ({
59
59
  new Proxy({} as Record<string, unknown>, {
60
60
  get: () => () => {},
61
61
  }),
62
- isDebug: () => false,
63
62
  truncateForLog: (value: string) => value,
64
63
  }));
65
64
 
@@ -68,7 +68,6 @@ mock.module("../util/logger.js", () => ({
68
68
  new Proxy({} as Record<string, unknown>, {
69
69
  get: () => () => {},
70
70
  }),
71
- isDebug: () => false,
72
71
  truncateForLog: (value: string) => value,
73
72
  }));
74
73
 
@@ -103,7 +103,6 @@ mock.module("../util/logger.js", () => ({
103
103
  new Proxy({} as Record<string, unknown>, {
104
104
  get: () => () => {},
105
105
  }),
106
- isDebug: () => false,
107
106
  truncateForLog: (value: string) => value,
108
107
  }));
109
108
 
@@ -31,7 +31,6 @@ mock.module("../util/logger.js", () => ({
31
31
  new Proxy({} as Record<string, unknown>, {
32
32
  get: () => () => {},
33
33
  }),
34
- isDebug: () => false,
35
34
  truncateForLog: (value: string) => value,
36
35
  }));
37
36
 
@@ -0,0 +1,29 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import { patternMatchesCandidate } from "../permissions/trust-store.js";
4
+
5
+ describe("patternMatchesCandidate", () => {
6
+ test("exact match", () => {
7
+ expect(patternMatchesCandidate("bash:git commit", "bash:git commit")).toBe(
8
+ true,
9
+ );
10
+ });
11
+
12
+ test("glob match", () => {
13
+ expect(patternMatchesCandidate("bash:git *", "bash:git commit")).toBe(true);
14
+ });
15
+
16
+ test("no match", () => {
17
+ expect(patternMatchesCandidate("bash:git *", "file_write:/foo")).toBe(
18
+ false,
19
+ );
20
+ });
21
+
22
+ test("globstar matches anything", () => {
23
+ expect(patternMatchesCandidate("**", "bash:anything")).toBe(true);
24
+ });
25
+
26
+ test("invalid pattern returns false", () => {
27
+ expect(patternMatchesCandidate("[", "bash:anything")).toBe(false);
28
+ });
29
+ });