@vellumai/assistant 0.11.3-dev.202608181709.1f85a67 → 0.11.3-dev.202608181913.444e3ab

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 (68) hide show
  1. package/AGENTS.md +1 -1
  2. package/docs/architecture/memory.md +17 -5
  3. package/docs/architecture/security.md +95 -153
  4. package/openapi.yaml +14 -0
  5. package/package.json +1 -1
  6. package/src/__tests__/checker.test.ts +0 -7
  7. package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +6 -2
  8. package/src/__tests__/context-search-memory-v2-source.test.ts +0 -1
  9. package/src/__tests__/default-plugin-names-guard.test.ts +12 -1
  10. package/src/__tests__/file-ops-service.test.ts +163 -30
  11. package/src/__tests__/filesystem-tools.test.ts +23 -24
  12. package/src/__tests__/host-file-read-tool.test.ts +16 -19
  13. package/src/__tests__/inline-skill-load-permissions.test.ts +23 -8
  14. package/src/__tests__/mtime-cache.test.ts +2 -0
  15. package/src/__tests__/plugin-disabled-state.test.ts +2 -0
  16. package/src/__tests__/plugin-effective-enabled-set.test.ts +55 -3
  17. package/src/__tests__/tool-executor-lifecycle-events.test.ts +132 -1
  18. package/src/__tests__/tool-executor.test.ts +43 -28
  19. package/src/__tests__/user-plugin-loader.test.ts +2 -0
  20. package/src/api/events/host-file.ts +2 -2
  21. package/src/cli/commands/memory/__tests__/memory-v2.test.ts +57 -7
  22. package/src/cli/commands/memory/index.help.ts +41 -27
  23. package/src/cli/commands/memory/index.ts +2 -0
  24. package/src/cli/commands/memory/memory-v2.ts +58 -54
  25. package/src/cli/commands/memory/memory-validate.ts +18 -0
  26. package/src/daemon/conversation-runtime-assembly.ts +1 -1
  27. package/src/hooks/registry.ts +65 -51
  28. package/src/permissions/AGENTS.md +16 -0
  29. package/src/permissions/checker.test.ts +58 -92
  30. package/src/permissions/checker.ts +48 -206
  31. package/src/permissions/risk-types.ts +3 -42
  32. package/src/persistence/conversation-queries.ts +14 -1
  33. package/src/plugins/defaults/main.ts +15 -0
  34. package/src/plugins/defaults/memory/AGENTS.md +11 -3
  35. package/src/plugins/defaults/memory/graph-topology/pending-buffer.ts +9 -12
  36. package/src/plugins/defaults/memory/src/memory-v2-routes.ts +12 -10
  37. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +114 -0
  38. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +8 -0
  39. package/src/plugins/defaults/memory/substrate/__tests__/edge-index.test.ts +0 -30
  40. package/src/plugins/defaults/memory/substrate/__tests__/ingest.test.ts +73 -1
  41. package/src/plugins/defaults/memory/substrate/__tests__/page-index.test.ts +37 -0
  42. package/src/plugins/defaults/memory/substrate/__tests__/page-links.test.ts +208 -0
  43. package/src/plugins/defaults/memory/substrate/__tests__/prompts-consolidation.test.ts +158 -1
  44. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +68 -6
  45. package/src/plugins/defaults/memory/substrate/edge-index.ts +0 -31
  46. package/src/plugins/defaults/memory/substrate/ingest.ts +59 -0
  47. package/src/plugins/defaults/memory/substrate/page-index.ts +35 -8
  48. package/src/plugins/defaults/memory/substrate/page-links.ts +133 -0
  49. package/src/plugins/defaults/memory/substrate/page-store.ts +10 -0
  50. package/src/plugins/defaults/memory/substrate/prompts/consolidation.ts +124 -34
  51. package/src/plugins/defaults/memory/v3/card.ts +9 -9
  52. package/src/plugins/defaults/memory/v3/core-set.test.ts +7 -0
  53. package/src/plugins/defaults/memory/v3/core-set.ts +5 -1
  54. package/src/plugins/defaults/memory/v3/edge.ts +13 -57
  55. package/src/plugins/pipeline.ts +4 -3
  56. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +133 -0
  57. package/src/runtime/routes/conversation-list-routes.ts +22 -7
  58. package/src/runtime/routes/settings-routes.ts +9 -2
  59. package/src/subagent/types.ts +7 -7
  60. package/src/tools/__tests__/tool-input-schemas.test.ts +7 -7
  61. package/src/tools/executor.ts +32 -2
  62. package/src/tools/filesystem/read.ts +27 -10
  63. package/src/tools/host-filesystem/read.ts +27 -15
  64. package/src/tools/permission-checker.ts +33 -32
  65. package/src/tools/shared/filesystem/file-ops-service.ts +63 -35
  66. package/src/tools/shared/filesystem/legacy-read-args.ts +22 -0
  67. package/src/tools/shared/filesystem/types.ts +5 -5
  68. package/src/tools/tool-types.ts +8 -0
package/AGENTS.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  For error handling conventions (throw vs result objects vs null), see [docs/error-handling.md](docs/error-handling.md).
4
4
 
5
- Subdirectory-scoped rules live in local AGENTS.md files: `src/cli/`, `src/runtime/`, `src/approvals/`, `src/notifications/`, `src/plugins/`, `src/workspace/migrations/`.
5
+ Subdirectory-scoped rules live in local AGENTS.md files: `src/cli/`, `src/runtime/`, `src/approvals/`, `src/notifications/`, `src/permissions/`, `src/plugins/`, `src/workspace/migrations/`.
6
6
 
7
7
  ## Adding new environment variables
8
8
 
@@ -99,6 +99,17 @@ graph LR
99
99
  failure-backoff-respecting);
100
100
  - manual "Run now" via `POST /v1/consolidation/run-now`.
101
101
  Failed runs enter an exponential backoff (transient vs billing curves).
