@vellumai/assistant 0.4.30 → 0.4.32

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 (194) hide show
  1. package/ARCHITECTURE.md +1 -1
  2. package/Dockerfile +14 -8
  3. package/README.md +2 -2
  4. package/docs/architecture/memory.md +28 -29
  5. package/docs/runbook-trusted-contacts.md +1 -4
  6. package/package.json +1 -1
  7. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +0 -7
  8. package/src/__tests__/anthropic-provider.test.ts +86 -1
  9. package/src/__tests__/assistant-feature-flags-integration.test.ts +2 -2
  10. package/src/__tests__/checker.test.ts +37 -98
  11. package/src/__tests__/commit-message-enrichment-service.test.ts +15 -4
  12. package/src/__tests__/config-schema.test.ts +6 -14
  13. package/src/__tests__/conflict-policy.test.ts +76 -0
  14. package/src/__tests__/conflict-store.test.ts +14 -20
  15. package/src/__tests__/contacts-tools.test.ts +8 -61
  16. package/src/__tests__/contradiction-checker.test.ts +5 -1
  17. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  18. package/src/__tests__/daemon-server-session-init.test.ts +1 -19
  19. package/src/__tests__/followup-tools.test.ts +0 -30
  20. package/src/__tests__/gemini-provider.test.ts +79 -1
  21. package/src/__tests__/guardian-decision-primitive-canonical.test.ts +5 -3
  22. package/src/__tests__/guardian-routing-invariants.test.ts +6 -4
  23. package/src/__tests__/ipc-snapshot.test.ts +0 -4
  24. package/src/__tests__/managed-proxy-context.test.ts +163 -0
  25. package/src/__tests__/memory-lifecycle-e2e.test.ts +13 -12
  26. package/src/__tests__/memory-regressions.test.ts +6 -6
  27. package/src/__tests__/openai-provider.test.ts +82 -0
  28. package/src/__tests__/provider-fail-open-selection.test.ts +134 -1
  29. package/src/__tests__/provider-managed-proxy-integration.test.ts +269 -0
  30. package/src/__tests__/recurrence-types.test.ts +0 -15
  31. package/src/__tests__/registry.test.ts +0 -10
  32. package/src/__tests__/schedule-tools.test.ts +28 -44
  33. package/src/__tests__/script-proxy-session-runtime.test.ts +6 -1
  34. package/src/__tests__/session-agent-loop.test.ts +0 -2
  35. package/src/__tests__/session-conflict-gate.test.ts +243 -388
  36. package/src/__tests__/session-profile-injection.test.ts +0 -2
  37. package/src/__tests__/session-runtime-assembly.test.ts +2 -3
  38. package/src/__tests__/session-skill-tools.test.ts +0 -49
  39. package/src/__tests__/session-workspace-cache-state.test.ts +0 -1
  40. package/src/__tests__/session-workspace-injection.test.ts +0 -1
  41. package/src/__tests__/session-workspace-tool-tracking.test.ts +0 -1
  42. package/src/__tests__/skill-feature-flags.test.ts +2 -2
  43. package/src/__tests__/task-management-tools.test.ts +111 -0
  44. package/src/__tests__/tool-grant-request-escalation.test.ts +2 -1
  45. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +2 -1
  46. package/src/__tests__/twilio-config.test.ts +0 -3
  47. package/src/amazon/session.ts +30 -91
  48. package/src/approvals/guardian-decision-primitive.ts +11 -7
  49. package/src/approvals/guardian-request-resolvers.ts +5 -3
  50. package/src/calls/call-controller.ts +423 -571
  51. package/src/calls/finalize-call.ts +20 -0
  52. package/src/calls/relay-access-wait.ts +340 -0
  53. package/src/calls/relay-server.ts +269 -899
  54. package/src/calls/relay-setup-router.ts +307 -0
  55. package/src/calls/relay-verification.ts +280 -0
  56. package/src/calls/twilio-config.ts +1 -8
  57. package/src/calls/voice-control-protocol.ts +184 -0
  58. package/src/calls/voice-session-bridge.ts +1 -8
  59. package/src/config/agent-schema.ts +1 -1
  60. package/src/config/bundled-skills/contacts/SKILL.md +7 -18
  61. package/src/config/bundled-skills/contacts/TOOLS.json +4 -20
  62. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +2 -4
  63. package/src/config/bundled-skills/contacts/tools/contact-search.ts +6 -12
  64. package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +3 -24
  65. package/src/config/bundled-skills/followups/TOOLS.json +0 -4
  66. package/src/config/bundled-skills/schedule/SKILL.md +1 -1
  67. package/src/config/bundled-skills/schedule/TOOLS.json +2 -10
  68. package/src/config/bundled-tool-registry.ts +0 -5
  69. package/src/config/core-schema.ts +1 -1
  70. package/src/config/env.ts +0 -10
  71. package/src/config/feature-flag-registry.json +1 -1
  72. package/src/config/loader.ts +19 -0
  73. package/src/config/memory-schema.ts +0 -10
  74. package/src/config/schema.ts +2 -2
  75. package/src/config/system-prompt.ts +6 -0
  76. package/src/contacts/contact-store.ts +36 -62
  77. package/src/contacts/contacts-write.ts +14 -3
  78. package/src/contacts/types.ts +9 -4
  79. package/src/daemon/handlers/config-heartbeat.ts +1 -2
  80. package/src/daemon/handlers/contacts.ts +2 -2
  81. package/src/daemon/handlers/guardian-actions.ts +1 -1
  82. package/src/daemon/handlers/session-history.ts +398 -0
  83. package/src/daemon/handlers/session-user-message.ts +982 -0
  84. package/src/daemon/handlers/sessions.ts +9 -1337
  85. package/src/daemon/ipc-contract/contacts.ts +2 -2
  86. package/src/daemon/ipc-contract/sessions.ts +0 -6
  87. package/src/daemon/ipc-contract-inventory.json +0 -1
  88. package/src/daemon/lifecycle.ts +0 -29
  89. package/src/daemon/session-agent-loop.ts +1 -45
  90. package/src/daemon/session-conflict-gate.ts +21 -82
  91. package/src/daemon/session-memory.ts +7 -52
  92. package/src/daemon/session-process.ts +3 -1
  93. package/src/daemon/session-runtime-assembly.ts +18 -35
  94. package/src/heartbeat/heartbeat-service.ts +5 -1
  95. package/src/home-base/app-link-store.ts +0 -7
  96. package/src/memory/conflict-intent.ts +3 -6
  97. package/src/memory/conflict-policy.ts +34 -0
  98. package/src/memory/conflict-store.ts +10 -18
  99. package/src/memory/contradiction-checker.ts +2 -2
  100. package/src/memory/conversation-attention-store.ts +1 -1
  101. package/src/memory/conversation-store.ts +0 -51
  102. package/src/memory/db-init.ts +8 -0
  103. package/src/memory/job-handlers/conflict.ts +24 -7
  104. package/src/memory/migrations/105-contacts-and-triage.ts +4 -7
  105. package/src/memory/migrations/134-contacts-notes-column.ts +68 -0
  106. package/src/memory/migrations/135-backfill-contact-interaction-stats.ts +31 -0
  107. package/src/memory/migrations/index.ts +2 -0
  108. package/src/memory/migrations/registry.ts +6 -0
  109. package/src/memory/recall-cache.ts +0 -5
  110. package/src/memory/schema/calls.ts +274 -0
  111. package/src/memory/schema/contacts.ts +125 -0
  112. package/src/memory/schema/conversations.ts +129 -0
  113. package/src/memory/schema/guardian.ts +172 -0
  114. package/src/memory/schema/index.ts +8 -0
  115. package/src/memory/schema/infrastructure.ts +205 -0
  116. package/src/memory/schema/memory-core.ts +196 -0
  117. package/src/memory/schema/notifications.ts +191 -0
  118. package/src/memory/schema/tasks.ts +78 -0
  119. package/src/memory/schema.ts +1 -1402
  120. package/src/memory/slack-thread-store.ts +0 -69
  121. package/src/messaging/index.ts +0 -1
  122. package/src/messaging/types.ts +0 -38
  123. package/src/notifications/decisions-store.ts +2 -105
  124. package/src/notifications/deliveries-store.ts +0 -11
  125. package/src/notifications/preferences-store.ts +1 -58
  126. package/src/permissions/checker.ts +6 -17
  127. package/src/providers/anthropic/client.ts +6 -2
  128. package/src/providers/gemini/client.ts +13 -2
  129. package/src/providers/managed-proxy/constants.ts +55 -0
  130. package/src/providers/managed-proxy/context.ts +77 -0
  131. package/src/providers/registry.ts +112 -0
  132. package/src/runtime/auth/__tests__/guard-tests.test.ts +51 -23
  133. package/src/runtime/guardian-action-service.ts +3 -2
  134. package/src/runtime/guardian-outbound-actions.ts +3 -3
  135. package/src/runtime/guardian-reply-router.ts +4 -4
  136. package/src/runtime/http-server.ts +83 -710
  137. package/src/runtime/http-types.ts +0 -16
  138. package/src/runtime/middleware/auth.ts +0 -12
  139. package/src/runtime/routes/app-routes.ts +33 -0
  140. package/src/runtime/routes/approval-routes.ts +32 -0
  141. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +6 -3
  142. package/src/runtime/routes/attachment-routes.ts +32 -0
  143. package/src/runtime/routes/brain-graph-routes.ts +27 -0
  144. package/src/runtime/routes/call-routes.ts +41 -0
  145. package/src/runtime/routes/channel-readiness-routes.ts +20 -0
  146. package/src/runtime/routes/channel-routes.ts +70 -0
  147. package/src/runtime/routes/contact-routes.ts +371 -29
  148. package/src/runtime/routes/conversation-attention-routes.ts +15 -0
  149. package/src/runtime/routes/conversation-routes.ts +192 -194
  150. package/src/runtime/routes/debug-routes.ts +15 -0
  151. package/src/runtime/routes/events-routes.ts +16 -0
  152. package/src/runtime/routes/global-search-routes.ts +17 -2
  153. package/src/runtime/routes/guardian-action-routes.ts +23 -1
  154. package/src/runtime/routes/guardian-approval-interception.ts +2 -1
  155. package/src/runtime/routes/guardian-bootstrap-routes.ts +26 -1
  156. package/src/runtime/routes/guardian-refresh-routes.ts +20 -0
  157. package/src/runtime/routes/identity-routes.ts +20 -0
  158. package/src/runtime/routes/inbound-message-handler.ts +8 -0
  159. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +5 -1
  160. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +2 -1
  161. package/src/runtime/routes/integration-routes.ts +83 -0
  162. package/src/runtime/routes/invite-routes.ts +31 -0
  163. package/src/runtime/routes/migration-routes.ts +47 -17
  164. package/src/runtime/routes/pairing-routes.ts +18 -0
  165. package/src/runtime/routes/secret-routes.ts +20 -0
  166. package/src/runtime/routes/surface-action-routes.ts +26 -0
  167. package/src/runtime/routes/trust-rules-routes.ts +31 -0
  168. package/src/runtime/routes/twilio-routes.ts +79 -0
  169. package/src/schedule/recurrence-types.ts +1 -11
  170. package/src/tools/followups/followup_create.ts +9 -3
  171. package/src/tools/mcp/mcp-tool-factory.ts +0 -17
  172. package/src/tools/memory/definitions.ts +0 -6
  173. package/src/tools/network/script-proxy/session-manager.ts +38 -3
  174. package/src/tools/schedule/create.ts +1 -3
  175. package/src/tools/schedule/update.ts +9 -6
  176. package/src/twitter/session.ts +29 -77
  177. package/src/util/cookie-session.ts +114 -0
  178. package/src/workspace/git-service.ts +6 -4
  179. package/src/__tests__/conversation-routes.test.ts +0 -99
  180. package/src/__tests__/get-weather.test.ts +0 -393
  181. package/src/__tests__/task-tools.test.ts +0 -685
  182. package/src/__tests__/weather-skill-regression.test.ts +0 -276
  183. package/src/autonomy/autonomy-resolver.ts +0 -62
  184. package/src/autonomy/autonomy-store.ts +0 -138
  185. package/src/autonomy/disposition-mapper.ts +0 -31
  186. package/src/autonomy/index.ts +0 -11
  187. package/src/autonomy/types.ts +0 -43
  188. package/src/config/bundled-skills/weather/SKILL.md +0 -38
  189. package/src/config/bundled-skills/weather/TOOLS.json +0 -36
  190. package/src/config/bundled-skills/weather/icon.svg +0 -24
  191. package/src/config/bundled-skills/weather/tools/get-weather.ts +0 -12
  192. package/src/contacts/startup-migration.ts +0 -21
  193. package/src/messaging/triage-engine.ts +0 -344
  194. package/src/tools/weather/service.ts +0 -712
