Permission revamp docs + new API endpoints

This commit is contained in:
opencode-agent[bot]
2026-01-02 00:33:08 +00:00
parent 7aa1dbe873
commit b1eb4cc44c
5 changed files with 115 additions and 50 deletions

View File

@@ -384,7 +384,7 @@ You can also use wildcards to control multiple tools at once. For example, to di
### Permissions
You can configure permissions to manage what actions an agent can take. Currently, the permissions for the `edit`, `bash`, and `webfetch` tools can be configured to:
You can configure permissions to manage what actions an agent can take. Most built-in tools support permissions (for example, `edit`, `bash`, `read`, `webfetch`, `glob`, `grep`, `task`, etc.), plus special gates like `external_directory` and `doom_loop`.
- `"ask"` — Prompt for approval before running the tool
- `"allow"` — Allow all operations without approval

View File

@@ -11,6 +11,7 @@ By default, OpenCode allows most operations without approval, except `doom_loop`
"permission": {
"edit": "allow",
"bash": "ask",
"read": "allow",
"skill": "ask",
"webfetch": "deny",
"doom_loop": "ask",
@@ -19,7 +20,7 @@ By default, OpenCode allows most operations without approval, except `doom_loop`
}
```
This lets you configure granular controls for the `edit`, `bash`, `skill`, `webfetch`, `doom_loop`, and `external_directory` tools.
This lets you configure granular controls for most built-in tools (and many plugin tools). Note that `write`, `patch`, and `multiedit` are all covered by `permission.edit`.
- `"ask"` — Prompt for approval before running the tool
- `"allow"` — Allow all operations without approval
@@ -29,7 +30,26 @@ This lets you configure granular controls for the `edit`, `bash`, `skill`, `webf
## Tools
Currently, the permissions for the `edit`, `bash`, `skill`, `webfetch`, `doom_loop`, and `external_directory` tools can be configured through the `permission` option.
Any tool that might access your filesystem, run commands, or use the network can be gated behind permissions.
Common permission keys:
- `edit` (also covers `write`, `patch`, `multiedit`)
- `bash`
- `read`
- `list`
- `glob`
- `grep`
- `lsp`
- `task`
- `todowrite` / `todoread`
- `skill`
- `webfetch`
- `websearch` / `codesearch`
- `doom_loop`
- `external_directory`
Each permission key can be either a single action (string), or an object mapping patterns to actions.
---
@@ -135,13 +155,38 @@ The wildcard uses simple regex globbing patterns.
#### Scope of the `"ask"` option
When the agent asks for permission to run a particular bash command, it will
request feedback with the three options "accept", "accept always" and "deny".
The "accept always" answer applies for the rest of the current session.
When the agent asks for permission to run a particular bash command, it will request feedback with three options: `once`, `always`, and `reject`.
In addition, command permissions are applied to the first two elements of a command. So, an "accept always" response for a command like `git log` would whitelist `git log *` but not `git commit ...`.
The `always` option stores a rule for a normalized **command prefix** (for example, `git status*` or `npm run dev*`). The prefix is derived from a built-in command-arity table (flags dont count); for unknown commands it falls back to the first token.
When an agent asks for permission to run a command in a pipeline, we use tree sitter to parse each command in the pipeline. The "accept always" permission thus applies separately to each command in the pipeline.
When an agent asks for permission to run a command in a pipeline, each command is parsed separately, and the saved `always` rule applies per command.
---
### read
Use the `permission.read` key to control whether file reads require approval.
The permission patterns for `read` are absolute file paths.
```json title="opencode.json" {4}
{
"$schema": "https://opencode.ai/config.json",
"permission": {
"read": "ask"
}
}
```
---
### list, glob, grep
You can also gate other "read-only" tools:
- `permission.list` — directory listings (patterns are directory paths)
- `permission.glob` — file globs (patterns are the glob patterns)
- `permission.grep` — content search (patterns are the regex patterns)
---

View File

@@ -170,8 +170,8 @@ Plugins can subscribe to events as seen below in the Examples section. Here is a
#### Permission Events
- `permission.asked`
- `permission.replied`
- `permission.updated`
#### Server Events

View File

