@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.
- package/AGENTS.md +1 -1
- package/docs/architecture/memory.md +17 -5
- package/docs/architecture/security.md +95 -153
- package/openapi.yaml +14 -0
- package/package.json +1 -1
- package/src/__tests__/checker.test.ts +0 -7
- package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +6 -2
- package/src/__tests__/context-search-memory-v2-source.test.ts +0 -1
- package/src/__tests__/default-plugin-names-guard.test.ts +12 -1
- package/src/__tests__/file-ops-service.test.ts +163 -30
- package/src/__tests__/filesystem-tools.test.ts +23 -24
- package/src/__tests__/host-file-read-tool.test.ts +16 -19
- package/src/__tests__/inline-skill-load-permissions.test.ts +23 -8
- package/src/__tests__/mtime-cache.test.ts +2 -0
- package/src/__tests__/plugin-disabled-state.test.ts +2 -0
- package/src/__tests__/plugin-effective-enabled-set.test.ts +55 -3
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +132 -1
- package/src/__tests__/tool-executor.test.ts +43 -28
- package/src/__tests__/user-plugin-loader.test.ts +2 -0
- package/src/api/events/host-file.ts +2 -2
- package/src/cli/commands/memory/__tests__/memory-v2.test.ts +57 -7
- package/src/cli/commands/memory/index.help.ts +41 -27
- package/src/cli/commands/memory/index.ts +2 -0
- package/src/cli/commands/memory/memory-v2.ts +58 -54
- package/src/cli/commands/memory/memory-validate.ts +18 -0
- package/src/daemon/conversation-runtime-assembly.ts +1 -1
- package/src/hooks/registry.ts +65 -51
- package/src/permissions/AGENTS.md +16 -0
- package/src/permissions/checker.test.ts +58 -92
- package/src/permissions/checker.ts +48 -206
- package/src/permissions/risk-types.ts +3 -42
- package/src/persistence/conversation-queries.ts +14 -1
- package/src/plugins/defaults/main.ts +15 -0
- package/src/plugins/defaults/memory/AGENTS.md +11 -3
- package/src/plugins/defaults/memory/graph-topology/pending-buffer.ts +9 -12
- package/src/plugins/defaults/memory/src/memory-v2-routes.ts +12 -10
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +114 -0
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +8 -0
- package/src/plugins/defaults/memory/substrate/__tests__/edge-index.test.ts +0 -30
- package/src/plugins/defaults/memory/substrate/__tests__/ingest.test.ts +73 -1
- package/src/plugins/defaults/memory/substrate/__tests__/page-index.test.ts +37 -0
- package/src/plugins/defaults/memory/substrate/__tests__/page-links.test.ts +208 -0
- package/src/plugins/defaults/memory/substrate/__tests__/prompts-consolidation.test.ts +158 -1
- package/src/plugins/defaults/memory/substrate/consolidation-job.ts +68 -6
- package/src/plugins/defaults/memory/substrate/edge-index.ts +0 -31
- package/src/plugins/defaults/memory/substrate/ingest.ts +59 -0
- package/src/plugins/defaults/memory/substrate/page-index.ts +35 -8
- package/src/plugins/defaults/memory/substrate/page-links.ts +133 -0
- package/src/plugins/defaults/memory/substrate/page-store.ts +10 -0
- package/src/plugins/defaults/memory/substrate/prompts/consolidation.ts +124 -34
- package/src/plugins/defaults/memory/v3/card.ts +9 -9
- package/src/plugins/defaults/memory/v3/core-set.test.ts +7 -0
- package/src/plugins/defaults/memory/v3/core-set.ts +5 -1
- package/src/plugins/defaults/memory/v3/edge.ts +13 -57
- package/src/plugins/pipeline.ts +4 -3
- package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +133 -0
- package/src/runtime/routes/conversation-list-routes.ts +22 -7
- package/src/runtime/routes/settings-routes.ts +9 -2
- package/src/subagent/types.ts +7 -7
- package/src/tools/__tests__/tool-input-schemas.test.ts +7 -7
- package/src/tools/executor.ts +32 -2
- package/src/tools/filesystem/read.ts +27 -10
- package/src/tools/host-filesystem/read.ts +27 -15
- package/src/tools/permission-checker.ts +33 -32
- package/src/tools/shared/filesystem/file-ops-service.ts +63 -35
- package/src/tools/shared/filesystem/legacy-read-args.ts +22 -0
- package/src/tools/shared/filesystem/types.ts +5 -5
- 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
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
|
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,
|
|
14
|
-
CLASSIFY -->
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
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"`
|
|
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
|
-
|
|
48
|
+
### Approval Policy
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
`DefaultApprovalPolicy.evaluate` (`assistant/src/permissions/approval-policy.ts`) turns a classified risk into allow / prompt, in this order:
|
|
51
51
|
|
|
52
|
-
|
|
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
|
-
|
|
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
|
-
|
|
60
|
+
### Trust Rules (v3)
|
|
65
61
|
|
|
66
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
68
|
+
Classifiers (`gateway/src/risk/`), keyed by tool:
|
|
82
69
|
|
|
83
|
-
|
|
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 `
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
98
|
+
### Allowlist and Scope Options
|
|
127
99
|
|
|
128
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
###
|
|
110
|
+
### Audit
|
|
179
111
|
|
|
180
|
-
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
|
187
|
-
|
|
|
188
|
-
| `assistant/src/permissions/
|
|
189
|
-
| `assistant/src/permissions/
|
|
190
|
-
| `assistant/src/permissions/
|
|
191
|
-
| `assistant/src/permissions/
|
|
192
|
-
| `assistant/src/
|
|
193
|
-
| `assistant/src/
|
|
194
|
-
| `assistant/src/tools/executor.ts`
|
|
195
|
-
| `assistant/src/
|
|
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
|
-
|
|
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
|
@@ -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
|
-
|
|
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.
|
|
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 {
|
|
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
|
});
|