102
+ - The agent writes pages through the file tools, so nothing validates a
103
+ page at write time. Two corpus-level defects are instead reported by the
104
+ page index and fed back into the next pass's prompt as repair steps:
105
+ pages the index could not parse (`PageIndex.parseFailures`) and
106
+ structural references (`links:`, inline `[[wikilinks]]`, `edges:`) whose
107
+ target page does not exist (`PageIndex.danglingLinks`). The read-side
108
+ graph drops a dangling reference silently, so the job also counts them
109
+ after each run (`danglingLinks` on the outcome, a warn line) without
110
+ withholding the reindex follow-ups: the pages that were written still
111
+ become retrievable. The `memory validate` CLI subcommand reports the same
112
+ list.
102
113
  - **Ingestion** (`substrate/ingest.ts`, exposed as `POST /v1/memory/ingest`;
103
114
  generated HTTP operation id `memory_ingest_post`, IPC method
104
115
  `memory_ingest`) is the second sanctioned writer of
@@ -110,11 +121,12 @@ graph LR
110
121
  minute, and its same-minute burst guard falls back to processing the
111
122
  whole buffer in a single oversized run. Ingest writes validated pages
112
123
  directly instead. Purely mechanical: each page is validated and reported
113
- individually, writes hold the consolidation lock so a batch cannot
114
- interleave with a consolidation pass, and a batch that wrote at least one
115
- page enqueues the same reindex follow-ups as consolidation
116
- (`memory_v2_reembed`, `memory_v3_maintain`). Consolidation remains the only
117
- LLM-driven writer.
124
+ individually (a `links:`/`[[wikilink]]`/`edges:` target that is neither on
125
+ disk nor in the batch is a per-page warning, not a rejection), writes hold
126
+ the consolidation lock so a batch cannot interleave with a consolidation
127
+ pass, and a batch that wrote at least one page enqueues the same reindex
128
+ follow-ups as consolidation (`memory_v2_reembed`, `memory_v3_maintain`).
129
+ Consolidation remains the only LLM-driven writer.
118
130
 
119
131
  ### Ingestion tracks and provenance
120
132
 
@@ -4,207 +4,149 @@ Permission, trust, and credential-security architecture details.
4
4
 
5
5
  ## Permission and Trust Security Model
6
6
 