@@ -226,27 +226,36 @@ const { providers, default: defaults } = await client.config.providers()
### Sessions
| Method | Description | Notes |
| ---------------------------------------------------------- | ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `session.list()` | List sessions | Returns <a href={typesUrl}><code>Session[]</code></a> |
| `session.get({ path })` | Get session | Returns <a href={typesUrl}><code>Session</code></a> |
| `session.children({ path })` | List child sessions | Returns <a href={typesUrl}><code>Session[]</code></a> |
| `session.create({ body })` | Create session | Returns <a href={typesUrl}><code>Session</code></a> |
| `session.delete({ path })` | Delete session | Returns `boolean` |
| `session.update({ path, body })` | Update session properties | Returns <a href={typesUrl}><code>Session</code></a> |
| `session.init({ path, body })` | Analyze app and create `AGENTS.md` | Returns `boolean` |
| `session.abort({ path })` | Abort a running session | Returns `boolean` |
| `session.share({ path })` | Share session | Returns <a href={typesUrl}><code>Session</code></a> |
| `session.unshare({ path })` | Unshare session | Returns <a href={typesUrl}><code>Session</code></a> |
| `session.summarize({ path, body })` | Summarize session | Returns `boolean` |
| `session.messages({ path })` | List messages in a session | Returns `{ info: `<a href={typesUrl}><code>Message</code></a>`, parts: `<a href={typesUrl}><code>Part[]</code></a>`}[]` |
| `session.message({ path })` | Get message details | Returns `{ info: `<a href={typesUrl}><code>Message</code></a>`, parts: `<a href={typesUrl}><code>Part[]</code></a>`}` |
| `session.prompt({ path, body })` | Send prompt message | `body.noReply: true` returns UserMessage (context only). Default returns <a href={typesUrl}><code>AssistantMessage</code></a> with AI response |
| `session.command({ path, body })` | Send command to session | Returns `{ info: `<a href={typesUrl}><code>AssistantMessage</code></a>`, parts: `<a href={typesUrl}><code>Part[]</code></a>`}` |
| `session.shell({ path, body })` | Run a shell command | Returns <a href={typesUrl}><code>AssistantMessage</code></a> |
| `session.revert({ path, body })` | Revert a message | Returns <a href={typesUrl}><code>Session</code></a> |
| `session.unrevert({ path })` | Restore reverted messages | Returns <a href={typesUrl}><code>Session</code></a> |
| `postSessionByIdPermissionsByPermissionId({ path, body })` | Respond to a permission request | Returns `boolean` |
| Method | Description | Notes |
| ----------------------------------- | ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `session.list()` | List sessions | Returns <a href={typesUrl}><code>Session[]</code></a> |
| `session.get({ path })` | Get session | Returns <a href={typesUrl}><code>Session</code></a> |
| `session.children({ path })` | List child sessions | Returns <a href={typesUrl}><code>Session[]</code></a> |
| `session.create({ body })` | Create session | Returns <a href={typesUrl}><code>Session</code></a> |
| `session.delete({ path })` | Delete session | Returns `boolean` |
| `session.update({ path, body })` | Update session properties | Returns <a href={typesUrl}><code>Session</code></a> |
| `session.init({ path, body })` | Analyze app and create `AGENTS.md` | Returns `boolean` |
| `session.abort({ path })` | Abort a running session | Returns `boolean` |
| `session.share({ path })` | Share session | Returns <a href={typesUrl}><code>Session</code></a> |
| `session.unshare({ path })` | Unshare session | Returns <a href={typesUrl}><code>Session</code></a> |
| `session.summarize({ path, body })` | Summarize session | Returns `boolean` |
| `session.messages({ path })` | List messages in a session | Returns `{ info: `<a href={typesUrl}><code>Message</code></a>`, parts: `<a href={typesUrl}><code>Part[]</code></a>`}[]` |
| `session.message({ path })` | Get message details | Returns `{ info: `<a href={typesUrl}><code>Message</code></a>`, parts: `<a href={typesUrl}><code>Part[]</code></a>`}` |
| `session.prompt({ path, body })` | Send prompt message | `body.noReply: true` returns UserMessage (context only). Default returns <a href={typesUrl}><code>AssistantMessage</code></a> with AI response |
| `session.command({ path, body })` | Send command to session | Returns `{ info: `<a href={typesUrl}><code>AssistantMessage</code></a>`, parts: `<a href={typesUrl}><code>Part[]</code></a>`}` |
| `session.shell({ path, body })` | Run a shell command | Returns <a href={typesUrl}><code>AssistantMessage</code></a> |
| `session.revert({ path, body })` | Revert a message | Returns <a href={typesUrl}><code>Session</code></a> |
| `session.unrevert({ path })` | Restore reverted messages | Returns <a href={typesUrl}><code>Session</code></a> |
---
### Permissions
| Method | Description | Notes |
| ----------------------------------------------------------- | ------------------------------- | ------------------------------------------------------------------ |
| `permission.list()` | List pending permissions | Returns <a href={typesUrl}><code>PermissionRequest[]</code></a> |
| `permission.reply({ requestID, reply })` | Respond to a permission request | `reply` is `"once"`, `"always"`, or `"reject"` (returns `boolean`) |
| `permission.respond({ sessionID, permissionID, response })` | Respond to a permission request | Deprecated alias (returns `boolean`) |
---

View File