package/ARCHITECTURE.md CHANGED
@@ -883,7 +883,7 @@ graph LR
883
883
  C5["user_message<br/>text, attachments"]
884
884
  C6["confirmation_response<br/>decision"]
885
885
  C7["cancel / undo"]
886
- C8["model_get / model_set<br/>sandbox_set (deprecated no-op)"]
886
+ C8["model_get / model_set"]
887
887
  C9["ping"]
888
888
  C10["ipc_blob_probe<br/>probeId, nonceSha256"]
889
889
  C11["work_items_list / work_item_get<br/>work_item_create / work_item_update<br/>work_item_complete / work_item_run_task<br/>(planned)"]
package/Dockerfile CHANGED
@@ -16,16 +16,21 @@ RUN apt-get update && apt-get install -y \
16
16
  RUN curl -fsSL https://bun.sh/install | bash
17
17
  ENV PATH="/root/.bun/bin:${PATH}"
18
18
 
19
- # Copy package files
20
- COPY package.json bun.lock ./
19
+ # Install assistant and CLI dependencies first for cache reuse
20
+ COPY assistant/package.json assistant/bun.lock ./assistant/
21
+ RUN cd /app/assistant && bun install --frozen-lockfile
21
22
 
22
- # Install dependencies
23
- RUN bun install --frozen-lockfile
23
+ COPY cli/package.json cli/bun.lock ./cli/
24
+ RUN cd /app/cli && bun install --frozen-lockfile
25
+
26
+ # Copy source
27
+ COPY assistant ./assistant
28
+ COPY cli ./cli
24
29
 