7
- The permission system controls which tool actions the agent can execute without explicit user approval. It supports two operating modes (`workspace` and `strict`), execution-target-scoped trust rules, and risk-based escalation to provide defense-in-depth against unintended or malicious tool execution.
7
+ The permission system decides which tool actions the agent may execute without explicit approval. Two processes share the work, and the split is the load-bearing fact of this section:
8
+
9
+ - The **gateway** owns risk classification (`gateway/src/risk/*`, entered through the `classify_risk` IPC method), the trust rules that raise or lower a classified risk (stored in the gateway's SQLite `trust_rules` table and applied inside the classifiers), the auto-approve thresholds and channel-permission cells, and the per-actor `TrustClass` verdict.
10
+ - The **assistant** owns the turn's actor and capabilities (`runtime/capabilities.ts`), the sensitive-tool capability floor (`tools/tool-approval-handler.ts`), the allow / prompt / deny decision over risk × threshold × capabilities (`permissions/checker.ts` `check()` and `DefaultApprovalPolicy`), the prompt UX (`permissions/prompter.ts`), execution, and the `tool_invocations` audit row.
11
+
12
+ The assistant has no local classifier and no fallback: an unreachable gateway fails closed. It classifies each tool invocation exactly once and passes that classification down; nothing in the assistant memoises or re-derives risk. `assistant/src/permissions/AGENTS.md` and `gateway/src/risk/AGENTS.md` state the rules that follow.
8
13
 
9
14
  ### Permission Evaluation Flow
10
15
 
11
16
  ```mermaid
12
17
  graph TB
13
- TOOL_CALL["Tool invocation<br/>(toolName, input, policyContext)"] --> CLASSIFY["classifyRisk()<br/>→ Low / Medium / High"]
14
- CLASSIFY --> CANDIDATES["buildCommandCandidates()<br/>tool:target strings +<br/>canonical path variants"]
15
- CANDIDATES --> FIND_RULE["findHighestPriorityRule()<br/>iterate sorted rules:<br/>tool, scope, pattern (minimatch),<br/>executionTarget"]
16
-
17
- FIND_RULE -->|"Deny rule"| DENY["decision: deny<br/>Blocked by rule"]
18
- FIND_RULE -->|"Ask rule"| PROMPT_ASK["decision: prompt<br/>Always ask user"]
19
- FIND_RULE -->|"Allow rule / No match"| SANDBOX_CHECK{"sandboxAutoApprove?<br/>(bash + allowlisted +<br/>containerized)"}
20
-
21
- SANDBOX_CHECK -->|"yes"| AUTO_SANDBOX["decision: allow<br/>Sandbox auto-approve"]
22
- SANDBOX_CHECK -->|"no, has Allow rule"| RISK_CHECK{"Risk level?"}
23
- SANDBOX_CHECK -->|"no, no match"| NO_MATCH{"Fallback logic"}
24
-
25
- RISK_CHECK -->|"Low / Medium"| AUTO_ALLOW["decision: allow<br/>Auto-allowed by rule"]
26
- RISK_CHECK -->|"High"| RISK_THRESHOLD{"Risk-based<br/>threshold fallback"}
27
-
28
- NO_MATCH -->|"tool.origin === 'skill'"| PROMPT_SKILL["decision: prompt<br/>Skill tools always ask"]
29
- NO_MATCH -->|"workspace-scoped<br/>+ Low risk"| AUTO_WS["decision: allow<br/>Workspace-scoped auto-allow"]
30
- NO_MATCH -->|"otherwise"| RISK_THRESHOLD
31
-
32
- RISK_THRESHOLD{"risk ≤ autoApproveUpTo<br/>threshold?"}
33
- RISK_THRESHOLD -->|"yes"| AUTO_THRESHOLD["decision: allow<br/>within auto-approve threshold"]
34
- RISK_THRESHOLD -->|"no"| PROMPT_THRESHOLD["decision: prompt<br/>above auto-approve threshold"]
18
+ TOOL_CALL["Tool invocation<br/>(toolName, input, context)"] --> CLASSIFY["classifyRisk() once, before the gates<br/>gateway classify_risk over IPC<br/>→ level, reason, matchType, options"]
19
+ CLASSIFY --> GATES["Pre-execution gates<br/>abort · unparseable args · guardian control-plane policy ·<br/>sensitive-tool floor + approval-matrix cell · disk pressure ·<br/>unknown tool · allowedToolNames · channel policy · Zod parse"]
20
+ GATES -->|"blocked"| GATE_OUT["denied / error<br/>audited with the classified level"]
21
+ GATES -->|"sensitive, non-guardian"| GRANT{"scoped grant<br/>consumed?"}
22
+ GRANT -->|"yes"| EXECUTE["execute<br/>(no permission check;<br/>provenance grant_scoped_consumed)"]
23
+ GRANT -->|"escalate-and-wait"| ESCALATE["guardian tool-grant request<br/>+ inline wait"]
24
+ GRANT -->|"deny actor / no grant"| GATE_OUT
25
+ GATES -->|"passed"| CHECK["checkPermission → check()<br/>threshold + capability context"]
26
+ CHECK --> POLICY["DefaultApprovalPolicy.evaluate"]
27
+ POLICY -->|"allow"| EXECUTE
28
+ POLICY -->|"prompt"| PROMPT_ROUTE{"presence?"}
29
+ PROMPT_ROUTE -->|"non-interactive turn"| AUTO_OR_DENY["guardian within background threshold → allow<br/>otherwise deny (no human to ask)"]
30
+ PROMPT_ROUTE -->|"interactive"| PROMPT["confirmation_request → user allow / deny"]
35
31
  ```
36
32
 
33
+ The order inside `check()`: the memory-retrospective skill-authoring grant, then the classification, then the auto-approve threshold for the turn's execution context (per-conversation override, then channel-permission cell, then global), then `DefaultApprovalPolicy.evaluate`. A `prompt` computed from a cached threshold is re-checked against a fresh read before the user is interrupted. `checkPermission` then applies `forcePromptSideEffects` and `requireFreshApproval` (allow → prompt), the non-interactive denial for uncovered inline-command skill loads, and platform-hosted sandboxed-bash auto-approve for guardians.
34
+
37
35
  ### Auto-Approve Threshold
38
36
 
39
- Auto-approve thresholds are **gateway-owned** — they live in the gateway's SQLite database and are read by the assistant via IPC (`get_global_thresholds`, `get_conversation_threshold`). Users control thresholds via the **Settings UI** (Permissions & Privacy tab) or the **per-conversation risk tolerance picker**. When the gateway is unreachable, the assistant defaults to `"none"` (Strict) — fail-closed with no local fallback.
37
+ Thresholds are **gateway-owned**: stored in the gateway's SQLite database, read by the assistant over IPC (`get_global_thresholds`, `get_conversation_threshold`), and set from the Settings UI (Permissions & Privacy) or the per-conversation risk tolerance picker. When the gateway is unreachable the assistant resolves `"none"` (Strict), fail-closed with no local fallback.
38
+
39
+ Gateway defaults per execution context (`gateway/src/ipc/threshold-handlers.ts`): `interactive` (a conversation with a client) `medium`, `autonomous` (background/scheduled) `low`, `headless` `none`. A per-conversation override wins over the global value; for non-guardian actors a channel-permission cell can only lower the effective threshold.
40
40
 
41
41
  | `autoApproveUpTo` | Low-risk tools | Medium-risk tools | High-risk tools |
42
42
  | ----------------- | -------------- | ----------------- | --------------- |
43
43
  | `"none"` | Prompted | Prompted | Prompted |
44
- | `"low"` (default) | Auto-allowed | Prompted | Prompted |
44
+ | `"low"` | Auto-allowed | Prompted | Prompted |
45
45
  | `"medium"` | Auto-allowed | Auto-allowed | Prompted |
46
46
  | `"high"` | Auto-allowed | Auto-allowed | Auto-allowed |
47
47
 
48
- When set to `"none"`, every tool invocation requires explicit approval. Explicit deny and ask rules always take precedence over the threshold.
48
+ ### Approval Policy
49
49
 
50
- ### Trust Rules (v3 Schema)
50
+ `DefaultApprovalPolicy.evaluate` (`assistant/src/permissions/approval-policy.ts`) turns a classified risk into allow / prompt, in this order:
51
51
 
52
- Rules are stored in `~/.vellum/protected/trust.json` with version `3`. Each rule can include the following fields:
52
+ 1. `bash` with the gateway's `sandboxAutoApprove` verdict, when the threshold is not `"none"`: allow.
53
+ 2. Third-party code (skill- or plugin-owned tools that are not first-party bundled, or a builtin running under a manifest override): allow within threshold, otherwise prompt.
54
+ 3. Low risk, workspace-scoped invocation, within threshold: allow.
55
+ 4. Low risk, bundled-skill tool, within threshold: allow.
56
+ 5. Otherwise: allow when risk ≤ threshold, prompt when above.
53
57
 
54
- | Field | Type | Purpose |
55
- | ----------------- | ---------------------- | ------------------------------------------------------------------------ |
56
- | `id` | `string` | Unique identifier (UUID for user rules, `default:*` for system defaults) |
57
- | `tool` | `string` | Tool name to match (e.g., `bash`, `file_write`, `skill_load`) |
58
- | `pattern` | `string` | Minimatch glob pattern for the command/target string |
59
- | `scope` | `string` | Path prefix or `everywhere` — restricts where the rule applies |
60
- | `decision` | `allow \| deny \| ask` | What to do when the rule matches |
61
- | `priority` | `number` | Higher priority wins; deny wins ties at equal priority |
62
- | `executionTarget` | `string?` | `sandbox` or `host` — restricts by execution context |
58
+ The policy never returns deny; denials come from the gates, the capability floor, and the checks in `checkPermission`. There is no allow / deny / ask rule axis: trust rules act on the classified risk, upstream of this policy.
63
59
 
64
- Missing optional fields act as wildcards. A rule with no `executionTarget` matches any target.
60
+ ### Trust Rules (v3)
65
61
 
66
- ### Risk Classification and Escalation
62
+ Rules live in the gateway (`gateway/src/db/trust-rule-store.ts`, cached in-process by `gateway/src/risk/trust-rule-cache.ts`, mutated only through the gateway HTTP routes, which refresh the cache). A rule is `{ tool, pattern, risk: low | medium | high, description, origin: default | user_defined, userModified, deleted }`, unique on `(tool, pattern)`. Default rules are seeded at gateway start from the bash command registry (`gateway/src/db/seed-trust-rules.ts`) and can be modified or reset; user rules are created, updated, and deleted through `/v1/trust-rules`.
67
63
 
68
- The `classifyRisk()` function determines the risk level for each tool invocation:
64
+ Matching happens inside the classifiers: the bash classifier looks the command up exact, path-stripped, then by shorter subcommand prefixes, each in literal and `action:` form, user rules winning over defaults; the file, web, skill, and schedule classifiers look up a per-tool override. A matched rule replaces the base risk and the classification carries `matchType: "user_rule"`. The assistant sees only that: it never stores, matches, or lists rules except to proxy the list over IPC for clients.
69
65
 
70
- | Tool | Risk level | Notes |
71
- | ---------------------------------------------------------------- | --------------------------- | -------------------------------------------------------------------------------------------- |
72
- | `file_read`, `web_search`, `skill_load` | Low | Read-only or informational |
73
- | `file_write`, `file_edit` | Medium (default) | Filesystem mutations |
74
- | `file_write`, `file_edit` targeting skill source paths | **High** | `isSkillSourcePath()` detects managed/bundled/workspace/extra skill roots |
75
- | `host_file_write`, `host_file_edit` targeting skill source paths | **High** | Same path classification, host variant |
76
- | `bash`, `host_bash` | Varies | Parsed via tree-sitter: low-risk programs = Low, high-risk programs = High, unknown = Medium |
77
- | `scaffold_managed_skill`, `delete_managed_skill` | High | Skill lifecycle mutations always high-risk |
78
- | `evaluate_typescript_code` | High | Arbitrary code execution |
79
- | Skill-origin tools with no matching rule | Prompted regardless of risk | Even Low-risk skill tools default to `ask` |
66
+ ### Risk Classification
80
67
 
81
- The escalation of skill source file mutations to High risk is a privilege-escalation defense: modifying skill source code could grant the agent new capabilities, so such operations always require explicit approval.
68
+ Classifiers (`gateway/src/risk/`), keyed by tool:
82
69
 
83
- ### Skill Load Approval
70
+ | Tool | Classifier | Notes |
71
+ | -------------------------------------------------------------- | --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
72
+ | `bash`, `host_bash` | `bash-risk-classifier.ts` | Tree-sitter parse (`shell-parser.ts`, memoised on the command text) against `command-registry/`; per-command arg rules; dangerous patterns; `sandboxAutoApprove` for allowlisted sandbox commands with lexically-resolved path args |
73
+ | `file_*`, `host_file_*` | `file-risk-classifier.ts` | Writes to skill source, workspace code, and other code-loaded directories escalate to High; the assistant sends symlink-resolved paths and the protected/skill directories with the request |
74
+ | `web_fetch`, `network_request` | `web-risk-classifier.ts` | URL-based; private-network access escalates |
75
+ | `skill_load`, `scaffold_managed_skill`, `delete_managed_skill` | `skill-risk-classifier.ts` | A `skill_load` whose skill has inline command expansions (executes shell at load time) is High; skill lifecycle mutations are High |
76
+ | `schedule_create`, `schedule_update` | `schedule-risk-classifier.ts` | Scheduled command risk |
77
+ | Everything else | fallback in `risk-classification-handlers.ts` | The tool's `defaultRiskLevel` from the assistant's registry (`matchType: "registry"`), or `medium` with an "Unknown tool" reason. This branch consults no trust rule and emits no allowlist options, so a user rule cannot cover an MCP or other classifier-less tool today |
84
78
 
85
- The `skill_load` tool generates version-aware command candidates for rule matching:
79
+ The response (`ClassificationResult`, mirrored in `assistant/src/permissions/ipc-risk-types.ts`) carries the level, reason, `matchType`, allowlist / scope / directory-scope options, command candidates and action keys, `sandboxAutoApprove`, and the path args it was based on. The assistant's one adjustment is the bash symlink-escape re-check: it resolves the gateway's lexically-checked path args through the real filesystem and revokes `sandboxAutoApprove` if any escapes the workspace. It runs on every classification.
86
80
 
87
- 1. `skill_load:<skill-id>@<version-hash>` — matches version-pinned rules
88
- 2. `skill_load:<skill-id>` — matches any-version rules
89
- 3. `skill_load:<raw-selector>` — matches the raw user-provided selector
81
+ ### Sensitive-Tool Capability Floor
90
82
 
91
- When `autoApproveUpTo` is `"none"`, `skill_load` without a matching rule is always prompted. The allowlist options presented to the user include both version-specific and any-version patterns. Note: the system default allow rule `skill_load:*` (priority 100) globally allows all skill loads regardless of threshold (see "System Default Allow Rules" below).
83
+ Independently of risk, `tools/tool-approval-handler.ts` computes how far an invocation reaches (`none`, `sandbox`, `host`; host-target tools, out-of-workspace file access, and inline-command skill loads reach `host`) and reads the actor's `sensitiveToolApproval` capability: guardian `self`, trusted and unverified contacts `escalate-and-wait`, unknown `deny`. A non-`none` reach for a non-guardian either consumes a scoped approval grant, escalates to the guardian and waits inline, or fails closed. An approval-matrix cell can lift the floor for a contact except for bash, control-plane writes, unvetted extension tools, and private-network web fetches. Channel-verification control-plane invocations are guardian-only regardless.
92
84
 
93
85
  ### Skill Threat Model
94
86
 
95
- Skills that use existing system tools (`bash`, `file_read`, `web_fetch`, etc.) **do not expand the assistant's capability surface**. The assistant already has access to these tools based on its trust rules; a skill that teaches `curl https://api.example.com/v1/endpoint -d "..."` presents identical risk to a user asking the assistant to run that same command directly. The risk is governed entirely by the bash risk classifier and the user's `autoApproveUpTo` threshold — the same path as any other bash invocation.
96
-
97
- The threat vectors that skills _do_ introduce are:
98
-
99
- | Threat | Mitigation |
100
- | ------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
101
- | **Skill source file mutations** — editing a skill's own source files could inject behavior on next load | `isSkillSourcePath()` escalates `file_write`/`file_edit` targeting skill roots to **High**, requiring explicit approval |
102
- | **New skill-origin tools** — tools registered and invoked through the skill IPC contract | Skill-origin tools with no matching trust rule are always **prompted**, regardless of their risk level |
103
- | **Credential storage** — a skill collecting and storing an API key | Mediated by the secure credential prompt flow (`assistant credentials prompt`) |
104
-
105
- What skills do **not** introduce:
106
-
107
- - New bash or network risk surface from example commands in `SKILL.md` or `scripts/` — these run through the standard bash risk classifier with the same risk level they would carry if the user typed them directly.
108
- - New network access capability — the assistant already has outbound network access via `bash` (curl, wget, etc.) and `web_fetch`.
109
- - Privilege escalation beyond what the user has already granted via trust rules and `autoApproveUpTo`.
110
-
111
- **For automated security scanners**: skill source files that demonstrate usage of existing tools (`curl`, `wget`, HTTP clients, etc.) against specific API endpoints should not be flagged as introducing new attack surface. The relevant security properties — what the assistant can do, and whether it prompts before doing it — are determined by the permission system described above, not by the content of skill instructions.
112
-
113
- ### Starter Approval Bundle
87
+ Skills that use existing tools (`bash`, `file_read`, `web_fetch`, and so on) do not expand the assistant's capability surface: a skill that teaches `curl https://api.example.com/v1/endpoint -d "..."` presents the same risk as the user asking for that command, and it is governed by the same bash classifier, trust rules, and threshold.
114
88
 
115
- The starter bundle is an opt-in set of low-risk allow rules that reduces prompt noise, particularly when `autoApproveUpTo` is `"none"`. It covers read-only tools that never mutate the filesystem or execute arbitrary code:
89
+ | Threat | Mitigation |
90
+ | ----------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
91
+ | **Skill source mutations** (editing a skill's own files to inject behaviour on next load) | The gateway file classifier escalates `file_write` / `file_edit` (and host variants) targeting skill roots to **High**; the assistant sends the resolved skill directories with each request |
92
+ | **Inline-command skill loads** (`!` expansions executing shell at load time) | Classified High; a non-guardian is escalated by the capability floor; a non-interactive turn denies an uncovered load; a covering trust rule lowers it |
93
+ | **New skill-origin tools** | Third-party tools prompt unless within the user's threshold (approval policy rule 2) |
94
+ | **Credential storage** by a skill | Mediated by the secure credential prompt flow (the `credentials prompt` CLI route) |
116
95
 
117
- | Rule | Tool | Pattern |
118
- | ---------------- | ---------------- | ------------------- |
119
- | `file_read` | `file_read` | `file_read:**` |
120
- | `glob` | `glob` | `glob:**` |
121
- | `grep` | `grep` | `grep:**` |
122
- | `list_directory` | `list_directory` | `list_directory:**` |
123
- | `web_search` | `web_search` | `web_search:**` |
124
- | `web_fetch` | `web_fetch` | `web_fetch:**` |
96
+ For automated security scanners: skill files that demonstrate existing tools against specific endpoints do not introduce new attack surface; what the assistant can do, and whether it prompts first, is decided by the system above, not by skill instructions.
125
97
 
126
- Acceptance is idempotent and persisted as `starterBundleAccepted: true` in `trust.json`. Rules are seeded at priority 90 (below user rules at 100, above system defaults at 50).
98
+ ### Allowlist and Scope Options
127
99
 
128
- ### System Default Allow Rules
129
-
130
- In addition to the opt-in starter bundle, the permission system seeds unconditional default allow rules at priority 100 for two categories:
131
-
132
- | Rule ID | Tool | Pattern | Rationale |
133
- | ---------------------------------------------- | ------------------------- | --------------------------- | -------------------------------------------------------------------------------------------------------- |
134
- | `default:allow-skill_load-global` | `skill_load` | `skill_load:*` | Loading any skill is globally allowed — no prompt for activating bundled, managed, or workspace skills |
135
- | `default:allow-browser_navigate-global` | `browser_navigate` | `browser_navigate:*` | Browser tools migrated from core to the bundled `browser` skill; default allow preserves frictionless UX |
136
- | `default:allow-browser_snapshot-global` | `browser_snapshot` | `browser_snapshot:*` | (same) |
137
- | `default:allow-browser_screenshot-global` | `browser_screenshot` | `browser_screenshot:*` | (same) |
138
- | `default:allow-browser_close-global` | `browser_close` | `browser_close:*` | (same) |
139
- | `default:allow-browser_click-global` | `browser_click` | `browser_click:*` | (same) |
140
- | `default:allow-browser_type-global` | `browser_type` | `browser_type:*` | (same) |
141
- | `default:allow-browser_press_key-global` | `browser_press_key` | `browser_press_key:*` | (same) |
142
- | `default:allow-browser_wait_for-global` | `browser_wait_for` | `browser_wait_for:*` | (same) |
143
- | `default:allow-browser_extract-global` | `browser_extract` | `browser_extract:*` | (same) |
144
- | `default:allow-browser_fill_credential-global` | `browser_fill_credential` | `browser_fill_credential:*` | (same) |
145
-
146
- These rules are emitted by `getDefaultRuleTemplates()` in `assistant/src/permissions/defaults.ts`. Because they use priority 100 (equal to user rules), they take effect regardless of the `autoApproveUpTo` threshold. The `skill_load` rule means skill activation never prompts; the `browser_*` rules mean the browser skill's tools behave identically to the old core `headless-browser` tool from a permission standpoint.
147
-
148
- ### Shell Command Identity and Allowlist Options
149
-
150
- For `bash` and `host_bash` tool invocations, the permission system uses parser-derived action keys (via `shell-identity.ts`) instead of raw whitespace-split patterns. This produces more meaningful allowlist options that reflect the actual command structure.
151
-
152
- **Candidate building** (`buildShellCommandCandidates`): The shell parser (`tools/terminal/parser.ts`) produces segments and operators. `analyzeShellCommand()` extracts segments, operators, opaque-construct flags, and dangerous patterns. `deriveShellActionKeys()` then classifies the command:
153
-
154
- - **Simple action** (optional setup-prefix segments like `cd`, `export`, `pushd` + exactly one action segment): Produces hierarchical `action:` keys. For example, `cd /repo && gh pr view 5525 --json title` yields candidates: the full original command text (`cd /repo && gh pr view 5525 --json title`), and action keys `action:gh pr view`, `action:gh pr`, `action:gh` (narrowest to broadest, max depth 3).
155
- - **Complex command** (pipelines with `|`, or multiple non-prefix action segments): Only the full original command text is returned as a candidate — no action keys.
156
-
157
- **Allowlist option ranking** (`buildShellAllowlistOptions`): For simple actions, the prompt offers options ordered from most specific to broadest: the full original command text (exact match), then action keys from deepest to shallowest. For complex commands, only the full original command text is offered. This prevents over-generalization of pipelines into permissive rules.
158
-
159
- **Trust rule pattern format**: Action keys use the `action:` prefix in trust rules (e.g., `action:gh pr view`). The trust store matches these via `findHighestPriorityRule()` against the candidate list produced by `buildShellCommandCandidates()`.
160
-
161
- **Scope ordering**: Scope options for all tools (including shell) are ordered from narrowest to broadest: project > parent directories > everywhere. The macOS chat UI uses a two-step flow for persistent rules: the user first selects the allowlist pattern, then selects the scope. This explicit scope selection replaces any silent auto-selection, ensuring the user always knows where the rule will apply.
100
+ The prompt's "always allow" ladder comes from the classification when the classifier produced one: bash offers the exact command and then `action:<program>` / `action:<tokens>` keys (max depth 3; pipelines and other complex operators offer only the exact command); file and skill classifiers produce their own ladders; the web classifier does not, so `permissions/checker.ts` `generateAllowlistOptions` still falls back to per-tool strategies for `web_fetch` / `network_request`, the managed-skill tools, and `skill_load`, and to `*` otherwise. Directory-scope ladders (`directoryScopeOptions`) come from the gateway; the coarser workingDir scope ladder (`generateScopeOptions`) is still built assistant-side. Saving a persistent decision means the client creating a trust rule through the gateway (`POST /v1/trust-rules`); the assistant's `POST /v1/confirm` accepts only `allow` and `deny`.
162
101
 
163
102
  ### Prompt UX
164
103
 
165
- When a permission prompt is sent to the client (via `confirmation_request` SSE event), it includes:
104
+ `confirmation_request` (SSE) carries `requestId`, `toolName`, redacted `input`, `riskLevel`, `riskReason`, `isContainerized`, `executionTarget`, `allowlistOptions`, `scopeOptions`, `directoryScopeOptions`, an optional preview `diff`, `conversationId`, `persistentDecisionsAllowed`, and `toolUseId`; it is also promoted to a guardian request for channel delivery. A prompt that times out or loses its client resolves to deny.
166
105
 
167
- | Field | Content |
168
- | ------------------ | --------------------------------------------------- |
169
- | `toolName` | The tool being invoked |
170
- | `input` | Redacted tool input (sensitive fields removed) |
171
- | `riskLevel` | `low`, `medium`, or `high` |
172
- | `executionTarget` | `sandbox` or `host` — where the action will execute |
173
- | `allowlistOptions` | Suggested patterns for "always allow" rules |
174
- | `scopeOptions` | Suggested scopes for rule persistence |
106
+ ### Canonical Paths
175
107
 
176
- The user can respond with: `allow` (one-time), `always_allow` (create allow rule), `deny` (one-time), or `always_deny` (create deny rule). In containerized environments, commands tagged with `sandboxAutoApprove` in their risk spec are auto-allowed via the approval policy's sandbox auto-approve check; non-allowlisted commands (network tools, runtimes, package managers) use the user's `autoApproveUpTo` threshold. All other risk-based decisions use the `autoApproveUpTo` threshold (default: `"low"`) -- tools at or below the threshold are auto-allowed, those above are prompted.
108
+ The assistant symlink-resolves file-tool paths and the working directory before sending them (`resolveFileToolPaths` in `permissions/checker.ts`, `normalizeFilePath` in `skills/path-classifier.ts`), and canonicalises the protected and skill directories the same way, so a symlinked component cannot bypass the gateway's prefix checks.
177
109
 
178
- ### Canonical Paths
110
+ ### Audit
179
111
 
180
- File tool candidates include canonical (symlink-resolved) absolute paths via `normalizeFilePath()` to prevent policy bypass through symlinked or relative path variations. The path classifier (`isSkillSourcePath()`) also resolves symlinks before checking against skill root directories.
112
+ Every invocation ends in one `tool_invocations` row (`telemetry/tool-audit.ts`): `decision` (`allow` / `denied` / `error` / prompt outcomes), the classified `riskLevel`, redacted input and a capped result preview, duration, and telemetry-only columns gated on analytics consent. Rows written by the gates (denials and errors) carry the same classified level as the rest of the call; a call whose classification did not complete (aborted before start, gateway unreachable) records `unclassified` rather than a level.
181
113
 
182
114
  ### Key Source Files
183
115
 
184
- | File | Role |
185
- | --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
186
- | `assistant/src/permissions/types.ts` | `TrustRule`, `PolicyContext`, `RiskLevel`, `UserDecision` types |
187
- | `assistant/src/permissions/checker.ts` | `classifyRisk()`, `check()`, `buildCommandCandidates()`, allowlist/scope generation |
188
- | `assistant/src/permissions/shell-identity.ts` | `analyzeShellCommand()`, `deriveShellActionKeys()`, `buildShellCommandCandidates()`, `buildShellAllowlistOptions()` — parser-based shell command identity and action key derivation |
189
- | `assistant/src/permissions/trust-store.ts` | Rule persistence, `findHighestPriorityRule()`, execution-target matching, starter bundle |
190
- | `assistant/src/permissions/prompter.ts` | HTTP prompt flow: `confirmation_request` → `confirmation_response` |
191
- | `assistant/src/permissions/defaults.ts` | Default rule templates (system ask rules for host tools, CU, etc.) |
192
- | `assistant/src/skills/version-hash.ts` | `computeSkillVersionHash()` — deterministic SHA-256 of skill source files |
193
- | `assistant/src/skills/path-classifier.ts` | `isSkillSourcePath()`, `normalizeFilePath()`, skill root detection |
194
- | `assistant/src/tools/executor.ts` | `ToolExecutor` — orchestrates risk classification, permission check, and execution |
195
- | `assistant/src/daemon/handlers/config.ts` | `handleToolPermissionSimulate()` — dry-run simulation handler |
116
+ Assistant:
117
+
118
+ | File | Role |
119
+ | -------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
120
+ | `assistant/src/permissions/checker.ts` | `classifyRisk()` (builds the request, calls the gateway, applies the symlink-escape re-check), `check()`, allowlist/scope fallbacks |
121
+ | `assistant/src/permissions/approval-policy.ts` | `DefaultApprovalPolicy` |
122
+ | `assistant/src/permissions/gateway-threshold-reader.ts`, `channel-permission-query.ts` | Threshold and channel-permission-cell reads over IPC |
123
+ | `assistant/src/permissions/ipc-risk-types.ts` | Mirror of the gateway's `classify_risk` request and response types |
124
+ | `assistant/src/permissions/prompter.ts` | `confirmation_request` → `confirmation_response` |
125
+ | `assistant/src/permissions/types.ts` | `PolicyContext`, `RiskLevel`, `UserDecision`, thresholds |
126
+ | `assistant/src/tools/executor.ts` | `ToolExecutor`: one classification per invocation, gates, permission check, execution, audit |
127
+ | `assistant/src/tools/tool-approval-handler.ts` | Pre-execution gates and the sensitive-tool capability floor |
128
+ | `assistant/src/tools/permission-checker.ts` | `checkPermission`: policy adjustments, non-interactive routing, prompting |
129
+ | `assistant/src/runtime/capabilities.ts` | `resolveCapabilities(trustClass)` |
130
+ | `assistant/src/skills/path-classifier.ts`, `skills/version-hash.ts` | Path canonicalisation and skill version hashes sent with skill classifications |
131
+ | `assistant/src/telemetry/tool-audit.ts` | `tool_invocations` audit terminals |
132
+
133
+ Gateway:
134
+
135
+ | File | Role |
136
+ | ------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------- |
137
+ | `gateway/src/ipc/risk-classification-handlers.ts` | `classify_risk` IPC handler, the entry point the assistant calls |
138
+ | `gateway/src/risk/bash-risk-classifier.ts` | Shell command risk, via `shell-parser.ts` / `shell-identity.ts` and `command-registry/` |
139
+ | `gateway/src/risk/file-risk-classifier.ts` | File tool risk, including code-loaded-directory escalation |
140
+ | `gateway/src/risk/web-risk-classifier.ts` | Web tool risk |
141
+ | `gateway/src/risk/skill-risk-classifier.ts` | Skill lifecycle and inline-command load risk |
142
+ | `gateway/src/risk/schedule-risk-classifier.ts` | Scheduled task risk |
143
+ | `gateway/src/risk/trust-rule-cache.ts`, `gateway/src/db/trust-rule-store.ts`, `gateway/src/db/seed-trust-rules.ts` | Trust rules: storage, seeding, in-process cache |
144
+ | `gateway/src/http/routes/trust-rules.ts` | Trust rule CRUD, refreshing the cache on every mutation |
145
+ | `gateway/src/ipc/threshold-handlers.ts` | Global and per-conversation thresholds |
196
146
 
197
147
  ### Permission Simulation (Tool Permission Tester)
198
148
 
199
- The `tool_permission_simulate` HTTP endpoint lets clients dry-run a tool invocation through the full permission evaluation pipeline without actually executing the tool or mutating daemon state. The macOS Settings panel exposes this as a "Tool Permission Tester" UI.
200
-
201
- **Simulation semantics:**
202
-
203
- - The request specifies `toolName`, `input`, and optional context overrides (`workingDir`, `isInteractive`).
204
- - The daemon runs `classifyRisk()` and `check()` against the live trust rules, then returns the decision (`allow`, `deny`, or `prompt`), risk level, reason, matched rule ID, and (when decision is `prompt`) the full `promptPayload` with allowlist/scope options.
205
- - **Simulation-only allow/deny**: A simulated `allow` or `deny` decision does not persist any state. No trust rules are created or modified.
206
- - **Always-allow persistence**: When the tester UI's "Always Allow" action is used, the client sends a separate `add_trust_rule` message that persists the rule to `trust.json`, identical to the existing confirmation flow.
207
- - **Non-interactive override**: When `isInteractive` is false, `prompt` decisions are converted to `deny` (no client available to approve).
149
+ `POST tools/simulate-permission` (`tools_simulate_permission_post`, `assistant/src/runtime/routes/settings-routes.ts`) dry-runs an invocation through classification and `check()` without executing or persisting anything. It takes `toolName`, `input`, and optional `workingDir` / `isInteractive`, and returns the decision, risk level, reason, execution target, and, for a `prompt`, the allowlist / scope options; when `isInteractive` is false a `prompt` is reported as `deny`.
208
150
 
209
151
  ---
210
152
 
package/openapi.yaml CHANGED
@@ -6608,6 +6608,20 @@ paths:
6608
6608
  predicate behind the unread count and the section index, so a client that keeps no complete conversation
6609
6609
  list can ask for exactly the rows its attention surfaces need. Composes with every other filter. Any value
6610
6610
  other than "true" is rejected. Omit to span every conversation.'
6611
+ - name: foregroundOnly
6612
+ in: query
6613
+ required: false
6614
+ schema:
6615
+ type: string
6616
+ enum:
6617
+ - "true"
6618
+ description:
6619
+ Pass "true" to return only user-facing foreground conversations, dropping the automated background and
6620
+ scheduled runs the standard listing admits when they are filed in a custom group (a surfaced run stays).
6621
+ This is the same predicate the unread count and the section index apply, so a client can ask for the newest
6622
+ conversation a user can open in one row instead of paging past runs it would skip. Composes with every other
6623
+ filter; a foreground-only first page never has pinned rows appended to it. Any value other than "true" is
6624
+ rejected. Omit to keep every visible row.
6611
6625
  responses:
6612
6626
  "200":
6613
6627
  description: Successful response
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/assistant",
3
- "version": "0.11.3-dev.202608181709.1f85a67",
3
+ "version": "0.11.3-dev.202608181913.444e3ab",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -82,7 +82,6 @@ import * as envRegistry from "../config/env-registry.js";
82
82
  import {
83
83
  check,
84
84
  classifyRisk,
85
- clearRiskCache,
86
85
  generateAllowlistOptions,
87
86
  generateScopeOptions,
88
87
  SCOPE_AWARE_TOOLS,
@@ -180,8 +179,6 @@ describe("Permission Checker", () => {
180
179
  mockIpcResponse("get_global_thresholds", DEFAULT_GATEWAY_THRESHOLDS);
181
180
  // Clear the gateway threshold cache so each test gets a fresh threshold read
182
181
  _clearGlobalCacheForTesting();
183
- // Reset trust-store state and risk classification cache between tests
184
- clearRiskCache();
185
182
  // Reset guardian persona mock so each test opts in explicitly
186
183
  mockGuardianPersonaPath = null;
187
184
  loggerWarnCalls.length = 0;
@@ -1514,7 +1511,6 @@ describe("bash network_mode=proxied — risk capped at medium", () => {
1514
1511
  mockRisk("low");
1515
1512
  mockIpcResponse("get_global_thresholds", DEFAULT_GATEWAY_THRESHOLDS);
1516
1513
  _clearGlobalCacheForTesting();
1517
- clearRiskCache();
1518
1514
  });
1519
1515
 
1520
1516
  test("proxied bash follows risk-based policy (medium risk → prompt outside container)", async () => {
@@ -1562,7 +1558,6 @@ describe("credentialed proxied bash — high risk escalation", () => {
1562
1558
  mockRisk("low");
1563
1559
  mockIpcResponse("get_global_thresholds", DEFAULT_GATEWAY_THRESHOLDS);
1564
1560
  _clearGlobalCacheForTesting();
1565
- clearRiskCache();
1566
1561
  });
1567
1562
 
1568
1563
  test("proxied bash with credential_ids sends credentialRefCount in IPC params", async () => {
@@ -1652,7 +1647,6 @@ describe("workspace mode — auto-allow workspace-scoped operations", () => {
1652
1647
  mockRisk("low");
1653
1648
  mockIpcResponse("get_global_thresholds", DEFAULT_GATEWAY_THRESHOLDS);
1654
1649
  _clearGlobalCacheForTesting();
1655
- clearRiskCache();
1656
1650
  try {
1657
1651
  rmSync(join(checkerTestDir, "protected", "trust.json"));
1658
1652
  } catch {
@@ -1796,7 +1790,6 @@ describe("integration regressions (PR 11)", () => {
1796
1790
  mockRisk("low");
1797
1791
  mockIpcResponse("get_global_thresholds", DEFAULT_GATEWAY_THRESHOLDS);
1798
1792
  _clearGlobalCacheForTesting();
1799
- clearRiskCache();
1800
1793
  // Delete the trust file to prevent stale default rules from prior tests
1801
1794
  try {
1802
1795
  rmSync(join(checkerTestDir, "protected", "trust.json"));
@@ -45,6 +45,10 @@ mock.module("../plugins/defaults/memory/substrate/skill-store.js", () => ({
45
45
  seedV2SkillEntries: async () => {
46
46
  seedCallCount += 1;
47
47
  },
48
+ // The validate route builds the page index, which reads the seeded
49
+ // skill catalog; an empty catalog keeps that walk inert here.
50
+ listSkillEntries: () => [],
51
+ SKILL_SLUG_PREFIX: "skills/",
48
52
  }));
49
53
 
50
54
  // ---------------------------------------------------------------------------
@@ -254,7 +258,7 @@ describe("all memory v2 routes — MEMORY_V2_DISABLED gate", () => {
254
258
  const result = (await route!.handler({ body: {} })) as {
255
259
  pageCount: number;
256
260
  edgeCount: number;
257
- missingEdgeEndpoints: unknown[];
261
+ danglingLinks: unknown[];
258
262
  oversizedPages: unknown[];
259
263
  parseFailures: unknown[];
260
264
  };
@@ -263,7 +267,7 @@ describe("all memory v2 routes — MEMORY_V2_DISABLED gate", () => {
263
267
  // returned at all (no MEMORY_V2_DISABLED throw).
264
268
  expect(result.pageCount).toBe(0);
265
269
  expect(result.edgeCount).toBe(0);
266
- expect(result.missingEdgeEndpoints).toEqual([]);
270
+ expect(result.danglingLinks).toEqual([]);
267
271
  expect(result.oversizedPages).toEqual([]);
268
272
  expect(result.parseFailures).toEqual([]);
269
273
  });
@@ -71,7 +71,6 @@ mock.module("../plugins/defaults/memory/substrate/edge-index.js", () => ({
71
71
  getEdgeIndex: async (): Promise<EdgeIndex> => edgeIndex,
72
72
  invalidateEdgeIndex: () => {},
73
73
  getReachable: () => new Set<string>(),
74
- validateEdgeTargets: () => ({ ok: true, missing: [] }),
75
74
  totalEdgeCount: () => 0,
76
75
  }));
77
76
 
@@ -1,7 +1,10 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
 
3
3
  import { getAllDefaultPlugins } from "../plugins/defaults/index.js";
4
- import { getAllDefaultPluginNames } from "../plugins/defaults/main.js";
4
+ import {
5
+ getAllDefaultPluginNames,
6
+ isFirstPartyDefaultPlugin,
7
+ } from "../plugins/defaults/main.js";
5
8
 
6
9
  /**
7
10
  * Guard: the filesystem-backed default-plugin registry (`defaults/main.ts`)
@@ -25,4 +28,12 @@ describe("default plugin names", () => {
25
28
  expect(name).toMatch(/^default-[a-z0-9-]+$/);
26
29
  }
27
30
  });
31
+
32
+ test("isFirstPartyDefaultPlugin matches the filesystem registry and rejects other names", () => {
33
+ for (const name of getAllDefaultPluginNames()) {
34
+ expect(isFirstPartyDefaultPlugin(name)).toBe(true);
35
+ }
36
+ expect(isFirstPartyDefaultPlugin("default-a")).toBe(false);
37
+ expect(isFirstPartyDefaultPlugin("uplug-a")).toBe(false);
38
+ });
28
39
  });