@vellumai/assistant 0.5.15 → 0.5.16

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 (175) hide show
  1. package/ARCHITECTURE.md +2 -2
  2. package/docs/architecture/integrations.md +15 -14
  3. package/knip.json +3 -1
  4. package/openapi.yaml +11 -43
  5. package/package.json +1 -1
  6. package/src/__tests__/assistant-feature-flags-integration.test.ts +3 -375
  7. package/src/__tests__/ces-rpc-credential-backend.test.ts +4 -1
  8. package/src/__tests__/checker.test.ts +59 -0
  9. package/src/__tests__/cli-command-risk-guard.test.ts +98 -10
  10. package/src/__tests__/cli-memory.test.ts +372 -0
  11. package/src/__tests__/computer-use-skill-manifest-regression.test.ts +12 -2
  12. package/src/__tests__/config-schema.test.ts +0 -2
  13. package/src/__tests__/config-watcher-feature-flags.test.ts +211 -0
  14. package/src/__tests__/conversation-runtime-assembly.test.ts +7 -4
  15. package/src/__tests__/conversation-slash-commands.test.ts +2 -6
  16. package/src/__tests__/conversation-usage.test.ts +1 -0
  17. package/src/__tests__/credential-security-e2e.test.ts +4 -1
  18. package/src/__tests__/docker-signing-key-bootstrap.test.ts +7 -73
  19. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +6 -7
  20. package/src/__tests__/guardian-routing-invariants.test.ts +151 -0
  21. package/src/__tests__/heartbeat-service.test.ts +1 -3
  22. package/src/__tests__/intent-routing.test.ts +6 -18
  23. package/src/__tests__/log-export-workspace.test.ts +2 -28
  24. package/src/__tests__/managed-skill-lifecycle.test.ts +7 -37
  25. package/src/__tests__/managed-store.test.ts +2 -10
  26. package/src/__tests__/messaging-send-tool.test.ts +6 -6
  27. package/src/__tests__/migration-cross-version-compatibility.test.ts +1 -29
  28. package/src/__tests__/migration-export-http.test.ts +3 -34
  29. package/src/__tests__/migration-import-commit-http.test.ts +1 -29
  30. package/src/__tests__/migration-import-preflight-http.test.ts +3 -34
  31. package/src/__tests__/no-domain-routing-in-prompt-guard.test.ts +2 -1
  32. package/src/__tests__/oauth-apps-routes.test.ts +120 -10
  33. package/src/__tests__/oauth-connect-orchestrator.test.ts +709 -0
  34. package/src/__tests__/oauth-provider-serializer.test.ts +2 -1
  35. package/src/__tests__/oauth-provider-visibility.test.ts +149 -0
  36. package/src/__tests__/oauth-providers-routes.test.ts +5 -2
  37. package/src/__tests__/oauth-store.test.ts +0 -5
  38. package/src/__tests__/outlook-messaging-provider.test.ts +576 -0
  39. package/src/__tests__/path-policy.test.ts +2 -17
  40. package/src/__tests__/permission-types.test.ts +0 -1
  41. package/src/__tests__/platform-callback-registration.test.ts +3 -7
  42. package/src/__tests__/provider-commit-message-generator.test.ts +0 -1
  43. package/src/__tests__/provider-error-scenarios.test.ts +0 -2
  44. package/src/__tests__/qdrant-manager.test.ts +68 -21
  45. package/src/__tests__/require-fresh-approval.test.ts +0 -1
  46. package/src/__tests__/sandbox-diagnostics.test.ts +20 -29
  47. package/src/__tests__/scaffold-managed-skill-tool.test.ts +2 -10
  48. package/src/__tests__/secret-allowlist.test.ts +20 -35
  49. package/src/__tests__/shell-credential-ref.test.ts +0 -5
  50. package/src/__tests__/skill-load-feature-flag.test.ts +2 -43
  51. package/src/__tests__/skill-load-inline-command.test.ts +3 -65
  52. package/src/__tests__/skill-load-inline-includes.test.ts +3 -65
  53. package/src/__tests__/skill-load-tool.test.ts +3 -67
  54. package/src/__tests__/skill-memory.test.ts +362 -119
  55. package/src/__tests__/skills.test.ts +22 -49
  56. package/src/__tests__/slack-channel-config.test.ts +2 -21
  57. package/src/__tests__/starter-bundle.test.ts +2 -8
  58. package/src/__tests__/stt-hints.test.ts +7 -2
  59. package/src/__tests__/system-prompt.test.ts +25 -45
  60. package/src/__tests__/task-compiler.test.ts +0 -21
  61. package/src/__tests__/task-management-tools.test.ts +0 -21
  62. package/src/__tests__/task-memory-cleanup.test.ts +0 -21
  63. package/src/__tests__/task-runner.test.ts +0 -21
  64. package/src/__tests__/task-scheduler.test.ts +0 -21
  65. package/src/__tests__/terminal-tools.test.ts +1 -17
  66. package/src/__tests__/token-estimator-accuracy.benchmark.test.ts +0 -79
  67. package/src/__tests__/tool-approval-handler.test.ts +1 -20
  68. package/src/__tests__/tool-execution-abort-cleanup.test.ts +2 -11
  69. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -25
  70. package/src/__tests__/tool-executor-lifecycle-events.test.ts +0 -1
  71. package/src/__tests__/tool-executor.test.ts +0 -1
  72. package/src/__tests__/tool-grant-request-escalation.test.ts +1 -20
  73. package/src/__tests__/tool-preview-lifecycle.test.ts +0 -20
  74. package/src/__tests__/trust-store.test.ts +9 -41
  75. package/src/__tests__/trusted-contact-approval-notifier.test.ts +1 -30
  76. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +1 -21
  77. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +0 -22
  78. package/src/__tests__/trusted-contact-multichannel.test.ts +0 -22
  79. package/src/__tests__/trusted-contact-verification.test.ts +0 -22
  80. package/src/__tests__/turn-boundary-resolution.test.ts +0 -28
  81. package/src/__tests__/twilio-provider.test.ts +0 -16
  82. package/src/__tests__/twilio-routes-twiml.test.ts +7 -12
  83. package/src/__tests__/twilio-routes.test.ts +0 -24
  84. package/src/__tests__/update-bulletin.test.ts +17 -89
  85. package/src/__tests__/usage-cache-backfill-migration.test.ts +0 -20
  86. package/src/__tests__/usage-routes.test.ts +0 -21
  87. package/src/__tests__/user-reference.test.ts +1 -5
  88. package/src/__tests__/vbundle-pax-and-symlink.test.ts +4 -34
  89. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +2 -53
  90. package/src/__tests__/voice-invite-redemption.test.ts +0 -21
  91. package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -24
  92. package/src/__tests__/voice-session-bridge.test.ts +0 -21
  93. package/src/__tests__/workspace-migration-009-backfill-conversation-disk-view.test.ts +2 -23
  94. package/src/__tests__/workspace-migration-012-rename-conversation-disk-view-dirs.test.ts +2 -2
  95. package/src/__tests__/workspace-migration-013-repair-conversation-disk-view.test.ts +2 -23
  96. package/src/__tests__/workspace-migration-down-functions.test.ts +0 -6
  97. package/src/acp/client-handler.ts +1 -2
  98. package/src/cli/__tests__/notifications.test.ts +0 -22
  99. package/src/cli/cli-memory.ts +176 -0
  100. package/src/cli/commands/oauth/__tests__/providers-update.test.ts +1 -1
  101. package/src/cli/commands/oauth/connect.ts +15 -0
  102. package/src/cli/commands/oauth/providers.ts +49 -42
  103. package/src/cli/commands/platform/__tests__/connect.test.ts +2 -48
  104. package/src/cli/commands/platform/__tests__/disconnect.test.ts +2 -48
  105. package/src/cli/commands/platform/__tests__/status.test.ts +0 -50
  106. package/src/config/bundled-skills/computer-use/TOOLS.json +7 -7
  107. package/src/config/bundled-skills/messaging/SKILL.md +17 -2
  108. package/src/config/bundled-skills/settings/TOOLS.json +3 -3
  109. package/src/config/feature-flag-registry.json +16 -0
  110. package/src/config/loader.ts +4 -0
  111. package/src/config/schemas/security.ts +0 -6
  112. package/src/config/schemas/services.ts +8 -0
  113. package/src/context/window-manager.ts +28 -9
  114. package/src/credential-execution/approval-bridge.ts +0 -1
  115. package/src/daemon/config-watcher.ts +51 -0
  116. package/src/daemon/conversation-agent-loop.ts +3 -2
  117. package/src/daemon/conversation-process.ts +1 -0
  118. package/src/daemon/conversation-usage.ts +1 -0
  119. package/src/daemon/handlers/skills.ts +9 -1
  120. package/src/daemon/lifecycle.ts +13 -4
  121. package/src/daemon/message-types/conversations.ts +1 -0
  122. package/src/daemon/providers-setup.ts +2 -0
  123. package/src/daemon/server.ts +26 -22
  124. package/src/events/domain-events.ts +1 -2
  125. package/src/memory/db-init.ts +9 -0
  126. package/src/memory/job-handlers/batch-extraction.ts +16 -4
  127. package/src/memory/job-handlers/embedding.test.ts +3 -27
  128. package/src/memory/job-handlers/journal-carry-forward.test.ts +1 -29
  129. package/src/memory/llm-usage-store.ts +35 -2
  130. package/src/memory/migrations/201-oauth-providers-feature-flag.ts +11 -0
  131. package/src/memory/migrations/202-drop-callback-transport-column.ts +13 -0
  132. package/src/memory/migrations/index.ts +2 -0
  133. package/src/memory/qdrant-manager.ts +26 -5
  134. package/src/memory/query-expansion.ts +1 -1
  135. package/src/memory/retriever.test.ts +22 -20
  136. package/src/memory/retriever.ts +10 -2
  137. package/src/memory/schema/oauth.ts +1 -1
  138. package/src/memory/search/mmr.ts +8 -5
  139. package/src/memory/slack-thread-store.ts +17 -0
  140. package/src/messaging/providers/outlook/adapter.ts +193 -0
  141. package/src/messaging/providers/outlook/client.ts +311 -0
  142. package/src/messaging/providers/outlook/types.ts +83 -0
  143. package/src/notifications/adapters/slack.ts +1 -1
  144. package/src/oauth/__tests__/identity-verifier.test.ts +1 -1
  145. package/src/oauth/connect-orchestrator.ts +10 -3
  146. package/src/oauth/oauth-store.ts +10 -11
  147. package/src/oauth/provider-serializer.ts +3 -0
  148. package/src/oauth/provider-visibility.ts +16 -0
  149. package/src/oauth/seed-providers.ts +49 -17
  150. package/src/permissions/checker.ts +39 -7
  151. package/src/permissions/types.ts +2 -4
  152. package/src/prompts/journal-context.ts +9 -11
  153. package/src/prompts/system-prompt.ts +3 -64
  154. package/src/prompts/templates/UPDATES.md +6 -0
  155. package/src/runtime/auth/__tests__/credential-service.test.ts +1 -27
  156. package/src/runtime/auth/__tests__/token-service.test.ts +1 -25
  157. package/src/runtime/auth/route-policy.ts +0 -4
  158. package/src/runtime/guardian-reply-router.ts +6 -2
  159. package/src/runtime/routes/conversation-query-routes.ts +2 -58
  160. package/src/runtime/routes/inbound-stages/background-dispatch.ts +43 -2
  161. package/src/runtime/routes/memory-item-routes.test.ts +0 -17
  162. package/src/runtime/routes/memory-item-routes.ts +103 -12
  163. package/src/runtime/routes/oauth-apps.ts +18 -1
  164. package/src/runtime/routes/oauth-providers.ts +13 -1
  165. package/src/runtime/routes/settings-routes.ts +1 -0
  166. package/src/runtime/routes/usage-routes.ts +19 -2
  167. package/src/runtime/routes/work-items-routes.test.ts +0 -21
  168. package/src/runtime/routes/workspace-routes.test.ts +3 -27
  169. package/src/security/secret-allowlist.ts +4 -4
  170. package/src/skills/skill-memory.ts +62 -23
  171. package/src/tools/memory/handlers.test.ts +1 -29
  172. package/src/tools/permission-checker.ts +0 -18
  173. package/src/tools/skills/skill-script-runner.ts +1 -1
  174. package/src/util/device-id.ts +3 -65
  175. package/src/workspace/git-service.ts +27 -6
