feat: vendor permission system source

This commit is contained in:
云服务部-叶林立
2026-08-19 14:35:19 +08:00
parent 198584daf8
commit 410c50a3e5
809 changed files with 157793 additions and 139 deletions
@@ -0,0 +1,169 @@
# Permission Frontmatter for Subagent Extensions
A convention guide for pi-subagent extension authors who want to offer users richer per-agent permission control.
## Motivation
Pi subagent extensions already let users restrict **which tools** an agent can see via frontmatter keys like `tools:`, `disallowed_tools:`, or `deny-tools:`.
These binary allow/deny mechanisms are simple and effective for tool visibility.
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).
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.
## The Two-Layer Model
```text
┌──────────────────────────────────────────────────────┐
│ Layer 1 Visibility (your extension) │
│ Controls which tools are registered / active │
│ before the agent session starts. │
├──────────────────────────────────────────────────────┤
│ Layer 2 Policy (pi-permission-system) │
│ Controls allow / ask / deny decisions on every │
│ tool call, bash command, MCP operation, etc. │
└──────────────────────────────────────────────────────┘
```
The two layers compose additively:
1. A tool hidden by your extension is never seen by the permission system — policy for it is irrelevant.
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.
3. Both denylist mechanisms are additive.
A tool blocked by either layer stays blocked.
## The `permission:` Frontmatter Format
The `permission:` key uses a flat policy map.
Each top-level key is either a tool name (for per-tool policy) or a named surface (`bash`, `mcp`, `skill`, `external_directory`, `special`).
The special key `"*"` is the universal fallback.
### Minimal example
```yaml
---
permission:
"*": ask
read: allow
write: deny
---
```
This means: allow all read operations without prompting, deny all write operations, and ask the user for everything else.
### Full example with bash patterns
```yaml
---
permission:
"*": ask
read: allow
bash:
"*": ask
"git status": allow
"git diff *": allow
"npm test": allow
mcp:
"*": deny
skill:
"*": ask
external_directory:
"*": deny
"~/projects/*": allow
---
```
### Composing with your extension's keys
Users can freely combine `permission:` with your extension's tool restriction key:
```yaml
---
# nicobailon/pi-subagents: restrict visible tools
tools: bash,read_file,write_file
# pi-permission-system: policy within the visible set
permission:
"*": ask
read_file: allow
bash:
"*": ask
"git *": allow
---
```
Both keys are read independently by their respective extensions.
There is no key collision — `permission:` is exclusively consumed by `pi-permission-system`.
## Permission Surfaces
| Surface | Key | Value format | Description |
| -------------------- | -------------------- | ---------------------------- | ------------------------------------------ |
| Tools | `<tool_name>` | `"allow" \| "ask" \| "deny"` | Per-tool invocation policy |
| Bash | `bash` | `{ pattern: decision }` | Pattern-matched bash commands (glob-style) |
| MCP | `mcp` | `{ pattern: decision }` | MCP tool-level policy |
| Skills | `skill` | `{ pattern: decision }` | Skill invocation policy |
| External directories | `external_directory` | `{ pattern: decision }` | Path-based access outside the project |
| Special | `special` | `{ pattern: decision }` | Special operations (e.g. `subagent_spawn`) |
| Universal fallback | `"*"` | `"allow" \| "ask" \| "deny"` | Applies when no specific rule matches |
Pattern maps use last-match-wins ordering: put broad catch-alls first and specific overrides after.
## What Adoption Looks Like
Adopting this convention does **not** require your extension to:
- Import or depend on `pi-permission-system`
- Evaluate the `permission:` key at runtime
- Change your existing tool restriction mechanism
Adoption means:
1. **Document** the `permission:` key as an optional frontmatter field in your extension's README or agent authoring guide.
2. **Explain** that it is consumed by `pi-permission-system` when both extensions are installed.
3. **Show** a combined example with your extension's key alongside `permission:`.
The permission system handles all evaluation, prompt dialogs, and policy enforcement independently.
## Runtime Integration (Optional)
If your extension runs subagents in-process (e.g. via `createAgentSession()`), you can optionally query the permission system's policy at runtime via the `Symbol.for()`-backed service accessor — no required peer dependency, just a dynamic `import()`.
### Querying policy
```typescript
try {
const { getPermissionsService } = await import(
"@gotgenes/pi-permission-system"
);
const permissions = getPermissionsService();
if (permissions) {
const result = permissions.checkPermission("bash", "git push", "Worker");
console.log(result.state); // "allow" | "deny" | "ask"
}
} catch {
// Not installed — graceful degradation
}
```
If `pi-permission-system` is not installed, `import()` throws; if it has not published a service yet (or has been unloaded), `getPermissionsService()` returns `undefined`.
Guard both cases as shown above.
Prompt forwarding for headless child agents is an internal subagent-to-parent mechanism, not a public cross-extension operation — there is no service-accessor equivalent to call directly.
For full API documentation, see [Cross-extension API](../cross-extension-api.md).
## Benefits for Your Users
1. **Richer semantics**`ask` is more useful than binary allow/deny; users can permit a tool but require approval for each invocation.
2. **Unified config** — one `permission:` block per agent instead of separate restriction keys in multiple extensions.
3. **Surface coverage** — policy covers bash patterns, MCP tools, skills, external directories, and special operations, not just tool names.
4. **Forwarding** — permission prompts from headless child agents surface in the parent session's UI.
5. **Programmatic access** — the `Symbol.for()` service accessor lets your extension query policy at runtime with only a dynamic `import()`, no required peer dependency.
## Further Reading
- [Subagent Integration](../subagent-integration.md) — full coexistence documentation and interaction rules
- [Cross-extension API](../cross-extension-api.md) — service accessor, event bus reference (decision and UI-prompt broadcasts)
- [Configuration](../configuration.md) — full policy reference including merge precedence
- [Schema](../../schemas/permissions.schema.json) — canonical JSON Schema for the flat permission format
@@ -0,0 +1,113 @@
# Upstream Issue Template
Template text for proposing the `permission:` frontmatter convention to subagent extension repositories.
Customize the placeholders (`{{...}}`) for each target repo.
---
## Issue Title
> Proposal: document `permission:` frontmatter for per-agent permission policy
## Issue Body
````markdown
## Context
Users of both `{{your-extension}}` and [`pi-permission-system`](https://github.com/gotgenes/pi-permission-system) currently configure tool restrictions in two places:
1. **Tool visibility** via `{{your-key}}` in agent frontmatter (consumed by your extension)
2. **Permission policy** via `permission:` in the same frontmatter (consumed by pi-permission-system)
These two layers compose correctly today — there is no conflict — but users may not realize they can combine them in the same agent file.
## Proposal
Document the `permission:` key as an optional companion to `{{your-key}}` in your agent authoring docs.
This is purely a documentation change — no code dependency on pi-permission-system is needed.
### What `permission:` provides
- **Three-state policy**: `allow`, `ask` (prompt the user), or `deny` — richer than binary allow/deny
- **Multiple surfaces**: tools, bash commands (glob patterns), MCP operations, skills, external directories
- **Prompt forwarding**: `ask` decisions in headless child agents surface in the parent session's UI
- **Service accessor API**: other extensions can query policy at runtime via a `Symbol.for()`-backed accessor, with only a dynamic `import()` and no required peer dependency
### Example
```yaml
---
# {{your-extension}}: restrict visible tools
{{your-key}}: {{example-value}}
# pi-permission-system (optional): policy within the visible set
permission:
"*": ask
read_file: allow
bash:
"*": ask
"git *": allow
---
```
### Two-layer model
```text
Layer 1 Visibility ({{your-extension}})
→ Controls which tools are registered before the session starts
Layer 2 Policy (pi-permission-system)
→ Controls allow/ask/deny decisions on every tool call, bash command, etc.
```
A tool hidden by Layer 1 is never evaluated by Layer 2.
A tool denied by Layer 2 cannot be restored by Layer 1.
Both mechanisms are additive — a tool blocked by either stays blocked.
### What adoption requires from you
1. Add a section to your README noting that `permission:` is an optional key consumed by pi-permission-system
2. Show a combined example with `{{your-key}}` + `permission:`
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
No code changes, no new dependency, no schema enforcement.
## References
- [Convention guide](https://github.com/gotgenes/pi-permission-system/blob/main/docs/guides/permission-frontmatter-for-subagent-extensions.md)
- [Subagent integration docs](https://github.com/gotgenes/pi-permission-system/blob/main/docs/subagent-integration.md)
- [Cross-extension API docs](https://github.com/gotgenes/pi-permission-system/blob/main/docs/cross-extension-api.md)
````
---
## Per-Repository Customization
### nicobailon/pi-subagents
| Placeholder | Value |
| -------------------- | --------------------------- |
| `{{your-extension}}` | `pi-subagents` |
| `{{your-key}}` | `tools` |
| `{{example-value}}` | `bash,read_file,write_file` |
### tintinweb/pi-subagents
| Placeholder | Value |
| -------------------- | ------------------ |
| `{{your-extension}}` | `pi-subagents` |
| `{{your-key}}` | `disallowed_tools` |
| `{{example-value}}` | `write_file,bash` |
Additional note for tintinweb: since this extension runs subagents in-process via `createAgentSession()`, mention the [service accessor](https://github.com/gotgenes/pi-permission-system/blob/main/docs/cross-extension-api.md#service-accessor) as an optional runtime integration path for querying policy without spawning a subprocess.
### HazAT/pi-interactive-subagents
| Placeholder | Value |
| -------------------- | -------------------------- |
| `{{your-extension}}` | `pi-interactive-subagents` |
| `{{your-key}}` | `deny-tools` |
| `{{example-value}}` | `write_file,bash` |
Additional note for HazAT: this extension already uses `PI_DENY_TOOLS` env var for subprocess tool denial.
The `permission:` frontmatter provides the same effect via `tool_name: deny` but adds `ask` as an intermediate option and covers surfaces beyond tools.