@gotgenes/pi-permission-system 18.0.2 → 18.1.1
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/CHANGELOG.md +22 -0
- package/docs/assets/logo.png +0 -0
- package/docs/assets/logo.svg +213 -0
- package/docs/configuration.md +689 -0
- package/docs/cross-extension-api.md +525 -0
- package/docs/guides/permission-frontmatter-for-subagent-extensions.md +201 -0
- package/docs/guides/upstream-issue-template.md +113 -0
- package/docs/migration/legacy-to-flat.md +365 -0
- package/docs/opencode-compatibility.md +213 -0
- package/docs/session-approvals.md +68 -0
- package/docs/subagent-integration.md +102 -0
- package/docs/troubleshooting.md +53 -0
- package/package.json +5 -1
- package/src/access-intent/bash/bash-path-resolver.ts +30 -10
- package/src/access-intent/bash/program.ts +9 -0
- package/src/access-intent/bash/token-classification.ts +39 -9
- package/src/handlers/gates/tool-call-gate-pipeline.ts +13 -2
- package/src/permission-manager.ts +51 -1
- package/src/permission-session.ts +12 -0
- package/src/rule.ts +1 -1
- package/src/types.ts +11 -0
- package/test/access-intent/bash/program.test.ts +57 -0
- package/test/access-intent/bash/token-classification.test.ts +47 -0
- package/test/composition-root.test.ts +73 -0
- package/test/handlers/gates/tool-call-gate-pipeline.test.ts +26 -0
- package/test/helpers/gate-fixtures.ts +7 -1
- package/test/helpers/session-fixtures.ts +8 -1
- package/test/permission-manager-unified.test.ts +81 -0
- package/test/permission-resolver.test.ts +8 -1
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# Permission Frontmatter for Subagent Extensions
|
|
2
|
+
|
|
3
|
+
A convention guide for pi-subagent extension authors who want to offer users richer per-agent permission control.
|
|
4
|
+
|
|
5
|
+
## Motivation
|
|
6
|
+
|
|
7
|
+
Pi subagent extensions already let users restrict **which tools** an agent can see via frontmatter keys like `tools:`, `disallowed_tools:`, or `deny-tools:`.
|
|
8
|
+
These binary allow/deny mechanisms are simple and effective for tool visibility.
|
|
9
|
+
|
|
10
|
+
The `pi-permission-system` extension adds a second layer: **policy evaluation** with three states — `allow`, `ask`, and `deny` — across multiple permission surfaces (tools, bash commands, MCP operations, skills, external directories, and special operations).
|
|
11
|
+
|
|
12
|
+
By documenting the `permission:` frontmatter key in your extension, you give users a single agent file that expresses both visibility restrictions (your extension) and runtime policy (the permission system) without any code coupling between the two extensions.
|
|
13
|
+
|
|
14
|
+
## The Two-Layer Model
|
|
15
|
+
|
|
16
|
+
```text
|
|
17
|
+
┌──────────────────────────────────────────────────────┐
|
|
18
|
+
│ Layer 1 – Visibility (your extension) │
|
|
19
|
+
│ Controls which tools are registered / active │
|
|
20
|
+
│ before the agent session starts. │
|
|
21
|
+
├──────────────────────────────────────────────────────┤
|
|
22
|
+
│ Layer 2 – Policy (pi-permission-system) │
|
|
23
|
+
│ Controls allow / ask / deny decisions on every │
|
|
24
|
+
│ tool call, bash command, MCP operation, etc. │
|
|
25
|
+
└──────────────────────────────────────────────────────┘
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The two layers compose additively:
|
|
29
|
+
|
|
30
|
+
1. A tool hidden by your extension is never seen by the permission system — policy for it is irrelevant.
|
|
31
|
+
2. A tool denied by the permission system is removed from the active set before the agent starts — your extension's allowlist cannot restore it.
|
|
32
|
+
3. Both denylist mechanisms are additive.
|
|
33
|
+
A tool blocked by either layer stays blocked.
|
|
34
|
+
|
|
35
|
+
## The `permission:` Frontmatter Format
|
|
36
|
+
|
|
37
|
+
The `permission:` key uses a flat policy map.
|
|
38
|
+
Each top-level key is either a tool name (for per-tool policy) or a named surface (`bash`, `mcp`, `skill`, `external_directory`, `special`).
|
|
39
|
+
The special key `"*"` is the universal fallback.
|
|
40
|
+
|
|
41
|
+
### Minimal example
|
|
42
|
+
|
|
43
|
+
```yaml
|
|
44
|
+
---
|
|
45
|
+
permission:
|
|
46
|
+
"*": ask
|
|
47
|
+
read: allow
|
|
48
|
+
write: deny
|
|
49
|
+
---
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
This means: allow all read operations without prompting, deny all write operations, and ask the user for everything else.
|
|
53
|
+
|
|
54
|
+
### Full example with bash patterns
|
|
55
|
+
|
|
56
|
+
```yaml
|
|
57
|
+
---
|
|
58
|
+
permission:
|
|
59
|
+
"*": ask
|
|
60
|
+
read: allow
|
|
61
|
+
bash:
|
|
62
|
+
"*": ask
|
|
63
|
+
"git status": allow
|
|
64
|
+
"git diff *": allow
|
|
65
|
+
"npm test": allow
|
|
66
|
+
mcp:
|
|
67
|
+
"*": deny
|
|
68
|
+
skill:
|
|
69
|
+
"*": ask
|
|
70
|
+
external_directory:
|
|
71
|
+
"*": deny
|
|
72
|
+
"~/projects/*": allow
|
|
73
|
+
---
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Composing with your extension's keys
|
|
77
|
+
|
|
78
|
+
Users can freely combine `permission:` with your extension's tool restriction key:
|
|
79
|
+
|
|
80
|
+
```yaml
|
|
81
|
+
---
|
|
82
|
+
# nicobailon/pi-subagents: restrict visible tools
|
|
83
|
+
tools: bash,read_file,write_file
|
|
84
|
+
|
|
85
|
+
# pi-permission-system: policy within the visible set
|
|
86
|
+
permission:
|
|
87
|
+
"*": ask
|
|
88
|
+
read_file: allow
|
|
89
|
+
bash:
|
|
90
|
+
"*": ask
|
|
91
|
+
"git *": allow
|
|
92
|
+
---
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Both keys are read independently by their respective extensions.
|
|
96
|
+
There is no key collision — `permission:` is exclusively consumed by `pi-permission-system`.
|
|
97
|
+
|
|
98
|
+
## Permission Surfaces
|
|
99
|
+
|
|
100
|
+
| Surface | Key | Value format | Description |
|
|
101
|
+
| -------------------- | -------------------- | ---------------------------- | ------------------------------------------ |
|
|
102
|
+
| Tools | `<tool_name>` | `"allow" \| "ask" \| "deny"` | Per-tool invocation policy |
|
|
103
|
+
| Bash | `bash` | `{ pattern: decision }` | Pattern-matched bash commands (glob-style) |
|
|
104
|
+
| MCP | `mcp` | `{ pattern: decision }` | MCP tool-level policy |
|
|
105
|
+
| Skills | `skill` | `{ pattern: decision }` | Skill invocation policy |
|
|
106
|
+
| External directories | `external_directory` | `{ pattern: decision }` | Path-based access outside the project |
|
|
107
|
+
| Special | `special` | `{ pattern: decision }` | Special operations (e.g. `subagent_spawn`) |
|
|
108
|
+
| Universal fallback | `"*"` | `"allow" \| "ask" \| "deny"` | Applies when no specific rule matches |
|
|
109
|
+
|
|
110
|
+
Pattern maps use last-match-wins ordering: put broad catch-alls first and specific overrides after.
|
|
111
|
+
|
|
112
|
+
## What Adoption Looks Like
|
|
113
|
+
|
|
114
|
+
Adopting this convention does **not** require your extension to:
|
|
115
|
+
|
|
116
|
+
- Import or depend on `pi-permission-system`
|
|
117
|
+
- Evaluate the `permission:` key at runtime
|
|
118
|
+
- Change your existing tool restriction mechanism
|
|
119
|
+
|
|
120
|
+
Adoption means:
|
|
121
|
+
|
|
122
|
+
1. **Document** the `permission:` key as an optional frontmatter field in your extension's README or agent authoring guide.
|
|
123
|
+
2. **Explain** that it is consumed by `pi-permission-system` when both extensions are installed.
|
|
124
|
+
3. **Show** a combined example with your extension's key alongside `permission:`.
|
|
125
|
+
|
|
126
|
+
The permission system handles all evaluation, prompt dialogs, and policy enforcement independently.
|
|
127
|
+
|
|
128
|
+
## Runtime Integration (Optional)
|
|
129
|
+
|
|
130
|
+
If your extension runs subagents in-process (e.g. via `createAgentSession()`), you can optionally query the permission system's policy at runtime via the event bus API — no package import needed.
|
|
131
|
+
|
|
132
|
+
### Querying policy
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
const requestId = crypto.randomUUID();
|
|
136
|
+
|
|
137
|
+
pi.events.on(
|
|
138
|
+
`permissions:rpc:check:reply:${requestId}`,
|
|
139
|
+
(raw) => {
|
|
140
|
+
const reply = raw as { success: boolean; data?: { result: string } };
|
|
141
|
+
if (reply.success) {
|
|
142
|
+
console.log(reply.data?.result); // "allow" | "deny" | "ask"
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
pi.events.emit("permissions:rpc:check", {
|
|
148
|
+
requestId,
|
|
149
|
+
surface: "bash",
|
|
150
|
+
value: "git push",
|
|
151
|
+
agentName: "Worker",
|
|
152
|
+
});
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Forwarding permission prompts
|
|
156
|
+
|
|
157
|
+
When a child agent encounters an `ask` permission and has no UI, the prompt can be forwarded to the parent session:
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
const requestId = crypto.randomUUID();
|
|
161
|
+
|
|
162
|
+
pi.events.on(
|
|
163
|
+
`permissions:rpc:prompt:reply:${requestId}`,
|
|
164
|
+
(raw) => {
|
|
165
|
+
const reply = raw as { success: boolean; data?: { approved: boolean } };
|
|
166
|
+
if (reply.success && reply.data?.approved) {
|
|
167
|
+
// proceed
|
|
168
|
+
} else {
|
|
169
|
+
// deny
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
pi.events.emit("permissions:rpc:prompt", {
|
|
175
|
+
requestId,
|
|
176
|
+
surface: "bash",
|
|
177
|
+
value: "rm -rf /tmp/build",
|
|
178
|
+
message: "Allow rm -rf /tmp/build?",
|
|
179
|
+
agentName: "Worker",
|
|
180
|
+
});
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
If `pi-permission-system` is not installed, no reply arrives.
|
|
184
|
+
Implement a timeout and treat no-reply as `deny` for graceful degradation.
|
|
185
|
+
|
|
186
|
+
For full API documentation, see [Cross-extension API](../cross-extension-api.md).
|
|
187
|
+
|
|
188
|
+
## Benefits for Your Users
|
|
189
|
+
|
|
190
|
+
1. **Richer semantics** — `ask` is more useful than binary allow/deny; users can permit a tool but require approval for each invocation.
|
|
191
|
+
2. **Unified config** — one `permission:` block per agent instead of separate restriction keys in multiple extensions.
|
|
192
|
+
3. **Surface coverage** — policy covers bash patterns, MCP tools, skills, external directories, and special operations, not just tool names.
|
|
193
|
+
4. **Forwarding** — permission prompts from headless child agents surface in the parent session's UI.
|
|
194
|
+
5. **Programmatic access** — the event bus API lets your extension query policy at runtime without importing the package.
|
|
195
|
+
|
|
196
|
+
## Further Reading
|
|
197
|
+
|
|
198
|
+
- [Subagent Integration](../subagent-integration.md) — full coexistence documentation and interaction rules
|
|
199
|
+
- [Cross-extension API](../cross-extension-api.md) — service accessor, event bus reference (decision broadcasts, RPC check, RPC prompt)
|
|
200
|
+
- [Configuration](../configuration.md) — full policy reference including merge precedence
|
|
201
|
+
- [Schema](../../schemas/permissions.schema.json) — canonical JSON Schema for the flat permission format
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# Upstream Issue Template
|
|
2
|
+
|
|
3
|
+
Template text for proposing the `permission:` frontmatter convention to subagent extension repositories.
|
|
4
|
+
Customize the placeholders (`{{...}}`) for each target repo.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Issue Title
|
|
9
|
+
|
|
10
|
+
> Proposal: document `permission:` frontmatter for per-agent permission policy
|
|
11
|
+
|
|
12
|
+
## Issue Body
|
|
13
|
+
|
|
14
|
+
````markdown
|
|
15
|
+
## Context
|
|
16
|
+
|
|
17
|
+
Users of both `{{your-extension}}` and [`pi-permission-system`](https://github.com/gotgenes/pi-permission-system) currently configure tool restrictions in two places:
|
|
18
|
+
|
|
19
|
+
1. **Tool visibility** via `{{your-key}}` in agent frontmatter (consumed by your extension)
|
|
20
|
+
2. **Permission policy** via `permission:` in the same frontmatter (consumed by pi-permission-system)
|
|
21
|
+
|
|
22
|
+
These two layers compose correctly today — there is no conflict — but users may not realize they can combine them in the same agent file.
|
|
23
|
+
|
|
24
|
+
## Proposal
|
|
25
|
+
|
|
26
|
+
Document the `permission:` key as an optional companion to `{{your-key}}` in your agent authoring docs.
|
|
27
|
+
This is purely a documentation change — no code dependency on pi-permission-system is needed.
|
|
28
|
+
|
|
29
|
+
### What `permission:` provides
|
|
30
|
+
|
|
31
|
+
- **Three-state policy**: `allow`, `ask` (prompt the user), or `deny` — richer than binary allow/deny
|
|
32
|
+
- **Multiple surfaces**: tools, bash commands (glob patterns), MCP operations, skills, external directories
|
|
33
|
+
- **Prompt forwarding**: `ask` decisions in headless child agents surface in the parent session's UI
|
|
34
|
+
- **Event bus API**: other extensions can query policy at runtime without importing the package
|
|
35
|
+
|
|
36
|
+
### Example
|
|
37
|
+
|
|
38
|
+
```yaml
|
|
39
|
+
---
|
|
40
|
+
# {{your-extension}}: restrict visible tools
|
|
41
|
+
{{your-key}}: {{example-value}}
|
|
42
|
+
|
|
43
|
+
# pi-permission-system (optional): policy within the visible set
|
|
44
|
+
permission:
|
|
45
|
+
"*": ask
|
|
46
|
+
read_file: allow
|
|
47
|
+
bash:
|
|
48
|
+
"*": ask
|
|
49
|
+
"git *": allow
|
|
50
|
+
---
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Two-layer model
|
|
54
|
+
|
|
55
|
+
```text
|
|
56
|
+
Layer 1 – Visibility ({{your-extension}})
|
|
57
|
+
→ Controls which tools are registered before the session starts
|
|
58
|
+
|
|
59
|
+
Layer 2 – Policy (pi-permission-system)
|
|
60
|
+
→ Controls allow/ask/deny decisions on every tool call, bash command, etc.
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
A tool hidden by Layer 1 is never evaluated by Layer 2.
|
|
64
|
+
A tool denied by Layer 2 cannot be restored by Layer 1.
|
|
65
|
+
Both mechanisms are additive — a tool blocked by either stays blocked.
|
|
66
|
+
|
|
67
|
+
### What adoption requires from you
|
|
68
|
+
|
|
69
|
+
1. Add a section to your README noting that `permission:` is an optional key consumed by pi-permission-system
|
|
70
|
+
2. Show a combined example with `{{your-key}}` + `permission:`
|
|
71
|
+
3. Link to the [convention guide](https://github.com/gotgenes/pi-permission-system/blob/main/docs/guides/permission-frontmatter-for-subagent-extensions.md) for full details
|
|
72
|
+
|
|
73
|
+
No code changes, no new dependency, no schema enforcement.
|
|
74
|
+
|
|
75
|
+
## References
|
|
76
|
+
|
|
77
|
+
- [Convention guide](https://github.com/gotgenes/pi-permission-system/blob/main/docs/guides/permission-frontmatter-for-subagent-extensions.md)
|
|
78
|
+
- [Subagent integration docs](https://github.com/gotgenes/pi-permission-system/blob/main/docs/subagent-integration.md)
|
|
79
|
+
- [Cross-extension API docs](https://github.com/gotgenes/pi-permission-system/blob/main/docs/cross-extension-api.md)
|
|
80
|
+
````
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Per-Repository Customization
|
|
85
|
+
|
|
86
|
+
### nicobailon/pi-subagents
|
|
87
|
+
|
|
88
|
+
| Placeholder | Value |
|
|
89
|
+
| -------------------- | --------------------------- |
|
|
90
|
+
| `{{your-extension}}` | `pi-subagents` |
|
|
91
|
+
| `{{your-key}}` | `tools` |
|
|
92
|
+
| `{{example-value}}` | `bash,read_file,write_file` |
|
|
93
|
+
|
|
94
|
+
### tintinweb/pi-subagents
|
|
95
|
+
|
|
96
|
+
| Placeholder | Value |
|
|
97
|
+
| -------------------- | ------------------ |
|
|
98
|
+
| `{{your-extension}}` | `pi-subagents` |
|
|
99
|
+
| `{{your-key}}` | `disallowed_tools` |
|
|
100
|
+
| `{{example-value}}` | `write_file,bash` |
|
|
101
|
+
|
|
102
|
+
Additional note for tintinweb: since this extension runs subagents in-process via `createAgentSession()`, mention the [Event Bus RPC](https://github.com/gotgenes/pi-permission-system/blob/main/docs/cross-extension-api.md#policy-query-rpc-deprecated) as an optional runtime integration path for querying or forwarding permission prompts without spawning a subprocess.
|
|
103
|
+
|
|
104
|
+
### HazAT/pi-interactive-subagents
|
|
105
|
+
|
|
106
|
+
| Placeholder | Value |
|
|
107
|
+
| -------------------- | -------------------------- |
|
|
108
|
+
| `{{your-extension}}` | `pi-interactive-subagents` |
|
|
109
|
+
| `{{your-key}}` | `deny-tools` |
|
|
110
|
+
| `{{example-value}}` | `write_file,bash` |
|
|
111
|
+
|
|
112
|
+
Additional note for HazAT: this extension already uses `PI_DENY_TOOLS` env var for subprocess tool denial.
|
|
113
|
+
The `permission:` frontmatter provides the same effect via `tool_name: deny` but adds `ask` as an intermediate option and covers surfaces beyond tools.
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
# Migration guide: legacy format → flat permission format
|
|
2
|
+
|
|
3
|
+
This guide covers migration from the pre-#66 config format to the flat `permission` format introduced in #66.
|
|
4
|
+
|
|
5
|
+
## Summary of changes
|
|
6
|
+
|
|
7
|
+
The old format had six top-level policy keys (`defaultPolicy`, `tools`, `bash`, `mcp`, `skills`, `special`).
|
|
8
|
+
The new format has a single `permission` key whose top-level entries map surface names to actions.
|
|
9
|
+
|
|
10
|
+
Runtime knobs (`debugLog`, `permissionReviewLog`, `yoloMode`) are **unchanged** and stay at the top level.
|
|
11
|
+
|
|
12
|
+
## Checklist
|
|
13
|
+
|
|
14
|
+
Go through each section below.
|
|
15
|
+
For each key present in your config, apply the translation and remove the old key.
|
|
16
|
+
|
|
17
|
+
- [ ] `defaultPolicy`
|
|
18
|
+
- [ ] `tools`
|
|
19
|
+
- [ ] `bash`
|
|
20
|
+
- [ ] `mcp`
|
|
21
|
+
- [ ] `skills`
|
|
22
|
+
- [ ] `special`
|
|
23
|
+
- [ ] Per-agent frontmatter
|
|
24
|
+
|
|
25
|
+
## Translation reference
|
|
26
|
+
|
|
27
|
+
### `defaultPolicy`
|
|
28
|
+
|
|
29
|
+
`defaultPolicy` set per-surface fallback actions.
|
|
30
|
+
In the flat format the universal fallback is `permission["*"]`; per-surface catch-alls are entries in the `permission` object.
|
|
31
|
+
|
|
32
|
+
```jsonc
|
|
33
|
+
// Before
|
|
34
|
+
{
|
|
35
|
+
"defaultPolicy": {
|
|
36
|
+
"tools": "ask",
|
|
37
|
+
"bash": "ask",
|
|
38
|
+
"mcp": "ask",
|
|
39
|
+
"skills": "ask",
|
|
40
|
+
"special": "ask"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// After — all surfaces default to "ask" via the universal fallback
|
|
45
|
+
{
|
|
46
|
+
"permission": {
|
|
47
|
+
"*": "ask"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
If surfaces had **different** defaults, express each one explicitly:
|
|
53
|
+
|
|
54
|
+
```jsonc
|
|
55
|
+
// Before
|
|
56
|
+
{
|
|
57
|
+
"defaultPolicy": {
|
|
58
|
+
"tools": "allow",
|
|
59
|
+
"bash": "ask",
|
|
60
|
+
"mcp": "ask",
|
|
61
|
+
"skills": "ask",
|
|
62
|
+
"special": "deny"
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// After
|
|
67
|
+
{
|
|
68
|
+
"permission": {
|
|
69
|
+
"*": "allow",
|
|
70
|
+
"bash": "ask",
|
|
71
|
+
"mcp": "ask",
|
|
72
|
+
"skill": "ask",
|
|
73
|
+
"external_directory": "deny"
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### `tools`
|
|
79
|
+
|
|
80
|
+
Each entry in `tools` maps a tool name to a permission.
|
|
81
|
+
In the flat format, tool names are surface keys directly inside `permission`.
|
|
82
|
+
|
|
83
|
+
```jsonc
|
|
84
|
+
// Before
|
|
85
|
+
{
|
|
86
|
+
"tools": {
|
|
87
|
+
"read": "allow",
|
|
88
|
+
"write": "deny",
|
|
89
|
+
"edit": "ask"
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// After
|
|
94
|
+
{
|
|
95
|
+
"permission": {
|
|
96
|
+
"read": "allow",
|
|
97
|
+
"write": "deny",
|
|
98
|
+
"edit": "ask"
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
**Special case — `tools.bash` and `tools.mcp`:** These were catch-all overrides for their respective surfaces.
|
|
104
|
+
In the flat format, use a string shorthand or an explicit `"*"` pattern:
|
|
105
|
+
|
|
106
|
+
```jsonc
|
|
107
|
+
// Before
|
|
108
|
+
{
|
|
109
|
+
"tools": { "bash": "allow", "mcp": "deny" }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// After — string shorthand (equivalent to { "*": "allow" })
|
|
113
|
+
{
|
|
114
|
+
"permission": {
|
|
115
|
+
"bash": "allow",
|
|
116
|
+
"mcp": "deny"
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### `bash`
|
|
122
|
+
|
|
123
|
+
Bash patterns translate directly; the surface name stays `bash`.
|
|
124
|
+
If you also had a `tools.bash` or `defaultPolicy.bash` value different from `defaultPolicy.tools`, add an explicit `"*"` catch-all pattern at the **start** of the object (so specific patterns placed after it override it via last-match-wins).
|
|
125
|
+
|
|
126
|
+
```jsonc
|
|
127
|
+
// Before
|
|
128
|
+
{
|
|
129
|
+
"defaultPolicy": { "tools": "allow", "bash": "ask" },
|
|
130
|
+
"bash": {
|
|
131
|
+
"git status": "allow",
|
|
132
|
+
"git diff": "allow",
|
|
133
|
+
"git *": "ask",
|
|
134
|
+
"rm -rf *": "deny"
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// After
|
|
139
|
+
{
|
|
140
|
+
"permission": {
|
|
141
|
+
"*": "allow",
|
|
142
|
+
"bash": {
|
|
143
|
+
"*": "ask",
|
|
144
|
+
"git status": "allow",
|
|
145
|
+
"git diff": "allow",
|
|
146
|
+
"git *": "ask",
|
|
147
|
+
"rm -rf *": "deny"
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
> **Note:** Pattern ordering matters within an object.
|
|
154
|
+
> `normalizeFlatConfig` preserves insertion order, and `evaluate` uses last-match-wins.
|
|
155
|
+
> Put broad catch-alls **first** and specific overrides **after** them.
|
|
156
|
+
|
|
157
|
+
### `mcp`
|
|
158
|
+
|
|
159
|
+
MCP patterns translate directly; the surface name stays `mcp`.
|
|
160
|
+
|
|
161
|
+
```jsonc
|
|
162
|
+
// Before
|
|
163
|
+
{
|
|
164
|
+
"mcp": {
|
|
165
|
+
"mcp_status": "allow",
|
|
166
|
+
"mcp_list": "allow",
|
|
167
|
+
"exa:*": "allow",
|
|
168
|
+
"dangerous-server": "deny"
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// After
|
|
173
|
+
{
|
|
174
|
+
"permission": {
|
|
175
|
+
"mcp": {
|
|
176
|
+
"mcp_status": "allow",
|
|
177
|
+
"mcp_list": "allow",
|
|
178
|
+
"exa:*": "allow",
|
|
179
|
+
"dangerous-server": "deny"
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### `skills`
|
|
186
|
+
|
|
187
|
+
The surface name changes from `skills` (plural) to `skill` (singular).
|
|
188
|
+
|
|
189
|
+
```jsonc
|
|
190
|
+
// Before
|
|
191
|
+
{
|
|
192
|
+
"skills": {
|
|
193
|
+
"*": "ask",
|
|
194
|
+
"librarian": "allow",
|
|
195
|
+
"dangerous-*": "deny"
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// After — note: "skills" → "skill"
|
|
200
|
+
{
|
|
201
|
+
"permission": {
|
|
202
|
+
"skill": {
|
|
203
|
+
"*": "ask",
|
|
204
|
+
"librarian": "allow",
|
|
205
|
+
"dangerous-*": "deny"
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### `special`
|
|
212
|
+
|
|
213
|
+
`special.external_directory` becomes a top-level surface key in `permission`.
|
|
214
|
+
Other deprecated keys (`doom_loop`, `tool_call_limit`) are simply dropped.
|
|
215
|
+
|
|
216
|
+
```jsonc
|
|
217
|
+
// Before
|
|
218
|
+
{
|
|
219
|
+
"special": {
|
|
220
|
+
"external_directory": "ask",
|
|
221
|
+
"doom_loop": "deny",
|
|
222
|
+
"tool_call_limit": "deny"
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// After — doom_loop and tool_call_limit are removed entirely
|
|
227
|
+
{
|
|
228
|
+
"permission": {
|
|
229
|
+
"external_directory": "ask"
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
> **Note:** In the old format, `special.external_directory: "deny"` produced a rule with `matchedPattern: "external_directory"`.
|
|
235
|
+
> In the flat format, the string shorthand produces `pattern: "*"`, so `matchedPattern` is now `"*"` when the explicit rule matches.
|
|
236
|
+
|
|
237
|
+
## Full before/after example
|
|
238
|
+
|
|
239
|
+
```jsonc
|
|
240
|
+
// Before (legacy format)
|
|
241
|
+
{
|
|
242
|
+
"$schema": "...",
|
|
243
|
+
"debugLog": false,
|
|
244
|
+
"permissionReviewLog": true,
|
|
245
|
+
"yoloMode": false,
|
|
246
|
+
"defaultPolicy": {
|
|
247
|
+
"tools": "ask",
|
|
248
|
+
"bash": "ask",
|
|
249
|
+
"mcp": "ask",
|
|
250
|
+
"skills": "ask",
|
|
251
|
+
"special": "ask"
|
|
252
|
+
},
|
|
253
|
+
"tools": {
|
|
254
|
+
"read": "allow",
|
|
255
|
+
"write": "deny"
|
|
256
|
+
},
|
|
257
|
+
"bash": {
|
|
258
|
+
"git status": "allow",
|
|
259
|
+
"git *": "ask"
|
|
260
|
+
},
|
|
261
|
+
"mcp": {
|
|
262
|
+
"mcp_status": "allow"
|
|
263
|
+
},
|
|
264
|
+
"skills": {
|
|
265
|
+
"*": "ask"
|
|
266
|
+
},
|
|
267
|
+
"special": {
|
|
268
|
+
"external_directory": "ask"
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
```jsonc
|
|
274
|
+
// After (flat format)
|
|
275
|
+
{
|
|
276
|
+
"$schema": "...",
|
|
277
|
+
"debugLog": false,
|
|
278
|
+
"permissionReviewLog": true,
|
|
279
|
+
"yoloMode": false,
|
|
280
|
+
"permission": {
|
|
281
|
+
"*": "ask",
|
|
282
|
+
"read": "allow",
|
|
283
|
+
"write": "deny",
|
|
284
|
+
"bash": {
|
|
285
|
+
"git status": "allow",
|
|
286
|
+
"git *": "ask"
|
|
287
|
+
},
|
|
288
|
+
"mcp": {
|
|
289
|
+
"mcp_status": "allow"
|
|
290
|
+
},
|
|
291
|
+
"skill": { "*": "ask" },
|
|
292
|
+
"external_directory": "ask"
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
## Per-agent frontmatter
|
|
298
|
+
|
|
299
|
+
Frontmatter in agent `.md` files uses the same flat shape under the `permission` key.
|
|
300
|
+
|
|
301
|
+
```yaml
|
|
302
|
+
# Before (legacy nested shape)
|
|
303
|
+
---
|
|
304
|
+
permission:
|
|
305
|
+
defaultPolicy:
|
|
306
|
+
tools: allow
|
|
307
|
+
bash:
|
|
308
|
+
"git *": allow
|
|
309
|
+
tools:
|
|
310
|
+
mcp: deny
|
|
311
|
+
mcp:
|
|
312
|
+
exa_web_search_exa: allow
|
|
313
|
+
special:
|
|
314
|
+
external_directory: allow
|
|
315
|
+
---
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
```yaml
|
|
319
|
+
# After (flat shape)
|
|
320
|
+
---
|
|
321
|
+
permission:
|
|
322
|
+
"*": allow
|
|
323
|
+
bash:
|
|
324
|
+
"git *": allow
|
|
325
|
+
mcp:
|
|
326
|
+
"*": deny
|
|
327
|
+
exa_web_search_exa: allow
|
|
328
|
+
external_directory: allow
|
|
329
|
+
---
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
Key differences from the old frontmatter:
|
|
333
|
+
|
|
334
|
+
1. The `"*"` key (quoted in YAML) replaces `defaultPolicy.tools`.
|
|
335
|
+
2. `tools.bash` / `tools.mcp` catch-alls become `bash: <state>` or `mcp: { "*": <state>, ... }`.
|
|
336
|
+
3. `special.external_directory` becomes `external_directory` at the top level of `permission`.
|
|
337
|
+
4. Any surface key now works in frontmatter — extension tool names and `mcp` are no longer silently ignored.
|
|
338
|
+
|
|
339
|
+
## Behavioral differences
|
|
340
|
+
|
|
341
|
+
### Agent scope catch-alls override parent scope patterns
|
|
342
|
+
|
|
343
|
+
In the old format, `tools.bash: allow` (override layer) was lower priority than config-layer patterns from any scope, including global.
|
|
344
|
+
In the flat format, `bash: allow` in an agent scope is a config-layer catch-all with **higher** priority than global-scope patterns (last-match-wins, agent rules come later).
|
|
345
|
+
|
|
346
|
+
If you relied on global `rm -rf *: deny` surviving an agent's `tools.bash: allow`, you must now explicitly deny the pattern within the agent's own `bash` object:
|
|
347
|
+
|
|
348
|
+
```yaml
|
|
349
|
+
# Old agent frontmatter — global "rm -rf *": "deny" survived
|
|
350
|
+
permission:
|
|
351
|
+
tools:
|
|
352
|
+
bash: allow
|
|
353
|
+
|
|
354
|
+
# New agent frontmatter — must repeat the deny if you want it preserved
|
|
355
|
+
permission:
|
|
356
|
+
bash:
|
|
357
|
+
"*": allow
|
|
358
|
+
"rm -rf *": deny
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
### `matchedPattern` for `external_directory`
|
|
362
|
+
|
|
363
|
+
In the old format, an explicit `special.external_directory: "deny"` rule had `matchedPattern: "external_directory"`.
|
|
364
|
+
In the flat format, `external_directory: "deny"` (string shorthand) has `matchedPattern: "*"`.
|
|
365
|
+
Code that inspected `matchedPattern` to detect explicit external-directory config must be updated.
|