25
30
  # Final stage
26
31
  FROM debian:trixie@sha256:3615a749858a1cba49b408fb49c37093db813321355a9ab7c1f9f4836341e9db AS runner
27
32
 
28
- WORKDIR /app
33
+ WORKDIR /app/assistant
29
34
 
30
35
  # Install runtime dependencies for Playwright and tree-sitter
31
36
  RUN apt-get update && apt-get install -y \
@@ -47,6 +52,10 @@ RUN apt-get update && apt-get install -y \
47
52
  COPY --from=builder /root/.bun/bin/bun /usr/local/bin/bun
48
53
  RUN ln -sf /usr/local/bin/bun /usr/local/bin/bunx
49
54
 
55
+ # Install a vellum CLI launcher backed by the bundled local cli package
56
+ RUN printf '#!/usr/bin/env sh\nexec bun run /app/cli/src/index.ts "$@"\n' > /usr/local/bin/vellum && \
57
+ chmod +x /usr/local/bin/vellum
58
+
50
59
  # Create non-root user that also has sudo access so it can like install stuff
51
60
  RUN groupadd --system --gid 1001 assistant && \
52
61
  useradd --system --uid 1001 --gid assistant --create-home --shell /bin/bash assistant && \
@@ -95,8 +104,5 @@ ENV IS_CONTAINERIZED=true
95
104
  # Copy from builder
96
105
  COPY --from=builder /app /app
97
106
 
98
- # Copy source
99
- COPY . .
100
-
101
107
  # Run the daemon + http server
102
108
  CMD ["bun", "run", "src/daemon/main.ts"]
package/README.md CHANGED
@@ -481,7 +481,7 @@ bun run db:push # Apply migrations
481
481
 
482
482
  ```bash
483
483
  # Build production image
484
- docker build -t vellum-assistant:local assistant
484
+ docker build -f assistant/Dockerfile -t vellum-assistant:local .
485
485
 
486
486
  # Run
487
487
  docker run --rm -p 3001:3001 \
@@ -489,7 +489,7 @@ docker run --rm -p 3001:3001 \
489
489
  vellum-assistant:local
490
490
  ```
491
491
 
492
- The image runs as non-root user `assistant` (uid 1001) and exposes port `3001`.
492
+ The image exposes port `3001` and bundles the `vellum` CLI binary.
493
493
 
494
494
  ## Troubleshooting
495
495
 
@@ -41,7 +41,7 @@ graph TB
41
41
 
42
42
  subgraph "Read Path (Memory Recall)"
43
43
  QUERY["Recall Query Builder<br/>User request + compacted context summary"]
44
- CONFLICT_GATE["Soft Conflict Gate<br/>dismiss non-actionable conflicts (kind + statement policy)<br/>resolve pending conflicts from user turn<br/>relevance + cooldown + configurable ask behavior"]
44
+ CONFLICT_GATE["Soft Conflict Gate<br/>dismiss non-actionable conflicts (kind + statement + provenance policy)<br/>attempt internal resolution from user turn<br/>relevance-based; never produces user-facing prompts"]
45
45
  PROFILE_BUILD["Dynamic Profile Compiler<br/>active trusted profile memories<br/>user_confirmed > user_reported > assistant_inferred"]
46
46
  PROFILE_INJECT["Inject profile context block<br/>into runtime user tail<br/>(strict token cap)"]
47
47
  BUDGET["Dynamic Recall Budget<br/>computeRecallBudget()<br/>from prompt headroom"]
@@ -158,28 +158,26 @@ The key distinction: normal compaction is a cost-optimized background process th
158
158
 
159
159
  ### Memory Retrieval Config Knobs (Defaults)
160
160
 
