@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.
- package/ARCHITECTURE.md +1 -1
- package/Dockerfile +14 -8
- package/README.md +2 -2
- package/docs/architecture/memory.md +28 -29
- package/docs/runbook-trusted-contacts.md +1 -4
- package/package.json +1 -1
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +0 -7
- package/src/__tests__/anthropic-provider.test.ts +86 -1
- package/src/__tests__/assistant-feature-flags-integration.test.ts +2 -2
- package/src/__tests__/checker.test.ts +37 -98
- package/src/__tests__/commit-message-enrichment-service.test.ts +15 -4
- package/src/__tests__/config-schema.test.ts +6 -14
- package/src/__tests__/conflict-policy.test.ts +76 -0
- package/src/__tests__/conflict-store.test.ts +14 -20
- package/src/__tests__/contacts-tools.test.ts +8 -61
- package/src/__tests__/contradiction-checker.test.ts +5 -1
- package/src/__tests__/credential-security-invariants.test.ts +1 -0
- package/src/__tests__/daemon-server-session-init.test.ts +1 -19
- package/src/__tests__/followup-tools.test.ts +0 -30
- package/src/__tests__/gemini-provider.test.ts +79 -1
- package/src/__tests__/guardian-decision-primitive-canonical.test.ts +5 -3
- package/src/__tests__/guardian-routing-invariants.test.ts +6 -4
- package/src/__tests__/ipc-snapshot.test.ts +0 -4
- package/src/__tests__/managed-proxy-context.test.ts +163 -0
- package/src/__tests__/memory-lifecycle-e2e.test.ts +13 -12
- package/src/__tests__/memory-regressions.test.ts +6 -6
- package/src/__tests__/openai-provider.test.ts +82 -0
- package/src/__tests__/provider-fail-open-selection.test.ts +134 -1
- package/src/__tests__/provider-managed-proxy-integration.test.ts +269 -0
- package/src/__tests__/recurrence-types.test.ts +0 -15
- package/src/__tests__/registry.test.ts +0 -10
- package/src/__tests__/schedule-tools.test.ts +28 -44
- package/src/__tests__/script-proxy-session-runtime.test.ts +6 -1
- package/src/__tests__/session-agent-loop.test.ts +0 -2
- package/src/__tests__/session-conflict-gate.test.ts +243 -388
- package/src/__tests__/session-profile-injection.test.ts +0 -2
- package/src/__tests__/session-runtime-assembly.test.ts +2 -3
- package/src/__tests__/session-skill-tools.test.ts +0 -49
- package/src/__tests__/session-workspace-cache-state.test.ts +0 -1
- package/src/__tests__/session-workspace-injection.test.ts +0 -1
- package/src/__tests__/session-workspace-tool-tracking.test.ts +0 -1
- package/src/__tests__/skill-feature-flags.test.ts +2 -2
- package/src/__tests__/task-management-tools.test.ts +111 -0
- package/src/__tests__/tool-grant-request-escalation.test.ts +2 -1
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +2 -1
- package/src/__tests__/twilio-config.test.ts +0 -3
- package/src/amazon/session.ts +30 -91
- package/src/approvals/guardian-decision-primitive.ts +11 -7
- package/src/approvals/guardian-request-resolvers.ts +5 -3
- package/src/calls/call-controller.ts +423 -571
- package/src/calls/finalize-call.ts +20 -0
- package/src/calls/relay-access-wait.ts +340 -0
- package/src/calls/relay-server.ts +269 -899
- package/src/calls/relay-setup-router.ts +307 -0
- package/src/calls/relay-verification.ts +280 -0
- package/src/calls/twilio-config.ts +1 -8
- package/src/calls/voice-control-protocol.ts +184 -0
- package/src/calls/voice-session-bridge.ts +1 -8
- package/src/config/agent-schema.ts +1 -1
- package/src/config/bundled-skills/contacts/SKILL.md +7 -18
- package/src/config/bundled-skills/contacts/TOOLS.json +4 -20
- package/src/config/bundled-skills/contacts/tools/contact-merge.ts +2 -4
- package/src/config/bundled-skills/contacts/tools/contact-search.ts +6 -12
- package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +3 -24
- package/src/config/bundled-skills/followups/TOOLS.json +0 -4
- package/src/config/bundled-skills/schedule/SKILL.md +1 -1
- package/src/config/bundled-skills/schedule/TOOLS.json +2 -10
- package/src/config/bundled-tool-registry.ts +0 -5
- package/src/config/core-schema.ts +1 -1
- package/src/config/env.ts +0 -10
- package/src/config/feature-flag-registry.json +1 -1
- package/src/config/loader.ts +19 -0
- package/src/config/memory-schema.ts +0 -10
- package/src/config/schema.ts +2 -2
- package/src/config/system-prompt.ts +6 -0
- package/src/contacts/contact-store.ts +36 -62
- package/src/contacts/contacts-write.ts +14 -3
- package/src/contacts/types.ts +9 -4
- package/src/daemon/handlers/config-heartbeat.ts +1 -2
- package/src/daemon/handlers/contacts.ts +2 -2
- package/src/daemon/handlers/guardian-actions.ts +1 -1
- package/src/daemon/handlers/session-history.ts +398 -0
- package/src/daemon/handlers/session-user-message.ts +982 -0
- package/src/daemon/handlers/sessions.ts +9 -1337
- package/src/daemon/ipc-contract/contacts.ts +2 -2
- package/src/daemon/ipc-contract/sessions.ts +0 -6
- package/src/daemon/ipc-contract-inventory.json +0 -1
- package/src/daemon/lifecycle.ts +0 -29
- package/src/daemon/session-agent-loop.ts +1 -45
- package/src/daemon/session-conflict-gate.ts +21 -82
- package/src/daemon/session-memory.ts +7 -52
- package/src/daemon/session-process.ts +3 -1
- package/src/daemon/session-runtime-assembly.ts +18 -35
- package/src/heartbeat/heartbeat-service.ts +5 -1
- package/src/home-base/app-link-store.ts +0 -7
- package/src/memory/conflict-intent.ts +3 -6
- package/src/memory/conflict-policy.ts +34 -0
- package/src/memory/conflict-store.ts +10 -18
- package/src/memory/contradiction-checker.ts +2 -2
- package/src/memory/conversation-attention-store.ts +1 -1
- package/src/memory/conversation-store.ts +0 -51
- package/src/memory/db-init.ts +8 -0
- package/src/memory/job-handlers/conflict.ts +24 -7
- package/src/memory/migrations/105-contacts-and-triage.ts +4 -7
- package/src/memory/migrations/134-contacts-notes-column.ts +68 -0
- package/src/memory/migrations/135-backfill-contact-interaction-stats.ts +31 -0
- package/src/memory/migrations/index.ts +2 -0
- package/src/memory/migrations/registry.ts +6 -0
- package/src/memory/recall-cache.ts +0 -5
- package/src/memory/schema/calls.ts +274 -0
- package/src/memory/schema/contacts.ts +125 -0
- package/src/memory/schema/conversations.ts +129 -0
- package/src/memory/schema/guardian.ts +172 -0
- package/src/memory/schema/index.ts +8 -0
- package/src/memory/schema/infrastructure.ts +205 -0
- package/src/memory/schema/memory-core.ts +196 -0
- package/src/memory/schema/notifications.ts +191 -0
- package/src/memory/schema/tasks.ts +78 -0
- package/src/memory/schema.ts +1 -1402
- package/src/memory/slack-thread-store.ts +0 -69
- package/src/messaging/index.ts +0 -1
- package/src/messaging/types.ts +0 -38
- package/src/notifications/decisions-store.ts +2 -105
- package/src/notifications/deliveries-store.ts +0 -11
- package/src/notifications/preferences-store.ts +1 -58
- package/src/permissions/checker.ts +6 -17
- package/src/providers/anthropic/client.ts +6 -2
- package/src/providers/gemini/client.ts +13 -2
- package/src/providers/managed-proxy/constants.ts +55 -0
- package/src/providers/managed-proxy/context.ts +77 -0
- package/src/providers/registry.ts +112 -0
- package/src/runtime/auth/__tests__/guard-tests.test.ts +51 -23
- package/src/runtime/guardian-action-service.ts +3 -2
- package/src/runtime/guardian-outbound-actions.ts +3 -3
- package/src/runtime/guardian-reply-router.ts +4 -4
- package/src/runtime/http-server.ts +83 -710
- package/src/runtime/http-types.ts +0 -16
- package/src/runtime/middleware/auth.ts +0 -12
- package/src/runtime/routes/app-routes.ts +33 -0
- package/src/runtime/routes/approval-routes.ts +32 -0
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +6 -3
- package/src/runtime/routes/attachment-routes.ts +32 -0
- package/src/runtime/routes/brain-graph-routes.ts +27 -0
- package/src/runtime/routes/call-routes.ts +41 -0
- package/src/runtime/routes/channel-readiness-routes.ts +20 -0
- package/src/runtime/routes/channel-routes.ts +70 -0
- package/src/runtime/routes/contact-routes.ts +371 -29
- package/src/runtime/routes/conversation-attention-routes.ts +15 -0
- package/src/runtime/routes/conversation-routes.ts +192 -194
- package/src/runtime/routes/debug-routes.ts +15 -0
- package/src/runtime/routes/events-routes.ts +16 -0
- package/src/runtime/routes/global-search-routes.ts +17 -2
- package/src/runtime/routes/guardian-action-routes.ts +23 -1
- package/src/runtime/routes/guardian-approval-interception.ts +2 -1
- package/src/runtime/routes/guardian-bootstrap-routes.ts +26 -1
- package/src/runtime/routes/guardian-refresh-routes.ts +20 -0
- package/src/runtime/routes/identity-routes.ts +20 -0
- package/src/runtime/routes/inbound-message-handler.ts +8 -0
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +5 -1
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +2 -1
- package/src/runtime/routes/integration-routes.ts +83 -0
- package/src/runtime/routes/invite-routes.ts +31 -0
- package/src/runtime/routes/migration-routes.ts +47 -17
- package/src/runtime/routes/pairing-routes.ts +18 -0
- package/src/runtime/routes/secret-routes.ts +20 -0
- package/src/runtime/routes/surface-action-routes.ts +26 -0
- package/src/runtime/routes/trust-rules-routes.ts +31 -0
- package/src/runtime/routes/twilio-routes.ts +79 -0
- package/src/schedule/recurrence-types.ts +1 -11
- package/src/tools/followups/followup_create.ts +9 -3
- package/src/tools/mcp/mcp-tool-factory.ts +0 -17
- package/src/tools/memory/definitions.ts +0 -6
- package/src/tools/network/script-proxy/session-manager.ts +38 -3
- package/src/tools/schedule/create.ts +1 -3
- package/src/tools/schedule/update.ts +9 -6
- package/src/twitter/session.ts +29 -77
- package/src/util/cookie-session.ts +114 -0
- package/src/workspace/git-service.ts +6 -4
- package/src/__tests__/conversation-routes.test.ts +0 -99
- package/src/__tests__/get-weather.test.ts +0 -393
- package/src/__tests__/task-tools.test.ts +0 -685
- package/src/__tests__/weather-skill-regression.test.ts +0 -276
- package/src/autonomy/autonomy-resolver.ts +0 -62
- package/src/autonomy/autonomy-store.ts +0 -138
- package/src/autonomy/disposition-mapper.ts +0 -31
- package/src/autonomy/index.ts +0 -11
- package/src/autonomy/types.ts +0 -43
- package/src/config/bundled-skills/weather/SKILL.md +0 -38
- package/src/config/bundled-skills/weather/TOOLS.json +0 -36
- package/src/config/bundled-skills/weather/icon.svg +0 -24
- package/src/config/bundled-skills/weather/tools/get-weather.ts +0 -12
- package/src/contacts/startup-migration.ts +0 -21
- package/src/messaging/triage-engine.ts +0 -344
- 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
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
|
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
|
|
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/>
|
|
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.
|
|
176
|
-
| `memory.conflicts.
|
|
177
|
-
| `memory.conflicts.
|
|
178
|
-
| `memory.conflicts.
|
|
179
|
-
| `memory.
|
|
180
|
-
| `memory.
|
|
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 :
|
|
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
|
|
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 (
|
|
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
|
-
"
|
|
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
|
@@ -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:
|
|
245
|
-
expect(result).
|
|
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
|
-
// '
|
|
58
|
+
// 'strict' and 'workspace' without re-registering the mock.
|
|
59
59
|
interface TestConfig {
|
|
60
|
-
permissions: { mode: "
|
|
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: "
|
|
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
|
|
173
|
-
testConfig.permissions = { mode: "
|
|
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 →
|
|
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
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
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
|
-
|
|
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 = "
|
|
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 = "
|
|
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 = "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
4838
|
+
testConfig.permissions = { mode: "workspace" };
|
|
4900
4839
|
testConfig.sandbox = { enabled: true };
|
|
4901
4840
|
});
|
|
4902
4841
|
|