@@ -134,26 +134,37 @@ The opencode server exposes the following APIs.
### Sessions
| Method | Path | Description | Notes |
| -------- | ---------------------------------------- | ------------------------------------- | ---------------------------------------------------------------------------------- |
| `GET` | `/session` | List all sessions | Returns <a href={typesUrl}><code>Session[]</code></a> |
| `POST` | `/session` | Create a new session | body: `{ parentID?, title? }`, returns <a href={typesUrl}><code>Session</code></a> |
| `GET` | `/session/status` | Get session status for all sessions | Returns `{ [sessionID: string]: `<a href={typesUrl}>SessionStatus</a>` }` |
| `GET` | `/session/:id` | Get session details | Returns <a href={typesUrl}><code>Session</code></a> |
| `DELETE` | `/session/:id` | Delete a session and all its data | Returns `boolean` |
| `PATCH` | `/session/:id` | Update session properties | body: `{ title? }`, returns <a href={typesUrl}><code>Session</code></a> |
| `GET` | `/session/:id/children` | Get a session's child sessions | Returns <a href={typesUrl}><code>Session[]</code></a> |
| `GET` | `/session/:id/todo` | Get the todo list for a session | Returns <a href={typesUrl}><code>Todo[]</code></a> |
| `POST` | `/session/:id/init` | Analyze app and create `AGENTS.md` | body: `{ messageID, providerID, modelID }`, returns `boolean` |
| `POST` | `/session/:id/fork` | Fork an existing session at a message | body: `{ messageID? }`, returns <a href={typesUrl}><code>Session</code></a> |
| `POST` | `/session/:id/abort` | Abort a running session | Returns `boolean` |
| `POST` | `/session/:id/share` | Share a session | Returns <a href={typesUrl}><code>Session</code></a> |
| `DELETE` | `/session/:id/share` | Unshare a session | Returns <a href={typesUrl}><code>Session</code></a> |
| `GET` | `/session/:id/diff` | Get the diff for this session | query: `messageID?`, returns <a href={typesUrl}><code>FileDiff[]</code></a> |
| `POST` | `/session/:id/summarize` | Summarize the session | body: `{ providerID, modelID }`, returns `boolean` |
| `POST` | `/session/:id/revert` | Revert a message | body: `{ messageID, partID? }`, returns `boolean` |
| `POST` | `/session/:id/unrevert` | Restore all reverted messages | Returns `boolean` |
| `POST` | `/session/:id/permissions/:permissionID` | Respond to a permission request | body: `{ response, remember? }`, returns `boolean` |
| Method | Path | Description | Notes |
| -------- | ------------------------ | ------------------------------------- | ---------------------------------------------------------------------------------- |
| `GET` | `/session` | List all sessions | Returns <a href={typesUrl}><code>Session[]</code></a> |
| `POST` | `/session` | Create a new session | body: `{ parentID?, title? }`, returns <a href={typesUrl}><code>Session</code></a> |
| `GET` | `/session/status` | Get session status for all sessions | Returns `{ [sessionID: string]: `<a href={typesUrl}>SessionStatus</a>` }` |
| `GET` | `/session/:id` | Get session details | Returns <a href={typesUrl}><code>Session</code></a> |
| `DELETE` | `/session/:id` | Delete a session and all its data | Returns `boolean` |
| `PATCH` | `/session/:id` | Update session properties | body: `{ title? }`, returns <a href={typesUrl}><code>Session</code></a> |
| `GET` | `/session/:id/children` | Get a session's child sessions | Returns <a href={typesUrl}><code>Session[]</code></a> |
| `GET` | `/session/:id/todo` | Get the todo list for a session | Returns <a href={typesUrl}><code>Todo[]</code></a> |
| `POST` | `/session/:id/init` | Analyze app and create `AGENTS.md` | body: `{ messageID, providerID, modelID }`, returns `boolean` |
| `POST` | `/session/:id/fork` | Fork an existing session at a message | body: `{ messageID? }`, returns <a href={typesUrl}><code>Session</code></a> |
| `POST` | `/session/:id/abort` | Abort a running session | Returns `boolean` |
| `POST` | `/session/:id/share` | Share a session | Returns <a href={typesUrl}><code>Session</code></a> |
| `DELETE` | `/session/:id/share` | Unshare a session | Returns <a href={typesUrl}><code>Session</code></a> |
| `GET` | `/session/:id/diff` | Get the diff for this session | query: `messageID?`, returns <a href={typesUrl}><code>FileDiff[]</code></a> |
| `POST` | `/session/:id/summarize` | Summarize the session | body: `{ providerID, modelID }`, returns `boolean` |
| `POST` | `/session/:id/revert` | Revert a message | body: `{ messageID, partID? }`, returns `boolean` |
| `POST` | `/session/:id/unrevert` | Restore all reverted messages | Returns `boolean` |
---
### Permissions
| Method | Path | Description | Notes |
| ------ | ---------------------------------------- | -------------------------------------------- | --------------------------------------------------------------- |
| `GET` | `/permission` | List pending permissions | Returns <a href={typesUrl}><code>PermissionRequest[]</code></a> |
| `POST` | `/permission/:requestID/reply` | Respond to a permission request | body: `{ reply }`, returns `boolean` |
| `POST` | `/session/:id/permissions/:permissionID` | Respond to a permission request (deprecated) | body: `{ response }`, returns `boolean` |
`reply` / `response` can be one of: `"once"`, `"always"`, or `"reject"`.
---