161
- | Config key | Default | Purpose |
162
- | --------------------------------------------------------- | ----------------------------------------------------------------: | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
163
- | `memory.retrieval.dynamicBudget.enabled` | `true` | Toggle per-turn recall budget calculation from live prompt headroom. |
164
- | `memory.retrieval.dynamicBudget.minInjectTokens` | `1200` | Lower clamp for computed recall injection budget. |
165
- | `memory.retrieval.dynamicBudget.maxInjectTokens` | `10000` | Upper clamp for computed recall injection budget. |
166
- | `memory.retrieval.dynamicBudget.targetHeadroomTokens` | `10000` | Reserved headroom to keep free for response generation/tool traces. |
167
- | `memory.entity.extractRelations.enabled` | `true` | Enable relation edge extraction and persistence in `memory_entity_relations`. |
168
- | `memory.entity.extractRelations.backfillBatchSize` | `200` | Batch size for checkpointed `backfill_entity_relations` jobs. |
169
- | `memory.entity.relationRetrieval.enabled` | `true` | Enable one-hop relation expansion from matched seed entities at recall time. |
170
- | `memory.entity.relationRetrieval.maxSeedEntities` | `8` | Maximum matched seed entities from the query. |
171
- | `memory.entity.relationRetrieval.maxNeighborEntities` | `20` | Maximum unique neighbor entities expanded from relation edges. |
172
- | `memory.entity.relationRetrieval.maxEdges` | `40` | Maximum relation edges traversed during expansion. |
173
- | `memory.entity.relationRetrieval.neighborScoreMultiplier` | `0.7` | Downweight multiplier for relation-expanded candidates vs direct entity hits. |
174
- | `memory.conflicts.enabled` | `true` | Enable soft conflict gate for unresolved `memory_item_conflicts`. |
175
- | `memory.conflicts.reaskCooldownTurns` | `3` | Minimum turn distance before re-asking the same conflict clarification. |
176
- | `memory.conflicts.resolverLlmTimeoutMs` | `12000` | Timeout bound for clarification resolver LLM fallback. |
177
- | `memory.conflicts.relevanceThreshold` | `0.3` | Similarity threshold for deciding whether a pending conflict is relevant to the current request. |
178
- | `memory.conflicts.gateMode` | `'soft'` | Conflict gate strategy. Currently only `'soft'` is supported (asks the user inline). |
179
- | `memory.conflicts.askOnIrrelevantTurns` | `false` | When `true`, soft-inject irrelevant conflict clarifications into every turn. When `false` (default), only ask when the conflict is topically relevant. |
180
- | `memory.conflicts.conflictableKinds` | `['preference', 'profile', 'constraint', 'instruction', 'style']` | Memory item kinds eligible for conflict detection. Items with kinds outside this list are auto-dismissed. |
181
- | `memory.profile.enabled` | `true` | Enable dynamic profile compilation from active trusted profile/preference/constraint/instruction memories. |
182
- | `memory.profile.maxInjectTokens` | `800` | Hard token cap enforced by `ProfileCompiler` when generating the runtime profile block. |
161
+ | Config key | Default | Purpose |
162
+ | --------------------------------------------------------- | ----------------------------------------------------------------: | ------------------------------------------------------------------------------------------------------------------ |
163
+ | `memory.retrieval.dynamicBudget.enabled` | `true` | Toggle per-turn recall budget calculation from live prompt headroom. |
164
+ | `memory.retrieval.dynamicBudget.minInjectTokens` | `1200` | Lower clamp for computed recall injection budget. |
165
+ | `memory.retrieval.dynamicBudget.maxInjectTokens` | `10000` | Upper clamp for computed recall injection budget. |
166
+ | `memory.retrieval.dynamicBudget.targetHeadroomTokens` | `10000` | Reserved headroom to keep free for response generation/tool traces. |
167
+ | `memory.entity.extractRelations.enabled` | `true` | Enable relation edge extraction and persistence in `memory_entity_relations`. |
168
+ | `memory.entity.extractRelations.backfillBatchSize` | `200` | Batch size for checkpointed `backfill_entity_relations` jobs. |
169
+ | `memory.entity.relationRetrieval.enabled` | `true` | Enable one-hop relation expansion from matched seed entities at recall time. |
170
+ | `memory.entity.relationRetrieval.maxSeedEntities` | `8` | Maximum matched seed entities from the query. |
171
+ | `memory.entity.relationRetrieval.maxNeighborEntities` | `20` | Maximum unique neighbor entities expanded from relation edges. |
172
+ | `memory.entity.relationRetrieval.maxEdges` | `40` | Maximum relation edges traversed during expansion. |
173
+ | `memory.entity.relationRetrieval.neighborScoreMultiplier` | `0.7` | Downweight multiplier for relation-expanded candidates vs direct entity hits. |
174
+ | `memory.conflicts.enabled` | `true` | Enable soft conflict gate for unresolved `memory_item_conflicts`. |
175
+ | `memory.conflicts.resolverLlmTimeoutMs` | `12000` | Timeout bound for clarification resolver LLM fallback. |
176
+ | `memory.conflicts.relevanceThreshold` | `0.3` | Similarity threshold for deciding whether a pending conflict is relevant to the current request. |
177
+ | `memory.conflicts.gateMode` | `'soft'` | Conflict gate strategy. Currently only `'soft'` is supported (resolves conflicts internally without user prompts). |
178
+ | `memory.conflicts.conflictableKinds` | `['preference', 'profile', 'constraint', 'instruction', 'style']` | Memory item kinds eligible for conflict detection. Items with kinds outside this list are auto-dismissed. |
179
+ | `memory.profile.enabled` | `true` | Enable dynamic profile compilation from active trusted profile/preference/constraint/instruction memories. |
180
+ | `memory.profile.maxInjectTokens` | `800` | Hard token cap enforced by `ProfileCompiler` when generating the runtime profile block. |
183
181
 
184
182
  ### Memory Recall Debugging Playbook
185
183
 
@@ -211,7 +209,7 @@ The key distinction: normal compaction is a cost-optimized background process th
211
209
  stateDiagram-v2
212
210
  [*] --> ActiveItems : extract_items/check_contradictions
213
211
  ActiveItems --> PendingConflict : ambiguous_contradiction\n(candidate -> pending_clarification)
214
- PendingConflict --> PendingConflict : soft gate ask\n(reask cooldown + relevance + askOnIrrelevantTurns)
212
+ PendingConflict --> PendingConflict : internal evaluation\n(relevance check, no user prompt)
215
213
  PendingConflict --> Dismissed : non-actionable\n(kind policy + transient statement filter)
216
214
  PendingConflict --> ResolvedKeepExisting : clarification resolver\n+ applyConflictResolution
217
215
  PendingConflict --> ResolvedKeepCandidate : clarification resolver\n+ applyConflictResolution
@@ -224,6 +222,10 @@ stateDiagram-v2
224
222
  SupersededItems --> CleanupItems : cleanup_stale_superseded_items
