@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
@@ -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
 
@@ -118,6 +118,7 @@ function makeIdleSession(opts?: {
118
118
  updateClient: () => {},
119
119
  setHostBashProxy: () => {},
120
120
  setHostFileProxy: () => {},
121
+ setHostCuProxy: () => {},
121
122
  enqueueMessage: () => ({ queued: false, requestId: "noop" }),
122
123
  hasAnyPendingConfirmation: () => false,
123
124
  runAgentLoop: async (
@@ -181,6 +182,7 @@ function makeConfirmationEmittingSession(opts?: {
181
182
  updateClient: () => {},
182
183
  setHostBashProxy: () => {},
183
184
  setHostFileProxy: () => {},
185
+ setHostCuProxy: () => {},
184
186
  enqueueMessage: () => ({ queued: false, requestId: "noop" }),
185
187
  hasAnyPendingConfirmation: () => false,
186
188
  runAgentLoop: async (
@@ -10,7 +10,6 @@ import {
10
10
  estimateBase64Bytes,
11
11
  inferMimeType,
12
12
  MAX_ASSISTANT_ATTACHMENT_BYTES,
13
- MAX_ASSISTANT_ATTACHMENTS,
14
13
  validateDrafts,
15
14
  } from "../daemon/assistant-attachments.js";
16
15
 
@@ -163,33 +162,27 @@ describe("validateDrafts", () => {
163
162
  expect(result.warnings[0]).toContain("exceeds");
164
163
  });
165
164
 
166
- test("truncates beyond max count", () => {
167
- const drafts = Array.from(
168
- { length: MAX_ASSISTANT_ATTACHMENTS + 2 },
169
- (_, i) => makeDraft({ filename: `file-${i}.txt` }),
165
+ test("accepts many drafts without count limit", () => {
166
+ const drafts = Array.from({ length: 20 }, (_, i) =>
167
+ makeDraft({ filename: `file-${i}.txt` }),
170
168
  );
171
169
  const result = validateDrafts(drafts);
172
- expect(result.accepted).toHaveLength(MAX_ASSISTANT_ATTACHMENTS);
173
- expect(result.warnings).toHaveLength(2);
174
- expect(result.warnings[0]).toContain(
175
- `file-${MAX_ASSISTANT_ATTACHMENTS}.txt`,
176
- );
177
- expect(result.warnings[0]).toContain("exceeded maximum");
170
+ expect(result.accepted).toHaveLength(20);
171
+ expect(result.warnings).toHaveLength(0);
178
172
  });
179
173
 
180
- test("rejects oversized before applying count cap", () => {
174
+ test("rejects oversized while accepting all valid drafts", () => {
181
175
  const drafts = [
182
176
  makeDraft({
183
177
  filename: "big.bin",
184
178
  sizeBytes: MAX_ASSISTANT_ATTACHMENT_BYTES + 1,
185
179
  }),
186
- ...Array.from({ length: MAX_ASSISTANT_ATTACHMENTS }, (_, i) =>
180
+ ...Array.from({ length: 10 }, (_, i) =>
187
181
  makeDraft({ filename: `ok-${i}.txt` }),
188
182
  ),
189
183
  ];
190
184
  const result = validateDrafts(drafts);
191
- // big.bin rejected for size; all MAX_ASSISTANT_ATTACHMENTS ok files accepted
192
- expect(result.accepted).toHaveLength(MAX_ASSISTANT_ATTACHMENTS);
185
+ expect(result.accepted).toHaveLength(10);
193
186
  expect(result.warnings).toHaveLength(1);
194
187
  expect(result.warnings[0]).toContain("big.bin");
195
188
  });
@@ -431,11 +424,8 @@ describe("contentBlocksToDrafts", () => {
431
424
  // ---------------------------------------------------------------------------
432
425
 
433
426
  describe("validateDrafts with reversed tool drafts", () => {
434
- test("most recent tool screenshots win the attachment cap when reversed before validation", () => {
435
- // Simulate a browser session producing many screenshots chronologically.
436
- // After reversing, the most recent (highest index) should appear first
437
- // and win the MAX_ASSISTANT_ATTACHMENTS cap.
438
- const totalScreenshots = MAX_ASSISTANT_ATTACHMENTS + 3;
427
+ test("all tool screenshots accepted after reversing", () => {
428
+ const totalScreenshots = 8;
439
429
  const toolDrafts = Array.from({ length: totalScreenshots }, (_, i) =>
440
430
  makeDraft({
441
431
  sourceType: "tool_block",
@@ -446,23 +436,11 @@ describe("validateDrafts with reversed tool drafts", () => {
446
436
  }),
447
437
  );
448
438
 
449
- // Reverse to prioritize most recent
450
439
  toolDrafts.reverse();
451
440
 
452
441
  const result = validateDrafts(toolDrafts);
453
- expect(result.accepted).toHaveLength(MAX_ASSISTANT_ATTACHMENTS);
454
-
455
- // The accepted drafts should be the most recent screenshots (highest step numbers)
456
- const acceptedFilenames = result.accepted.map((d) => d.filename);
457
- for (let i = 0; i < MAX_ASSISTANT_ATTACHMENTS; i++) {
458
- expect(acceptedFilenames[i]).toBe(
459
- `screenshot-step-${totalScreenshots - 1 - i}.png`,
460
- );
461
- }
462
-
463
- // The oldest screenshots should be dropped
464
- expect(result.warnings).toHaveLength(3);
465
- expect(result.warnings[0]).toContain("screenshot-step-2.png");
442
+ expect(result.accepted).toHaveLength(totalScreenshots);
443
+ expect(result.warnings).toHaveLength(0);
466
444
  });
467
445
  });
468
446
 
@@ -10,6 +10,11 @@
10
10
  * `isAssistantFeatureFlagEnabled('<key>', ...)` in production code must be
11
11
  * declared in the unified registry. This keeps flag usage declarative while
12
12
  * allowing skills to exist without corresponding feature flags.
13
+ *
14
+ * 3. Indirect key coverage: all `feature_flags.<id>.enabled` string literals
15
+ * anywhere in production code (maps, constants, variables, etc.) must be
16
+ * declared in the unified registry. This catches indirect key patterns that
17
+ * Guard 2 would miss, such as flag keys stored in lookup maps or constants.
13
18
  */
14
19
 
15
20
  import { execSync } from "node:child_process";
@@ -197,3 +202,74 @@ describe("assistant feature flag declaration coverage guard", () => {
197
202
  }
198
203
  });
199
204
  });
205
+
206
+ // ---------------------------------------------------------------------------
207
+ // Guard 3: Indirect key coverage — flag key literals anywhere in production code
208
+ // ---------------------------------------------------------------------------
209
+
210
+ describe("assistant feature flag indirect key coverage guard", () => {
211
+ test("all feature_flags.<id>.enabled string literals in production code are declared in the unified registry", () => {
212
+ const repoRoot = getRepoRoot();
213
+
214
+ // Load the unified registry and extract all declared keys (any scope)
215
+ const registry = loadRegistry();
216
+ const declaredKeys = new Set(registry.flags.map((f) => f.key));
217
+
218
+ // Search for any string literal matching the canonical key pattern
219
+ // in production .ts files under assistant/src/ and gateway/src/.
220
+ // This catches keys in maps, constants, variables, or any other
221
+ // indirect patterns that Guard 2 would miss.
222
+ let grepOutput = "";
223
+ try {
224
+ grepOutput = execSync(
225
+ `git grep -nE "feature_flags\\.[a-z0-9_-]+\\.enabled\\b" -- 'assistant/src/**/*.ts' 'gateway/src/**/*.ts'`,
226
+ { encoding: "utf-8", cwd: repoRoot },
227
+ ).trim();
228
+ } catch (err) {
229
+ // Exit code 1 means no matches — happy path
230
+ if ((err as { status?: number }).status === 1) {
231
+ return;
232
+ }
233
+ throw err;
234
+ }
235
+
236
+ const keyPattern = /feature_flags\.[a-z0-9_-]+\.enabled\b/g;
237
+ const undeclared: string[] = [];
238
+
239
+ for (const line of grepOutput.split("\n")) {
240
+ if (!line) continue;
241
+
242
+ // Format: "file:line:content"
243
+ const colonIdx = line.indexOf(":");
244
+ if (colonIdx === -1) continue;
245
+ const filePath = line.slice(0, colonIdx);
246
+
247
+ // Skip test files
248
+ if (isTestFile(filePath)) continue;
249
+
250
+ // Extract all key occurrences from this line
251
+ const content = line.slice(colonIdx + 1);
252
+ for (const match of content.matchAll(keyPattern)) {
253
+ const key = match[0];
254
+ if (!declaredKeys.has(key)) {
255
+ undeclared.push(`${filePath}: ${key}`);
256
+ }
257
+ }
258
+ }
259
+
260
+ if (undeclared.length > 0) {
261
+ const message = [
262
+ "Found feature_flags.<id>.enabled string literals in production code that are NOT declared in the unified registry.",
263
+ "This catches indirect flag key usage (maps, constants, variables) that the direct-call guard misses.",
264
+ `Registry: meta/feature-flags/feature-flag-registry.json`,
265
+ "",
266
+ "Undeclared keys:",
267
+ ...undeclared.map((k) => ` - ${k}`),
268
+ "",
269
+ "To fix: add the missing key(s) to the unified registry, or remove the stale reference.",
270
+ ].join("\n");
271
+
272
+ expect(undeclared, message).toEqual([]);
273
+ }
274
+ });
275
+ });
@@ -75,7 +75,6 @@ mock.module("../util/logger.js", () => ({
75
75
  ...realLogger,
76
76
  getLogger: () => noopLogger,
77
77
  getCliLogger: () => noopLogger,
78
- isDebug: () => false,
79
78
  truncateForLog: (v: string) => v,
80
79
  initLogger: () => {},
81
80
  pruneOldLogFiles: () => 0,
@@ -55,10 +55,10 @@ describe("browser skill cutover — startup tool payload", () => {
55
55
  const definitions = getAllToolDefinitions();
56
56
  const serialized = JSON.stringify(definitions);
57
57
  // Startup payload is ~22 000 chars without browser tools.
58
- // Floor at 15 000 catches accidental wholesale removal; ceiling at 35 000
58
+ // Floor at 14 000 catches accidental wholesale removal; ceiling at 35 000
59
59
  // gives headroom while still catching browser tool leakage
60
60
  // (~4 640 chars would push it past the ceiling).
61
- expect(serialized.length).toBeGreaterThan(15_000);
61
+ expect(serialized.length).toBeGreaterThan(14_000);
62
62
  expect(serialized.length).toBeLessThan(35_000);
63
63
  });
64
64
 
@@ -1305,8 +1305,6 @@ function createMockCtx(): {
1305
1305
  let captured: ChannelVerificationSessionResponse | null = null;
1306
1306
  const ctx = {
1307
1307
  sessions: new Map(),
1308
- cuSessions: new Map(),
1309
- cuObservationParseSequence: new Map(),
1310
1308
  sharedRequestTimestamps: [],
1311
1309
  debounceTimers: {
1312
1310
  schedule: () => {},
@@ -11,7 +11,6 @@ import { beforeEach, describe, expect, mock, test } from "bun:test";
11
11
  // Mocks — must be set up before importing the service
12
12
  // ---------------------------------------------------------------------------
13
13
 
14
- let mockTwilioPhoneNumberEnv: string | undefined;
15
14
  let mockRawConfig: Record<string, unknown> | undefined;
16
15
  let mockSecureKeys: Record<string, string>;
17
16
  let mockHasTwilioCredentials: boolean;
@@ -23,16 +22,27 @@ mock.module("../calls/twilio-rest.js", () => ({
23
22
  getTollFreeVerificationStatus: async () => null,
24
23
  }));
25
24
 
26
- mock.module("../config/env.js", () => ({
27
- getTwilioPhoneNumberEnv: () => mockTwilioPhoneNumberEnv,
28
- }));
25
+ mock.module("../config/env.js", () => ({}));
29
26
 
30
27
  mock.module("../config/loader.js", () => ({
31
28
  loadRawConfig: () => mockRawConfig,
29
+ loadConfig: () => {
30
+ const raw = mockRawConfig ?? {};
31
+ const wa = (raw.whatsapp ?? {}) as Record<string, unknown>;
32
+ const tw = (raw.twilio ?? {}) as Record<string, unknown>;
33
+ return {
34
+ twilio: { phoneNumber: (tw.phoneNumber as string) ?? "" },
35
+ whatsapp: { phoneNumber: (wa.phoneNumber as string) ?? "" },
36
+ };
37
+ },
32
38
  getConfig: () => {
33
39
  const raw = mockRawConfig ?? {};
34
40
  const wa = (raw.whatsapp ?? {}) as Record<string, unknown>;
35
- return { whatsapp: { phoneNumber: (wa.phoneNumber as string) ?? "" } };
41
+ const tw = (raw.twilio ?? {}) as Record<string, unknown>;
42
+ return {
43
+ twilio: { phoneNumber: (tw.phoneNumber as string) ?? "" },
44
+ whatsapp: { phoneNumber: (wa.phoneNumber as string) ?? "" },
45
+ };
36
46
  },
37
47
  invalidateConfigCache: () => {},
38
48
  }));
@@ -60,7 +70,6 @@ import { credentialKey } from "../security/credential-key.js";
60
70
 
61
71
  describe("channel readiness routes — email and WhatsApp probes", () => {
62
72
  beforeEach(() => {
63
- mockTwilioPhoneNumberEnv = undefined;
64
73
  mockRawConfig = undefined;
65
74
  mockSecureKeys = {};
66
75
  mockHasTwilioCredentials = false;
@@ -1,6 +1,6 @@
1
1
  import { beforeEach, describe, expect, mock, spyOn, test } from "bun:test";
2
2
 
3
- let mockTwilioPhoneNumberEnv: string | undefined;
3
+ let mockTwilioPhoneNumber: string | undefined;
4
4
  let mockRawConfig: Record<string, unknown> | undefined;
5
5
  let mockSecureKeys: Record<string, string>;
6
6
  let mockHasTwilioCredentials: boolean;
@@ -27,16 +27,17 @@ mock.module("../channels/config.js", () => ({
27
27
  }),
28
28
  }));
29
29
 
30
- mock.module("../config/env.js", () => ({
31
- getTwilioPhoneNumberEnv: () => mockTwilioPhoneNumberEnv,
32
- }));
30
+ mock.module("../config/env.js", () => ({}));
33
31
 
34
32
  mock.module("../config/loader.js", () => ({
35
33
  loadRawConfig: () => mockRawConfig,
34
+ loadConfig: () => ({
35
+ twilio: { phoneNumber: mockTwilioPhoneNumber ?? "" },
36
+ whatsapp: { phoneNumber: "" },
37
+ }),
36
38
  getConfig: () => ({
37
- whatsapp: {
38
- phoneNumber: "",
39
- },
39
+ twilio: { phoneNumber: mockTwilioPhoneNumber ?? "" },
40
+ whatsapp: { phoneNumber: "" },
40
41
  }),
41
42
  }));
42
43
 
@@ -100,7 +101,7 @@ describe("ChannelReadinessService", () => {
100
101
 
101
102
  beforeEach(() => {
102
103
  service = new ChannelReadinessService();
103
- mockTwilioPhoneNumberEnv = undefined;
104
+ mockTwilioPhoneNumber = undefined;
104
105
  mockRawConfig = undefined;
105
106
  mockSecureKeys = {};
106
107
  mockHasTwilioCredentials = false;
@@ -406,7 +407,7 @@ describe("ChannelReadinessService", () => {
406
407
 
407
408
  test("voice readiness includes gateway_health when ingress is configured", async () => {
408
409
  mockHasTwilioCredentials = true;
409
- mockTwilioPhoneNumberEnv = "+15550001111";
410
+ mockTwilioPhoneNumber = "+15550001111";
410
411
  mockRawConfig = {
411
412
  ingress: {
412
413
  enabled: true,
@@ -135,9 +135,7 @@ registerTool(mockBundledSkillTool);
135
135
  // Register CU tools so classifyRisk returns their declared Low risk level
136
136
  // instead of falling through to Medium (unknown tool).
137
137
  import { registerComputerUseActionTools } from "../tools/computer-use/registry.js";
138
- import { requestComputerControlTool } from "../tools/computer-use/request-computer-control.js";
139
138
  registerComputerUseActionTools();
140
- registerTool(requestComputerControlTool);
141
139
 
142
140
  function writeSkill(
143
141
  skillId: string,
@@ -673,14 +671,14 @@ describe("Permission Checker", () => {
673
671
  expect(result.decision).toBe("allow");
674
672
  });
675
673
 
676
- test("file_write within workspace with no rule → auto-allowed in workspace mode", async () => {
674
+ test("file_write within workspace with no rule → prompt (medium risk bypasses workspace auto-allow)", async () => {
677
675
  const result = await check(
678
676
  "file_write",
679
677
  { path: "/tmp/file.txt" },
680
678
  "/tmp",
681
679
  );
682
- expect(result.decision).toBe("allow");
683
- expect(result.reason).toContain("workspace-scoped");
680
+ expect(result.decision).toBe("prompt");
681
+ expect(result.reason).toContain("medium risk");
684
682
  });
685
683
 
686
684
  test("file_write outside workspace with no rule → prompt", async () => {
@@ -897,19 +895,6 @@ describe("Permission Checker", () => {
897
895
  );
898
896
  });
899
897
 
900
- test("computer_use_request_control prompts by default via computer-use ask rule", async () => {
901
- const result = await check(
902
- "computer_use_request_control",
903
- { task: "Open system settings" },
904
- "/tmp",
905
- );
906
- expect(result.decision).toBe("prompt");
907
- expect(result.reason).toContain("ask rule");
908
- expect(result.matchedRule?.id).toBe(
909
- "default:ask-computer_use_request_control-global",
910
- );
911
- });
912
-
913
898
  test("higher-priority allow rule can override default computer-use ask rule", async () => {
914
899
  addRule(
915
900
  "computer_use_click",
@@ -4407,11 +4392,6 @@ describe("computer-use tool permission defaults", () => {
4407
4392
  expect(risk).toBe(RiskLevel.Low);
4408
4393
  }
4409
4394
  });
4410
-
4411
- test("computer_use_request_control classifies as Low risk", async () => {
4412
- const risk = await classifyRisk("computer_use_request_control", {});
4413
- expect(risk).toBe(RiskLevel.Low);
4414
- });
4415
4395
  });
4416
4396
 
4417
4397
  // ---------------------------------------------------------------------------
@@ -4588,24 +4568,24 @@ describe("workspace mode — auto-allow workspace-scoped operations", () => {
4588
4568
  expect(result.reason).toContain("Workspace mode");
4589
4569
  });
4590
4570
 
4591
- test("file_write within workspace → allow (workspace-scoped)", async () => {
4571
+ test("file_write within workspace → prompt (Medium risk bypasses workspace auto-allow)", async () => {
4592
4572
  const result = await check(
4593
4573
  "file_write",
4594
4574
  { file_path: "/home/user/my-project/src/index.ts" },
4595
4575
  workspaceDir,
4596
4576
  );
4597
- expect(result.decision).toBe("allow");
4598
- expect(result.reason).toContain("Workspace mode");
4577
+ expect(result.decision).toBe("prompt");
4578
+ expect(result.reason).toContain("medium risk");
4599
4579
  });
4600
4580
 
4601
- test("file_edit within workspace → allow (workspace-scoped)", async () => {
4581
+ test("file_edit within workspace → prompt (Medium risk bypasses workspace auto-allow)", async () => {
4602
4582
  const result = await check(
4603
4583
  "file_edit",
4604
4584
  { file_path: "/home/user/my-project/src/index.ts" },
4605
4585
  workspaceDir,
4606
4586
  );
4607
- expect(result.decision).toBe("allow");
4608
- expect(result.reason).toContain("Workspace mode");
4587
+ expect(result.decision).toBe("prompt");
4588
+ expect(result.reason).toContain("medium risk");
4609
4589
  });
4610
4590
 
4611
4591
  // ── file operations outside workspace follow risk-based fallback ──
@@ -29,7 +29,7 @@ const manifestPath = resolve(
29
29
  const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
30
30
 
31
31
  describe("computer-use skill manifest regression", () => {
32
- test("manifest has exactly 12 tools", () => {
32
+ test("manifest has exactly 11 tools", () => {
33
33
  expect(manifest.tools).toHaveLength(COMPUTER_USE_TOOL_COUNT);
34
34
  });
35
35
 
@@ -13,7 +13,6 @@ import {
13
13
  computerUseTypeTextTool,
14
14
  computerUseWaitTool,
15
15
  } from "../tools/computer-use/definitions.js";
16
- import { requestComputerControlTool } from "../tools/computer-use/request-computer-control.js";
17
16
  import { forwardComputerUseProxyTool } from "../tools/computer-use/skill-proxy-bridge.js";
18
17
  import type { ToolContext } from "../tools/types.js";
19
18
 
@@ -40,22 +39,20 @@ const ctx: ToolContext = {
40
39
  // ── Tool definitions ────────────────────────────────────────────────
41
40
 
42
41
  describe("computer-use tool definitions", () => {
43
- test("allComputerUseTools contains 10 tools", () => {
44
- expect(allComputerUseTools.length).toBe(10);
42
+ test("allComputerUseTools contains 11 tools", () => {
43
+ expect(allComputerUseTools.length).toBe(11);
45
44
  });
46
45
 
47
46
  test("all tools have proxy execution mode", () => {
48
47
  for (const tool of allComputerUseTools) {
49
48
  expect(tool.executionMode).toBe("proxy");
50
49
  }
51
- expect(requestComputerControlTool.executionMode).toBe("proxy");
52
50
  });
53
51
 
54
52
  test("all tools belong to computer-use category", () => {
55
53
  for (const tool of allComputerUseTools) {
56
54
  expect(tool.category).toBe("computer-use");
57
55
  }
58
- expect(requestComputerControlTool.category).toBe("computer-use");
59
56
  });
60
57
 
61
58
  test("all tools have unique names", () => {
@@ -225,20 +222,6 @@ describe("computer_use_respond", () => {
225
222
  });
226
223
  });
227
224
 
228
- // ── request_computer_control ────────────────────────────────────────
229
-
230
- describe("computer_use_request_control", () => {
231
- test("requires task parameter", () => {
232
- expect(schema(requestComputerControlTool).required).toContain("task");
233
- });
234
-
235
- test("execute throws proxy error", () => {
236
- expect(() => requestComputerControlTool.execute({}, ctx)).toThrow(
237
- "surfaceProxyResolver",
238
- );
239
- });
240
- });
241
-
242
225
  // ── skill-proxy-bridge ──────────────────────────────────────────────
243
226
 
244
227
  describe("forwardComputerUseProxyTool", () => {
@@ -54,7 +54,6 @@ mock.module("../util/logger.js", () => ({
54
54
  new Proxy({} as Record<string, unknown>, {
55
55
  get: () => () => {},
56
56
  }),
57
- isDebug: () => false,
58
57
  truncateForLog: (v: string) => v,
59
58
  }));
60
59
 
@@ -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