@@ -432,6 +432,12 @@ describe("Permission Checker", () => {
432
432
 
433
433
  // shell commands - high risk
434
434
  describe("shell — high risk", () => {
435
+ test("assistant trust clear is high risk", async () => {
436
+ expect(
437
+ await classifyRisk("bash", { command: "assistant trust clear" }),
438
+ ).toBe(RiskLevel.High);
439
+ });
440
+
435
441
  test("sudo is high risk", async () => {
436
442
  expect(await classifyRisk("bash", { command: "sudo rm -rf /" })).toBe(
437
443
  RiskLevel.High,
@@ -2017,6 +2023,9 @@ describe("Permission Checker", () => {
2017
2023
  function ensureSkillsDir(): void {
2018
2024
  mkdirSync(join(checkerTestDir, "skills"), { recursive: true });
2019
2025
  }
2026
+ function ensureHooksDir(): void {
2027
+ mkdirSync(join(checkerTestDir, "hooks"), { recursive: true });
2028
+ }
2020
2029
 
2021
2030
  test("file_write to skill directory is High risk", async () => {
2022
2031
  ensureSkillsDir();
@@ -2147,6 +2156,56 @@ describe("Permission Checker", () => {
2147
2156
  expect(risk).toBe(RiskLevel.Low);
2148
2157
  });
2149
2158
 
2159
+ test("file_write to hooks directory is High risk", async () => {
2160
+ ensureHooksDir();
2161
+ const hookPath = join(
2162
+ checkerTestDir,
2163
+ "hooks",
2164
+ "post-tool-use",
2165
+ "hook.sh",
2166
+ );
2167
+ const risk = await classifyRisk("file_write", { path: hookPath });
2168
+ expect(risk).toBe(RiskLevel.High);
2169
+ });
2170
+
2171
+ test("file_edit of hooks config is High risk", async () => {
2172
+ ensureHooksDir();
2173
+ const configPath = join(checkerTestDir, "hooks", "config.json");
2174
+ const risk = await classifyRisk("file_edit", { path: configPath });
2175
+ expect(risk).toBe(RiskLevel.High);
2176
+ });
2177
+
2178
+ test("file_write to hooks directory prompts as High risk", async () => {
2179
+ ensureHooksDir();
2180
+ const hookPath = join(
2181
+ checkerTestDir,
2182
+ "hooks",
2183
+ "post-tool-use",
2184
+ "hook.sh",
2185
+ );
2186
+ const result = await check("file_write", { path: hookPath }, "/tmp");
2187
+ expect(result.decision).toBe("prompt");
2188
+ });
2189
+
2190
+ test("host_file_write to hooks directory is High risk", async () => {
2191
+ ensureHooksDir();
2192
+ const hookPath = join(
2193
+ checkerTestDir,
2194
+ "hooks",
2195
+ "post-tool-use",
2196
+ "hook.sh",
2197
+ );
2198
+ const risk = await classifyRisk("host_file_write", { path: hookPath });
2199
+ expect(risk).toBe(RiskLevel.High);
2200
+ });
2201
+
2202
+ test("host_file_edit of hooks config is High risk", async () => {
2203
+ ensureHooksDir();
2204
+ const configPath = join(checkerTestDir, "hooks", "config.json");
2205
+ const risk = await classifyRisk("host_file_edit", { path: configPath });
2206
+ expect(risk).toBe(RiskLevel.High);
2207
+ });
2208
+
2150
2209
  test("host_file_write to non-skill path remains Medium risk (via registry)", async () => {
2151
2210
  const normalPath = "/tmp/some-file.txt";
2152
2211
  const risk = await classifyRisk("host_file_write", { path: normalPath });
@@ -157,27 +157,55 @@ describe("CLI command risk guard: elevated assistant subcommands", () => {
157
157
  expect(risk).toBe(RiskLevel.Medium);
158
158
  });
159
159
 
160
- test("--help on elevated subcommands is Low risk (read-only)", async () => {
161
- const helpCommands = [
160
+ test("--help on non-elevated subcommands remains Low risk", async () => {
161
+ // GIVEN non-elevated subcommands with --help / -h flags
162
+ const lowRiskWithHelp = [
163
+ "assistant oauth --help",
164
+ "assistant credentials --help",
165
+ "assistant trust -h",
166
+ "assistant keys --help",
167
+ "assistant config --help",
168
+ ];
169
+
170
+ // WHEN classifying risk
171
+ // THEN they remain Low since the subcommand itself is Low
172
+ for (const command of lowRiskWithHelp) {
173
+ const risk = await classifyRisk("bash", { command });
174
+ expectLowRisk(command, risk);
175
+ }
176
+ });
177
+
178
+ test("--help does not downgrade risk on elevated subcommands", async () => {
179
+ // GIVEN elevated subcommands with --help / -h flags appended
180
+ const highRiskWithHelp = [
162
181
  "assistant oauth token --help",
163
182
  "assistant oauth mode --set --help",
164
183
  "assistant credentials reveal --help",
184
+ "assistant trust clear --help",
185
+ "assistant trust remove -h",
186
+ "assistant credentials set --help",
187
+ "assistant credentials delete -h",
188
+ "assistant keys set --help",
189
+ "assistant keys delete -h",
190
+ ];
191
+
192
+ const mediumRiskWithHelp = [
165
193
  "assistant oauth request --help",
166
194
  "assistant oauth connect --help",
167
195
  "assistant oauth disconnect -h",
168
196
  ];
169
197
 
170
- for (const command of helpCommands) {
198
+ // WHEN classifying risk
199
+ // THEN --help does not bypass the elevated risk level
200
+ for (const command of highRiskWithHelp) {
171
201
  const risk = await classifyRisk("bash", { command });
172
- expectLowRisk(command, risk);
202
+ expect(risk).toBe(RiskLevel.High);
173
203
  }
174
- });
175
204
 
176
- test("--help after -- option terminator does not downgrade risk", async () => {
177
- const risk = await classifyRisk("bash", {
178
- command: "assistant oauth token -- --help",
179
- });
180
- expect(risk).toBe(RiskLevel.High);
205
+ for (const command of mediumRiskWithHelp) {
206
+ const risk = await classifyRisk("bash", { command });
207
+ expect(risk).toBe(RiskLevel.Medium);
208
+ }
181
209
  });
182
210
 
183
211
  test("non-sensitive oauth subcommands remain Low risk", async () => {
@@ -205,6 +233,66 @@ describe("CLI command risk guard: elevated assistant subcommands", () => {
205
233
  expectLowRisk(command, risk);
206
234
  }
207
235
  });
236
+
237
+ test("assistant credentials set is High risk (modifies stored credentials)", async () => {
238
+ const risk = await classifyRisk("bash", {
239
+ command: "assistant credentials set",
240
+ });
241
+ expect(risk).toBe(RiskLevel.High);
242
+ });
243
+
244
+ test("assistant credentials delete is High risk (removes stored credentials)", async () => {
245
+ const risk = await classifyRisk("bash", {
246
+ command: "assistant credentials delete",
247
+ });
248
+ expect(risk).toBe(RiskLevel.High);
249
+ });
250
+
251
+ test("assistant keys set is High risk (modifies API keys)", async () => {
252
+ const risk = await classifyRisk("bash", {
253
+ command: "assistant keys set anthropic sk-ant-xxx",
254
+ });
255
+ expect(risk).toBe(RiskLevel.High);
256
+ });
257
+
258
+ test("assistant keys delete is High risk (removes API keys)", async () => {
259
+ const risk = await classifyRisk("bash", {
260
+ command: "assistant keys delete openai",
261
+ });
262
+ expect(risk).toBe(RiskLevel.High);
263
+ });
264
+
265
+ test("non-sensitive keys subcommands remain Low risk", async () => {
266
+ const lowRiskKeysCommands = ["assistant keys", "assistant keys list"];
267
+
268
+ for (const command of lowRiskKeysCommands) {
269
+ const risk = await classifyRisk("bash", { command });
270
+ expectLowRisk(command, risk);
271
+ }
272
+ });
273
+
274
+ test("assistant trust remove is High risk (removes trust rules)", async () => {
275
+ const risk = await classifyRisk("bash", {
276
+ command: "assistant trust remove abc123",
277
+ });
278
+ expect(risk).toBe(RiskLevel.High);
279
+ });
280
+
281
+ test("assistant trust clear is High risk (clears all trust rules)", async () => {
282
+ const risk = await classifyRisk("bash", {
283
+ command: "assistant trust clear",
284
+ });
285
+ expect(risk).toBe(RiskLevel.High);
286
+ });
287
+
288
+ test("non-sensitive trust subcommands remain Low risk", async () => {
289
+ const lowRiskTrustCommands = ["assistant trust", "assistant trust list"];
290
+
291
+ for (const command of lowRiskTrustCommands) {
292
+ const risk = await classifyRisk("bash", { command });
293
+ expectLowRisk(command, risk);
294
+ }
295
+ });
208
296
  });
209
297
 
210
298
  describe("CLI command risk guard: wrapper program propagation", () => {
@@ -0,0 +1,372 @@
1
+ import { rmSync } from "node:fs";
2
+ import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
3
+
4
+ import { Command } from "commander";
5
+ import { eq } from "drizzle-orm";
6
+
7
+ mock.module("../util/logger.js", () => ({
8
+ getLogger: () =>
9
+ new Proxy({} as Record<string, unknown>, {
10
+ get: () => () => {},
11
+ }),
12
+ }));
13
+
14
+ mock.module("../memory/qdrant-client.js", () => ({
15
+ getQdrantClient: () => ({
16
+ searchWithFilter: async () => [],
17
+ hybridSearch: async () => [],
18
+ upsertPoints: async () => {},
19
+ deletePoints: async () => {},
20
+ }),
21
+ initQdrantClient: () => {},
22
+ }));
23
+
24
+ // Controllable mock for buildCliProgram
25
+ let mockCommands: { name: string; description: string }[] = [];
26
+
27
+ function makeMockProgram(): Command {
28
+ const program = new Command();
29
+ for (const cmd of mockCommands) {
30
+ program.command(cmd.name).description(cmd.description);
31
+ }
32
+ return program;
33
+ }
34
+
35
+ mock.module("../cli/program.js", () => ({
36
+ buildCliProgram: () => makeMockProgram(),
37
+ }));
38
+
39
+ import { DEFAULT_CONFIG } from "../config/defaults.js";
40
+
41
+ const TEST_CONFIG = {
42
+ ...DEFAULT_CONFIG,
43
+ memory: {
44
+ ...DEFAULT_CONFIG.memory,
45
+ enabled: true,
46
+ extraction: {
47
+ ...DEFAULT_CONFIG.memory.extraction,
48
+ useLLM: false,
49
+ },
50
+ },
51
+ };
52
+
53
+ mock.module("../config/loader.js", () => ({
54
+ loadConfig: () => TEST_CONFIG,
55
+ getConfig: () => TEST_CONFIG,
56
+ loadRawConfig: () => ({}),
57
+ saveRawConfig: () => {},
58
+ invalidateConfigCache: () => {},
59
+ }));
60
+
61
+ import {
62
+ buildCliCapabilityStatement,
63
+ seedCliCommandMemories,
64
+ upsertCliCapabilityMemory,
65
+ } from "../cli/cli-memory.js";
66
+ import { getDb, initializeDb, resetDb } from "../memory/db.js";
67
+ import { memoryItems, memoryJobs } from "../memory/schema.js";
68
+ import { ensureDataDir, getDbPath } from "../util/platform.js";
69
+
70
+ ensureDataDir();
71
+ initializeDb();
72
+
73
+ afterAll(() => {
74
+ resetDb();
75
+ });
76
+
77
+ function resetTables() {
78
+ const db = getDb();
79
+ db.run("DELETE FROM memory_item_sources");
80
+ db.run("DELETE FROM memory_embeddings");
81
+ db.run("DELETE FROM memory_items");
82
+ db.run("DELETE FROM memory_jobs");
83
+ }
84
+
85
+ // ─── buildCliCapabilityStatement ────────────────────────────────────────────
86
+
87
+ describe("buildCliCapabilityStatement", () => {
88
+ test("includes 'assistant' prefix, name, and description", () => {
89
+ const result = buildCliCapabilityStatement("doctor", "Run diagnostic checks");
90
+ expect(result).toContain('"assistant doctor"');
91
+ expect(result).toContain("Run diagnostic checks");
92
+ });
93
+
94
+ test("truncates long statements to 500 chars", () => {
95
+ const longDesc = "x".repeat(600);
96
+ const result = buildCliCapabilityStatement("test", longDesc);
97
+ expect(result.length).toBe(500);
98
+ });
99
+ });
100
+
101
+ // ─── upsertCliCapabilityMemory ──────────────────────────────────────────────
102
+
103
+ describe("upsertCliCapabilityMemory", () => {
104
+ beforeEach(resetTables);
105
+
106
+ test("inserts with correct kind, subject, confidence, importance", () => {
107
+ upsertCliCapabilityMemory("doctor", "Run diagnostic checks");
108
+
109
+ const db = getDb();
110
+ const items = db.select().from(memoryItems).all();
111
+ expect(items).toHaveLength(1);
112
+ expect(items[0].kind).toBe("capability");
113
+ expect(items[0].subject).toBe("cli:doctor");
114
+ expect(items[0].confidence).toBe(1.0);
115
+ expect(items[0].importance).toBe(0.7);
116
+ expect(items[0].status).toBe("active");
117
+ expect(items[0].scopeId).toBe("default");
118
+
119
+ // Should also enqueue an embed_item job
120
+ const jobs = db.select().from(memoryJobs).all();
121
+ expect(jobs).toHaveLength(1);
122
+ expect(jobs[0].type).toBe("embed_item");
123
+ });
124
+
125
+ test("is idempotent (same entry only touches lastSeenAt)", () => {
126
+ upsertCliCapabilityMemory("doctor", "Run diagnostic checks");
127
+
128
+ const db = getDb();
129
+ const before = db.select().from(memoryItems).all();
130
+ expect(before).toHaveLength(1);
131
+ const originalLastSeen = before[0].lastSeenAt;
132
+
133
+ // Upsert again
134
+ upsertCliCapabilityMemory("doctor", "Run diagnostic checks");
135
+
136
+ const after = db.select().from(memoryItems).all();
137
+ expect(after).toHaveLength(1);
138
+ // Fingerprint should be the same, so only lastSeenAt changes
139
+ expect(after[0].fingerprint).toBe(before[0].fingerprint);
140
+ expect(after[0].lastSeenAt).toBeGreaterThanOrEqual(originalLastSeen);
141
+
142
+ // Should NOT enqueue a second embed job (only 1 from initial insert)
143
+ const jobs = db.select().from(memoryJobs).all();
144
+ expect(jobs).toHaveLength(1);
145
+ });
146
+
147
+ test("updates statement when description changes", () => {
148
+ upsertCliCapabilityMemory("doctor", "Original description");
149
+
150
+ const db = getDb();
151
+ const before = db.select().from(memoryItems).all();
152
+ expect(before).toHaveLength(1);
153
+ expect(before[0].statement).toContain("Original description");
154
+
155
+ // Change description
156
+ upsertCliCapabilityMemory("doctor", "Updated description");
157
+
158
+ const after = db.select().from(memoryItems).all();
159
+ expect(after).toHaveLength(1);
160
+ expect(after[0].statement).toContain("Updated description");
161
+ expect(after[0].fingerprint).not.toBe(before[0].fingerprint);
162
+
163
+ // Should enqueue a second embed job
164
+ const jobs = db.select().from(memoryJobs).all();
165
+ expect(jobs).toHaveLength(2);
166
+ });
167
+
168
+ test("reactivates soft-deleted items", () => {
169
+ upsertCliCapabilityMemory("doctor", "Run diagnostic checks");
170
+
171
+ const db = getDb();
172
+ // Soft-delete the item
173
+ db.update(memoryItems)
174
+ .set({ status: "deleted" })
175
+ .where(eq(memoryItems.subject, "cli:doctor"))
176
+ .run();
177
+
178
+ const deleted = db.select().from(memoryItems).all();
179
+ expect(deleted[0].status).toBe("deleted");
180
+
181
+ // Clear jobs from initial insert
182
+ db.run("DELETE FROM memory_jobs");
183
+
184
+ // Upsert again — should reactivate
185
+ upsertCliCapabilityMemory("doctor", "Run diagnostic checks");
186
+
187
+ const reactivated = db.select().from(memoryItems).all();
188
+ expect(reactivated).toHaveLength(1);
189
+ expect(reactivated[0].status).toBe("active");
190
+
191
+ // Should enqueue embed job for reactivated item
192
+ const jobs = db.select().from(memoryJobs).all();
193
+ expect(jobs).toHaveLength(1);
194
+ expect(jobs[0].type).toBe("embed_item");
195
+ });
196
+
197
+ test("does not throw on DB error", () => {
198
+ resetDb();
199
+ const db = getDb();
200
+ db.run("DROP TABLE IF EXISTS memory_items");
201
+
202
+ expect(() => {
203
+ upsertCliCapabilityMemory("doctor", "Run diagnostic checks");
204
+ }).not.toThrow();
205
+
206
+ // Restore DB state for subsequent tests.
207
+ resetDb();
208
+ const dbPath = getDbPath();
209
+ for (const ext of ["", "-wal", "-shm"]) {
210
+ rmSync(`${dbPath}${ext}`, { force: true });
211
+ }
212
+ initializeDb();
213
+ });
214
+ });
215
+
216
+ // ─── seedCliCommandMemories ─────────────────────────────────────────────────
217
+
218
+ describe("seedCliCommandMemories", () => {
219
+ beforeEach(() => {
220
+ resetTables();
221
+ // Reset mock commands
222
+ mockCommands = [];
223
+ });
224
+
225
+ test("upserts capability memories for all commands", () => {
226
+ mockCommands = [
227
+ { name: "doctor", description: "Run diagnostic checks" },
228
+ { name: "config", description: "Manage configuration" },
229
+ { name: "keys", description: "Manage API keys" },
230
+ ];
231
+
232
+ seedCliCommandMemories();
233
+
234
+ const db = getDb();
235
+ const items = db
236
+ .select()
237
+ .from(memoryItems)
238
+ .where(eq(memoryItems.kind, "capability"))
239
+ .all();
240
+ expect(items).toHaveLength(3);
241
+
242
+ const subjects = items.map((i) => i.subject).sort();
243
+ expect(subjects).toEqual([
244
+ "cli:config",
245
+ "cli:doctor",
246
+ "cli:keys",
247
+ ]);
248
+
249
+ // All should be active
250
+ for (const item of items) {
251
+ expect(item.status).toBe("active");
252
+ }
253
+ });
254
+
255
+ test("prunes stale capabilities for commands no longer registered", () => {
256
+ // First seed with three commands
257
+ mockCommands = [
258
+ { name: "doctor", description: "Run diagnostic checks" },
259
+ { name: "config", description: "Manage configuration" },
260
+ { name: "keys", description: "Manage API keys" },
261
+ ];
262
+ seedCliCommandMemories();
263
+
264
+ const db = getDb();
265
+ const beforeItems = db
266
+ .select()
267
+ .from(memoryItems)
268
+ .where(eq(memoryItems.kind, "capability"))
269
+ .all();
270
+ expect(beforeItems).toHaveLength(3);
271
+ expect(beforeItems.every((i) => i.status === "active")).toBe(true);
272
+
273
+ // Now seed with only doctor — config and keys should be pruned
274
+ mockCommands = [
275
+ { name: "doctor", description: "Run diagnostic checks" },
276
+ ];
277
+ seedCliCommandMemories();
278
+
279
+ const afterItems = db
280
+ .select()
281
+ .from(memoryItems)
282
+ .where(eq(memoryItems.kind, "capability"))
283
+ .all();
284
+ expect(afterItems).toHaveLength(3); // still 3 rows, but 2 are soft-deleted
285
+
286
+ const active = afterItems.filter((i) => i.status === "active");
287
+ const deleted = afterItems.filter((i) => i.status === "deleted");
288
+
289
+ expect(active).toHaveLength(1);
290
+ expect(active[0].subject).toBe("cli:doctor");
291
+
292
+ expect(deleted).toHaveLength(2);
293
+ const deletedSubjects = deleted.map((i) => i.subject).sort();
294
+ expect(deletedSubjects).toEqual(["cli:config", "cli:keys"]);
295
+ });
296
+
297
+ test("handles empty command list without errors", () => {
298
+ // Pre-populate a CLI command so we can verify it gets pruned
299
+ upsertCliCapabilityMemory("old-command", "An old command");
300
+
301
+ const db = getDb();
302
+ const beforeItems = db.select().from(memoryItems).all();
303
+ expect(beforeItems).toHaveLength(1);
304
+ expect(beforeItems[0].status).toBe("active");
305
+
306
+ // Seed with empty commands
307
+ mockCommands = [];
308
+ seedCliCommandMemories();
309
+
310
+ // The existing command should be pruned (soft-deleted)
311
+ const afterItems = db.select().from(memoryItems).all();
312
+ expect(afterItems).toHaveLength(1);
313
+ expect(afterItems[0].status).toBe("deleted");
314
+ });
315
+
316
+ test("does not prune non-cli capability memories", () => {
317
+ // Pre-insert a skill capability memory directly into the DB
318
+ const db = getDb();
319
+ const now = Date.now();
320
+ db.insert(memoryItems)
321
+ .values({
322
+ id: "skill-test-item",
323
+ kind: "capability",
324
+ subject: "skill:test-skill",
325
+ statement: "The test skill does things.",
326
+ status: "active",
327
+ confidence: 1.0,
328
+ importance: 0.7,
329
+ fingerprint: "skill-test-fp",
330
+ sourceType: "extraction",
331
+ scopeId: "default",
332
+ firstSeenAt: now,
333
+ lastSeenAt: now,
334
+ })
335
+ .run();
336
+
337
+ // Seed with empty commands — CLI pruner runs but should skip skill:* items
338
+ mockCommands = [];
339
+ seedCliCommandMemories();
340
+
341
+ const item = db
342
+ .select()
343
+ .from(memoryItems)
344
+ .where(eq(memoryItems.subject, "skill:test-skill"))
345
+ .get();
346
+ expect(item).toBeDefined();
347
+ expect(item!.status).toBe("active");
348
+ });
349
+
350
+ test("does not throw on error", () => {
351
+ mockCommands = [
352
+ { name: "doctor", description: "Run diagnostic checks" },
353
+ ];
354
+
355
+ // Drop memory_items to force a DB error during the prune phase
356
+ resetDb();
357
+ const db = getDb();
358
+ db.run("DROP TABLE IF EXISTS memory_items");
359
+
360
+ expect(() => {
361
+ seedCliCommandMemories();
362
+ }).not.toThrow();
363
+
364
+ // Restore DB state for subsequent tests.
365
+ resetDb();
366
+ const dbPath = getDbPath();
367
+ for (const ext of ["", "-wal", "-shm"]) {
368
+ rmSync(`${dbPath}${ext}`, { force: true });
369
+ }
370
+ initializeDb();
371
+ });
372
+ });
@@ -52,9 +52,19 @@ describe("computer-use skill manifest regression", () => {
52
52
  }
53
53
  });
54
54
 
55
- test("all manifest tools have risk: low", () => {
55
+ test("read-only tools have risk: low, side-effect tools have risk: medium", () => {
56
+ const readOnlyTools = new Set([
57
+ "computer_use_observe",
58
+ "computer_use_wait",
59
+ "computer_use_done",
60
+ "computer_use_respond",
61
+ ]);
56
62
  for (const tool of manifest.tools) {
57
- expect(tool.risk).toBe("low");
63
+ if (readOnlyTools.has(tool.name)) {
64
+ expect(tool.risk).toBe("low");
65
+ } else {
66
+ expect(tool.risk).toBe("medium");
67
+ }
58
68
  }
59
69
  });
60
70
 
@@ -421,7 +421,6 @@ describe("AssistantConfigSchema", () => {
421
421
  const result = AssistantConfigSchema.parse({});
422
422
  expect(result.permissions).toEqual({
423
423
  mode: "workspace",
424
- dangerouslySkipPermissions: false,
425
424
  });
426
425
  });
427
426
 
@@ -1129,7 +1128,6 @@ describe("loadConfig with schema validation", () => {
1129
1128
  const config = loadConfig();
1130
1129
  expect(config.permissions).toEqual({
1131
1130
  mode: "workspace",
1132
- dangerouslySkipPermissions: false,
1133
1131
  });
1134
1132
  });
1135
1133