225
223
  ```
226
224
 
225
+ ### Internal-Only Conflict Handling
226
+
227
+ Memory conflict resolution is entirely internal and non-interruptive. The conflict gate evaluates pending conflicts on each turn, dismisses non-actionable ones (based on kind policy, statement eligibility, coherence, and provenance), and attempts resolution when user input looks like a natural clarification. At no point does the conflict system produce user-facing clarification prompts, inject conflict instructions into the assistant's response, or block the user's request. The user is never aware that a conflict exists; the runtime response path always continues answering the user's actual request. This invariant is enforced across the conflict gate (`session-conflict-gate.ts`), session memory (`session-memory.ts`), session agent loop (`session-agent-loop.ts`), and runtime assembly (`session-runtime-assembly.ts`).
228
+
227
229
  Runtime profile flow (per turn):
228
230
 
229
231
  1. `ProfileCompiler` builds a trusted profile block from active `profile` / `preference` / `constraint` / `instruction` items under strict token cap.
@@ -238,7 +240,7 @@ Two trust gates enforce trust-class-based access control over the memory pipelin
238
240
 
239
241
  - **Write gate** (`indexer.ts`): The `extract_items` and `resolve_conflicts` jobs only run for messages from trusted actors (guardian or undefined provenance). Messages from untrusted actors (`trusted_contact`, `unknown`) are still segmented and embedded — so they appear in conversation context — but no profile extraction or conflict resolution is triggered. This prevents untrusted channels from injecting or mutating long-term memory items.
240
242
 
241
- - **Read gate** (`session-memory.ts`): When the current session's actor is untrusted, the memory recall pipeline returns a no-op context — no recall injection, no dynamic profile, no conflict clarification prompts. This ensures untrusted actors cannot surface or exploit previously extracted memory.
243
+ - **Read gate** (`session-memory.ts`): When the current session's actor is untrusted, the memory recall pipeline returns a no-op context — no recall injection, no dynamic profile, no conflict resolution. This ensures untrusted actors cannot surface or exploit previously extracted memory.
242
244
 
243
245
  Trust policy is **cross-channel and trust-class-based**: decisions use `trustContext.trustClass`, not the channel string. Desktop/IPC sessions default to `trustClass: 'guardian'`. External channels (Telegram, SMS, WhatsApp, voice) provide explicit trust context via the resolver. Messages without provenance metadata are treated as trusted (guardian); all new messages carry provenance.
244
246
 
@@ -381,7 +383,7 @@ graph TB
381
383
  - **Prepend, not append**: The workspace block is prepended to the user message content so that Anthropic cache breakpoints continue to land on the trailing user text block, preserving prompt cache efficiency.
382
384
  - **Runtime-only**: The injected `<workspace_top_level>` block is stripped from `this.messages` after the agent loop completes. It never persists in conversation history or the database.
383
385
  - **Dirty-refresh**: The scanner runs once on the first turn, then only re-runs after a successful mutation tool (`file_edit`, `file_write`, `bash`). Failed tool results do not trigger a refresh.
384
- - **Injection ordering**: Workspace context is injected after other runtime injections (soft conflict instruction, active surface) via `applyRuntimeInjections`, but because it is **prepended** to content blocks, it appears first in the final message.
386
+ - **Injection ordering**: Workspace context is injected after other runtime injections (active surface, etc.) via `applyRuntimeInjections`, but because it is **prepended** to content blocks, it appears first in the final message.
385
387
 
386
388
  ### Cache compatibility
387
389
 
@@ -506,7 +508,6 @@ graph TB
506
508
  10. **Provider-aware commit message generation (optional)**: When `workspaceGit.commitMessageLLM.enabled` is `true`, turn-boundary commits attempt to generate a descriptive commit message using the configured LLM provider before falling back to deterministic messages. The feature ships disabled by default and is designed to never degrade turn completion guarantees.
507
509
 
508
510
  **Commit message LLM fallback chain**: The generator runs a sequence of pre-flight checks before calling the LLM. Each check that fails produces a machine-readable `llmFallbackReason` in the structured log output and immediately returns a deterministic message. The checks, in order:
509
-
510
511
  1. `disabled` — `commitMessageLLM.enabled` is `false` or `useConfiguredProvider` is `false`
511
512
  2. `missing_provider_api_key` — the configured provider's API key is not set in `config.apiKeys` (skipped for keyless providers like Ollama that run without an API key)
512
513
  3. `breaker_open` — the generator's internal circuit breaker is open after consecutive LLM failures (exponential backoff)
@@ -516,11 +517,9 @@ graph TB
516
517
  7. `timeout` — the LLM call exceeded `timeoutMs` (AbortController fires)
517
518
  8. `provider_error` — the provider threw an exception during the LLM call
518
519
  9. `invalid_output` — the LLM returned empty text, the literal string "FALLBACK", or total output > 500 chars
519
-
520
520
  - **Subject line capping**: If the LLM subject line exceeds 72 chars it is deterministically truncated to 72 chars. This is NOT treated as a failure (no breaker penalty, no deterministic fallback).
521
521
 
522
522
  **Fast model resolution**: The LLM call uses a small/fast model to minimize latency and cost. The model is resolved **before** any provider call as a pre-flight check:
523
-
524
523
  - If `commitMessageLLM.providerFastModelOverrides[provider]` is set, that model is used.
525
524
  - Otherwise, a built-in default is used: `anthropic` -> `claude-haiku-4-5-20251001`, `openai` -> `gpt-4o-mini`, `gemini` -> `gemini-2.0-flash`.
526
525
  - If the configured provider has no override and no built-in default (e.g., `ollama`, `fireworks`, `openrouter`), the generator returns a deterministic fallback with reason `missing_fast_model` and the provider is never called. To enable LLM commit messages for such providers, set `providerFastModelOverrides[provider]` to the desired model.
@@ -62,10 +62,7 @@ Response shape:
62
62
  {
63
63
  "id": "uuid",
64
64
  "displayName": "Alice",
65
- "relationship": "friend",
66
- "importance": 0.5,
67
- "responseExpectation": null,
68
- "preferredTone": null,
65
+ "notes": null,
69
66
  "lastInteraction": 1700000000000,
70
67
  "interactionCount": 12,
71
68
  "createdAt": 1699000000000,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/assistant",
3
- "version": "0.4.30",
3
+ "version": "0.4.32",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "vellum": "./src/index.ts"
@@ -145,13 +145,6 @@ exports[`IPC message snapshots ClientMessage types usage_request serializes to e
145
145
  }
146
146
  `;
147
147
 
148
- exports[`IPC message snapshots ClientMessage types sandbox_set serializes to expected JSON 1`] = `
149
- {
150
- "enabled": true,
151
- "type": "sandbox_set",
152
- }
153
- `;
154
-
155
148
  exports[`IPC message snapshots ClientMessage types cu_session_create serializes to expected JSON 1`] = `
156
149
  {
157
150
  "screenHeight": 1080,
@@ -8,6 +8,7 @@ import type { Message, ToolDefinition } from "../providers/types.js";
8
8
 
9
9
  let lastStreamParams: Record<string, unknown> | null = null;
10
10
  let _lastStreamOptions: Record<string, unknown> | null = null;
11
+ let lastConstructorArgs: Record<string, unknown> | null = null;
11
12
 
12
13
  const fakeResponse = {
13
14
  content: [{ type: "text", text: "Hello" }],
@@ -33,7 +34,9 @@ class FakeAPIError extends Error {
33
34
  mock.module("@anthropic-ai/sdk", () => ({
34
35
  default: class MockAnthropic {
35
36
  static APIError = FakeAPIError;
36
- constructor() {}
37
+ constructor(args: Record<string, unknown>) {
38
+ lastConstructorArgs = { ...args };
39
+ }
37
40
  messages = {
38
41
  stream: (
39
42
  params: Record<string, unknown>,
@@ -127,6 +130,7 @@ describe("AnthropicProvider — Cache-Control Characterization", () => {
127
130
  beforeEach(() => {
128
131
  lastStreamParams = null;
129
132
  _lastStreamOptions = null;
133
+ lastConstructorArgs = null;
130
134
  provider = new AnthropicProvider("sk-ant-test", "claude-sonnet-4-6");
131
135
  });
132
136
 
@@ -935,3 +939,84 @@ describe("AnthropicProvider — Cache-Control Characterization", () => {
935
939
  expect(userMsgs[2].content[1].cache_control).toEqual({ type: "ephemeral" });
936
940
  });
937
941
  });
942
+
943
+ // ---------------------------------------------------------------------------
944
+ // Tests — Managed Proxy Fallback
945
+ // ---------------------------------------------------------------------------
946
+
947
+ describe("AnthropicProvider — Managed Proxy Fallback", () => {
948
+ beforeEach(() => {
949
+ lastStreamParams = null;
950
+ _lastStreamOptions = null;
951
+ lastConstructorArgs = null;
952
+ });
953
+
954
+ test("constructor passes baseURL to Anthropic SDK when provided", () => {
955
+ new AnthropicProvider("managed-key", "claude-sonnet-4-6", {
956
+ baseURL: "https://platform.example.com/v1/runtime-proxy/anthropic",
957
+ });
958
+
959
+ expect(lastConstructorArgs).not.toBeNull();
960
+ expect(lastConstructorArgs!.apiKey).toBe("managed-key");
961
+ expect(lastConstructorArgs!.baseURL).toBe(
962
+ "https://platform.example.com/v1/runtime-proxy/anthropic",
963
+ );
964
+ });
965
+
966
+ test("constructor does not set baseURL when option is omitted", () => {
967
+ new AnthropicProvider("sk-ant-user-key", "claude-sonnet-4-6");
968
+
969
+ expect(lastConstructorArgs).not.toBeNull();
970
+ expect(lastConstructorArgs!.apiKey).toBe("sk-ant-user-key");
971
+ expect(lastConstructorArgs!.baseURL).toBeUndefined();
972
+ });
973
+
974
+ test("managed mode provider preserves tool-pairing behavior", async () => {
975
+ const provider = new AnthropicProvider("managed-key", "claude-sonnet-4-6", {
976
+ baseURL: "https://platform.example.com/v1/runtime-proxy/anthropic",
977
+ });
978
+
979
+ const messages: Message[] = [
980
+ userMsg("Read file"),
981
+ toolUseMsg("tu_1", "file_read"),
982
+ toolResultMsg("tu_1", "file contents"),
983
+ ];
984
+ await provider.sendMessage(messages);
985
+
986
+ const sent = lastStreamParams!.messages as Array<{
987
+ role: string;
988
+ content: Array<{ type: string; tool_use_id?: string }>;
989
+ }>;
990
+
991
+ expect(sent).toHaveLength(3);
992
+ const toolResults = sent[2].content.filter((b) => b.type === "tool_result");
993
+ expect(toolResults).toHaveLength(1);
994
+ expect(toolResults[0].tool_use_id).toBe("tu_1");
995
+ });
996
+
997
+ test("managed mode provider preserves cache-control behavior", async () => {
998
+ const provider = new AnthropicProvider("managed-key", "claude-sonnet-4-6", {
999
+ baseURL: "https://platform.example.com/v1/runtime-proxy/anthropic",
1000
+ });
1001
+
1002
+ await provider.sendMessage(
1003
+ [userMsg("Hi")],
1004
+ sampleTools,
1005
+ "You are helpful.",
1006
+ );
1007
+
1008
+ // System prompt cache control
1009
+ const system = lastStreamParams!.system as Array<{
1010
+ cache_control?: { type: string };
1011
+ }>;
1012
+ expect(system[0].cache_control).toEqual({ type: "ephemeral" });
1013
+
1014
+ // Last tool cache control
1015
+ const tools = lastStreamParams!.tools as Array<{
1016
+ cache_control?: { type: string };
1017
+ }>;
1018
+ expect(tools[tools.length - 1].cache_control).toEqual({
1019
+ type: "ephemeral",
1020
+ });
1021
+ });
1022
+ });
@@ -241,8 +241,8 @@ describe("buildSystemPrompt assistant feature flag filtering", () => {
241
241
 
242
242
  const result = buildSystemPrompt();
243
243
 
244
- // browser is declared in the registry with defaultEnabled: false
245
- expect(result).not.toContain('id="browser"');
244
+ // browser is declared in the registry with defaultEnabled: true
245
+ expect(result).toContain('id="browser"');
246
246
  });
247
247
  });
248
248
 
@@ -55,16 +55,16 @@ mock.module("../util/logger.js", () => ({
55
55
  }));
56
56
 
57
57
  // Mutable config object so tests can switch permissions.mode between
58
- // 'legacy', 'strict', and 'workspace' without re-registering the mock.
58
+ // 'strict' and 'workspace' without re-registering the mock.
59
59
  interface TestConfig {
60
- permissions: { mode: "legacy" | "strict" | "workspace" };
60
+ permissions: { mode: "strict" | "workspace" };
61
61
  skills: { load: { extraDirs: string[] } };
62
62
  sandbox: { enabled: boolean };
63
63
  [key: string]: unknown;
64
64
  }
65
65
 
66
66
  const testConfig: TestConfig = {
67
- permissions: { mode: "legacy" },
67
+ permissions: { mode: "workspace" },
68
68
  skills: { load: { extraDirs: [] } },
69
69
  sandbox: { enabled: true },
70
70
  };
@@ -81,7 +81,6 @@ mock.module("../config/loader.js", () => ({
81
81
  }));
82
82
 
83
83
  import {
84
- _resetLegacyDeprecationWarning,
85
84
  check,
86
85
  classifyRisk,
87
86
  generateAllowlistOptions,
@@ -169,11 +168,9 @@ describe("Permission Checker", () => {
169
168
  beforeEach(() => {
170
169
  // Reset trust-store state between tests
171
170
  clearCache();
172
- // Reset permissions mode to legacy so existing tests are not affected
173
- testConfig.permissions = { mode: "legacy" };
171
+ // Reset permissions mode to workspace (default) so existing tests are not affected
172
+ testConfig.permissions = { mode: "workspace" };
174
173
  testConfig.skills = { load: { extraDirs: [] } };
175
- // Reset the one-time legacy deprecation warning flag and captured log calls
176
- _resetLegacyDeprecationWarning();
177
174
  loggerWarnCalls.length = 0;
178
175
  try {
179
176
  rmSync(join(checkerTestDir, "protected", "trust.json"));
@@ -684,12 +681,22 @@ describe("Permission Checker", () => {
684
681
  expect(result.decision).toBe("allow");
685
682
  });
686
683
 
687
- test("file_write with no rule → prompt", async () => {
684
+ test("file_write within workspace with no rule → auto-allowed in workspace mode", async () => {
688
685
  const result = await check(
689
686
  "file_write",
690
687
  { path: "/tmp/file.txt" },
691
688
  "/tmp",
692
689
  );
690
+ expect(result.decision).toBe("allow");
691
+ expect(result.reason).toContain("workspace-scoped");
692
+ });
693
+
694
+ test("file_write outside workspace with no rule → prompt", async () => {
695
+ const result = await check(
696
+ "file_write",
697
+ { path: "/etc/some-file.txt" },
698
+ "/tmp",
699
+ );
693
700
  expect(result.decision).toBe("prompt");
694
701
  });
695
702
 
@@ -1354,12 +1361,10 @@ describe("Permission Checker", () => {
1354
1361
  });
1355
1362
 
1356
1363
  test("core tool (no origin) still follows risk-based fallback", async () => {
1357
- // file_read is a core tool with Low risk should auto-allow as before
1358
- const result = await check(
1359
- "file_read",
1360
- { path: "/tmp/test.txt" },
1361
- "/tmp",
1362
- );
1364
+ // file_read is a core tool with Low risk in workspace mode,
1365
+ // workspace-scoped invocations are auto-allowed before risk fallback.
1366
+ // Use a path outside the workspace to test the risk-based fallback.
1367
+ const result = await check("file_read", { path: "/etc/hosts" }, "/tmp");
1363
1368
  expect(result.decision).toBe("allow");
1364
1369
  expect(result.reason).toContain("Low risk");
1365
1370
  });
@@ -1488,7 +1493,8 @@ describe("Permission Checker", () => {
1488
1493
 
1489
1494
  test("file_write of non-workspace file is not auto-allowed", async () => {
1490
1495
  const otherPath = join(checkerTestDir, "workspace", "OTHER.md");
1491
- const result = await check("file_write", { path: otherPath }, "/tmp");
1496
+ // Use a workingDir that doesn't contain the path so it's not workspace-scoped
1497
+ const result = await check("file_write", { path: otherPath }, "/home");
1492
1498
  // Medium risk with no matching allow rule → prompt
1493
1499
  expect(result.decision).toBe("prompt");
1494
1500
  });
@@ -2565,7 +2571,7 @@ describe("Permission Checker", () => {
2565
2571
  });
2566
2572
 
2567
2573
  test("legacy mode: file_write to skill source still prompts as High risk", async () => {
2568
- testConfig.permissions.mode = "legacy";
2574
+ testConfig.permissions.mode = "workspace";
2569
2575
  ensureSkillsDir();
2570
2576
  const skillPath = join(
2571
2577
  checkerTestDir,
@@ -3327,7 +3333,7 @@ describe("Permission Checker", () => {
3327
3333
  });
3328
3334
 
3329
3335
  test("skill_load auto-allows in legacy mode (backward compat)", async () => {
3330
- testConfig.permissions.mode = "legacy";
3336
+ testConfig.permissions.mode = "workspace";
3331
3337
  const result = await check("skill_load", { skill: "any-skill" }, "/tmp");
3332
3338
  expect(result.decision).toBe("allow");
3333
3339
  // The default allow rule matches before the Low risk fallback
@@ -3850,7 +3856,7 @@ describe("Permission Checker", () => {
3850
3856
 
3851
3857
  describe("Invariant 6: user can set broad rules if they choose", () => {
3852
3858
  test("wildcard allow rule matches any command in legacy mode", async () => {
3853
- testConfig.permissions.mode = "legacy";
3859
+ testConfig.permissions.mode = "workspace";
3854
3860
  addRule("bash", "*", "everywhere");
3855
3861
  const result = await check(
3856
3862
  "bash",
@@ -4203,7 +4209,7 @@ describe("Permission Checker", () => {
4203
4209
  }
4204
4210
 
4205
4211
  test("browser tools are auto-allowed in legacy mode", async () => {
4206
- testConfig.permissions = { mode: "legacy" };
4212
+ testConfig.permissions = { mode: "workspace" };
4207
4213
  for (const toolName of browserToolNames) {
4208
4214
  const result = await check(toolName, {}, "/tmp");
4209
4215
  expect(result.decision).toBe("allow");
@@ -4218,7 +4224,7 @@ describe("Permission Checker", () => {
4218
4224
  expect(result.decision).toBe("allow");
4219
4225
  }
4220
4226
  } finally {
4221
- testConfig.permissions = { mode: "legacy" };
4227
+ testConfig.permissions = { mode: "workspace" };
4222
4228
  }
4223
4229
  });
4224
4230
  });
@@ -4295,7 +4301,7 @@ describe("Permission Checker", () => {
4295
4301
  describe("bash network_mode=proxied — no special-casing", () => {
4296
4302
  beforeEach(() => {
4297
4303
  clearCache();
4298
- testConfig.permissions = { mode: "legacy" };
4304
+ testConfig.permissions = { mode: "workspace" };
4299
4305
  testConfig.skills = { load: { extraDirs: [] } };
4300
4306
  });
4301
4307
 
@@ -4416,7 +4422,7 @@ describe("computer-use tool permission defaults", () => {
4416
4422
  describe("scope matching behavior", () => {
4417
4423
  beforeEach(() => {
4418
4424
  clearCache();
4419
- testConfig.permissions = { mode: "legacy" };
4425
+ testConfig.permissions = { mode: "workspace" };
4420
4426
  try {
4421
4427
  rmSync(join(checkerTestDir, "protected", "trust.json"));
4422
4428
  } catch {
@@ -4456,6 +4462,8 @@ describe("scope matching behavior", () => {
4456
4462
  });
4457
4463
 
4458
4464
  test("project-scoped rule does NOT match invocations from sibling directory", async () => {
4465
+ // Use strict mode to test rule-matching isolation without workspace auto-allow
4466
+ testConfig.permissions.mode = "strict";
4459
4467
  const projectDir = "/home/user/my-project";
4460
4468
  // Use a broad pattern that matches any file, scoped to the project
4461
4469
  addRule("file_write", "file_write:*", projectDir);
@@ -4470,6 +4478,8 @@ describe("scope matching behavior", () => {
4470
4478
  });
4471
4479
 
4472
4480
  test("project-scoped rule does NOT match invocations from parent directory", async () => {
4481
+ // Use strict mode to test rule-matching isolation without workspace auto-allow
4482
+ testConfig.permissions.mode = "strict";
4473
4483
  const projectDir = "/home/user/my-project";
4474
4484
  addRule("file_write", "file_write:*", projectDir);
4475
4485
 
@@ -4483,6 +4493,8 @@ describe("scope matching behavior", () => {
4483
4493
  });
4484
4494
 
4485
4495
  test("project-scoped rule does NOT match directory with shared prefix", async () => {
4496
+ // Use strict mode to test rule-matching isolation without workspace auto-allow
4497
+ testConfig.permissions.mode = "strict";
4486
4498
  // A rule for /home/user/project should NOT match /home/user/project-evil
4487
4499
  // (directory-boundary enforcement in matchesScope)
4488
4500
  const projectDir = "/home/user/project";
@@ -4562,7 +4574,7 @@ describe("workspace mode — auto-allow workspace-scoped operations", () => {
4562
4574
  });
4563
4575
 
4564
4576
  afterEach(() => {
4565
- testConfig.permissions = { mode: "legacy" };
4577
+ testConfig.permissions = { mode: "workspace" };
4566
4578
  });
4567
4579
 
4568
4580
  // ── workspace-scoped file operations auto-allow ──────────────────
@@ -4771,79 +4783,6 @@ describe("workspace mode — auto-allow workspace-scoped operations", () => {
4771
4783
  });
4772
4784
  });
4773
4785
 
4774
- // ── legacy mode deprecation warning ─────────────────────────────────────
4775
-
4776
- describe("legacy mode — deprecation warning", () => {
4777
- beforeEach(() => {
4778
- clearCache();
4779
- _resetLegacyDeprecationWarning();
4780
- loggerWarnCalls.length = 0;
4781
- testConfig.permissions = { mode: "legacy" };
4782
- testConfig.skills = { load: { extraDirs: [] } };
4783
- try {
4784
- rmSync(join(checkerTestDir, "protected", "trust.json"));
4785
- } catch {
4786
- /* may not exist */
4787
- }
4788
- });
4789
-
4790
- afterEach(() => {
4791
- testConfig.permissions = { mode: "legacy" };
4792
- });
4793
-
4794
- test("emits deprecation warning on first check() call in legacy mode", async () => {
4795
- await check("file_read", { file_path: "/tmp/test.txt" }, "/tmp");
4796
- expect(loggerWarnCalls.some((m) => m.includes("deprecated"))).toBe(true);
4797
- expect(loggerWarnCalls.some((m) => m.includes("legacy"))).toBe(true);
4798
- });
4799
-
4800
- test("deprecation warning fires only once per process", async () => {
4801
- await check("file_read", { file_path: "/tmp/a.txt" }, "/tmp");
4802
- const firstCount = loggerWarnCalls.filter((m) =>
4803
- m.includes("deprecated"),
4804
- ).length;
4805
- expect(firstCount).toBe(1);
4806
-
4807
- await check("file_read", { file_path: "/tmp/b.txt" }, "/tmp");
4808
- const secondCount = loggerWarnCalls.filter((m) =>
4809
- m.includes("deprecated"),
4810
- ).length;
4811
- expect(secondCount).toBe(1);
4812
- });
4813
-
4814
- test("no deprecation warning in workspace mode", async () => {
4815
- testConfig.permissions = { mode: "workspace" };
4816
- await check("file_read", { file_path: "/tmp/test.txt" }, "/tmp");
4817
- expect(loggerWarnCalls.some((m) => m.includes("deprecated"))).toBe(false);
4818
- });
4819
-
4820
- test("no deprecation warning in strict mode", async () => {
4821
- testConfig.permissions = { mode: "strict" };
4822
- await check("file_read", { file_path: "/tmp/test.txt" }, "/tmp");
4823
- expect(loggerWarnCalls.some((m) => m.includes("deprecated"))).toBe(false);
4824
- });
4825
-
4826
- test("legacy mode still produces correct decisions (low risk auto-allowed)", async () => {
4827
- const result = await check(
4828
- "file_read",
4829
- { file_path: "/tmp/test.txt" },
4830
- "/tmp",
4831
- );
4832
- expect(result.decision).toBe("allow");
4833
- expect(result.reason).toContain("Low risk");
4834
- });
4835
-
4836
- test("legacy mode still prompts for medium risk", async () => {
4837
- const result = await check(
4838
- "file_write",
4839
- { file_path: "/tmp/test.txt" },
4840
- "/tmp",
4841
- );
4842
- expect(result.decision).toBe("prompt");
4843
- expect(result.reason).toContain("risk");
4844
- });
4845
- });
4846
-
4847
4786
  describe("shell command candidates wiring (PR 04)", () => {
4848
4787
  test("existing raw shell rule still matches", async () => {
4849
4788
  clearCache();
@@ -4896,7 +4835,7 @@ describe("integration regressions (PR 11)", () => {
4896
4835
  /* may not exist */
4897
4836
  }
4898
4837
  clearCache();
4899
- testConfig.permissions = { mode: "legacy" };
4838
+ testConfig.permissions = { mode: "workspace" };
4900
4839
  testConfig.sandbox = { enabled: true };
4901
4840
  });
4902
4841