@mortz.prk/opencode-auto-mode 1.0.2 → 1.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/CONTRIBUTING.md +234 -0
- package/README.llm.md +314 -0
- package/README.md +174 -295
- package/dist/gui-experimental.cjs +1 -1
- package/package.json +9 -8
- package/src/audit.ts +130 -49
- package/src/classifier.ts +44 -47
- package/src/code-mode.ts +55 -0
- package/src/config.ts +102 -105
- package/src/debug-events.ts +223 -0
- package/src/decision.ts +72 -0
- package/src/gates.ts +16 -0
- package/src/global-options.ts +85 -0
- package/src/gui-state.ts +230 -121
- package/src/gui.tsx +56 -107
- package/src/index.ts +482 -373
- package/src/mode.ts +237 -0
- package/src/reviewer.ts +70 -94
- package/src/rpc.ts +48 -38
- package/src/score/files.ts +49 -0
- package/src/score/history.ts +141 -0
- package/src/score/index.ts +37 -0
- package/src/score/limits.ts +24 -0
- package/src/score/shell-argv.ts +74 -0
- package/src/score/shell-baseline.ts +113 -0
- package/src/score/shell-safety.ts +54 -0
- package/src/score/shell.ts +30 -0
- package/src/score/types.ts +14 -0
- package/src/score.ts +9 -0
- package/src/shell-policy.ts +672 -30
- package/src/shell.ts +272 -52
- package/src/status.ts +49 -36
- package/src/tiers.ts +285 -19
- package/src/tui.tsx +44 -34
- package/src/user-files.ts +75 -0
- package/src/worktree.ts +30 -0
- package/src/decisions.ts +0 -236
- package/src/logger.ts +0 -26
- package/src/otlp.ts +0 -77
- package/src/policy.ts +0 -36
- package/src/state.ts +0 -135
- package/src/storage.ts +0 -26
- package/src/traces.ts +0 -90
- package/src/transcript.ts +0 -204
- package/src/tui-state.ts +0 -13
package/README.md
CHANGED
|
@@ -1,336 +1,215 @@
|
|
|
1
1
|
# OpenCode Auto Mode
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
Automatic review of OpenCode permission requests. While a session's auto mode is
|
|
4
|
+
on, the plugin reviews native **allow** and **ask** effects, including saved
|
|
5
|
+
**Always** approvals. A native **deny** is always final.
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
Auto Mode reduces prompts. It is not a security sandbox, and it cannot make an
|
|
8
|
+
invalid configuration fail closed.
|
|
7
9
|
|
|
8
|
-
|
|
9
|
-
action and reviews those requests using built-in checks. No model or API key is
|
|
10
|
-
needed:
|
|
10
|
+
## Requirements
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
"package": "@mortz.prk/opencode-auto-mode",
|
|
21
|
-
"options": {
|
|
22
|
-
"classifiers": [
|
|
23
|
-
{ "type": "static" }
|
|
24
|
-
]
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
]
|
|
28
|
-
}
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
OpenCode watches configuration files and reloads plugins after a save, so no
|
|
32
|
-
restart is needed. If a change does not take effect, run `/reload` in the TUI to
|
|
33
|
-
force a full reload.
|
|
34
|
-
|
|
35
|
-
There is no default review pipeline, and empty options are invalid. Add at least
|
|
36
|
-
one review entry. Built-in checks deny dangerous shell commands, allow common
|
|
37
|
-
read-only commands, and send other parsed commands to the configured reviewers.
|
|
38
|
-
Dynamic syntax such as command substitution, nested shells, and stateful builtins
|
|
39
|
-
still requires manual approval.
|
|
40
|
-
|
|
41
|
-
The snippets below change parts of that configuration; keep your `classifiers`
|
|
42
|
-
list when adding other options.
|
|
43
|
-
|
|
44
|
-
### How a shell command is classified
|
|
12
|
+
- OpenCode V2. The checked-in plugin SDK and schema targets OpenCode `2.2.0`.
|
|
13
|
+
- Model references only if you use `decision` or `llm` classifier stages. Models
|
|
14
|
+
and provider credentials belong in your personal OpenCode provider config.
|
|
15
|
+
This repository supplies no model, endpoint, credential, or allowlist.
|
|
16
|
+
- **Data egress:** a configured `decision` or `llm` stage sends the action and
|
|
17
|
+
resources (including full Code Mode source), agent, project/worktree, git
|
|
18
|
+
state, and up to 12 KB of user messages to that model's provider, even with
|
|
19
|
+
audit off. Choose a provider you trust.
|
|
45
20
|
|
|
46
|
-
|
|
47
|
-
dangerous word — a commit message, a search pattern, a heredoc body — is not
|
|
48
|
-
treated as a dangerous operation. Denials apply to the parsed executable and its
|
|
49
|
-
arguments, and nested shells are inspected recursively.
|
|
21
|
+
## Install
|
|
50
22
|
|
|
51
|
-
|
|
52
|
-
write files or run programs (`rg --pre`, `sort -o`, `tree -o`, `file -C`,
|
|
53
|
-
`bat --pager`, `hostname NAME`, `uniq IN OUT`) are handed to the reviewers
|
|
54
|
-
instead of being auto-approved.
|
|
23
|
+
Register the plugin **once** in your user-owned global config:
|
|
55
24
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
the
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
model reviewer may allow it. With no reviewer configured, it stays `ask`.
|
|
62
|
-
|
|
63
|
-
## Add a typed decision model
|
|
25
|
+
`~/.config/opencode/opencode.jsonc` (or
|
|
26
|
+
`$XDG_CONFIG_HOME/opencode/opencode.jsonc`). The global config must be outside
|
|
27
|
+
the repository, resolve with no symlink in its path, be user-owned and not
|
|
28
|
+
group/other-writable, be a single hardlink, and use an absolute
|
|
29
|
+
`XDG_CONFIG_HOME` when set.
|
|
64
30
|
|
|
65
31
|
```jsonc
|
|
66
32
|
{
|
|
67
|
-
"
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
| Provider | Setup |
|
|
77
|
-
| --- | --- |
|
|
78
|
-
| `typesafe-ai` | Set `TYPESAFE_API_KEY`. |
|
|
79
|
-
| `openai` | Set `OPENAI_API_KEY`. |
|
|
80
|
-
| `laya` | Run a loopback `/v1/systemone` service. `LAYA_API_KEY` is optional. |
|
|
81
|
-
|
|
82
|
-
Decision entries accept `minConfidence` (default `0.9`), `timeoutMs`, and
|
|
83
|
-
`apiKeyEnv`. `laya` also accepts `endpoint`.
|
|
84
|
-
|
|
85
|
-
## Add an LLM
|
|
86
|
-
|
|
87
|
-
Add an `llm` entry after the built-in checks:
|
|
88
|
-
|
|
89
|
-
```jsonc
|
|
90
|
-
{
|
|
91
|
-
"options": {
|
|
92
|
-
"classifiers": [
|
|
93
|
-
{ "type": "static" },
|
|
94
|
-
{ "type": "llm" }
|
|
95
|
-
]
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
When an `llm` entry has no `model`, the **session model** is used. To choose a
|
|
101
|
-
different model:
|
|
102
|
-
|
|
103
|
-
```jsonc
|
|
104
|
-
{ "type": "llm", "model": "anthropic/claude-sonnet-4-5" }
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
## Combine reviewers
|
|
108
|
-
|
|
109
|
-
Entries run in order. Add as many as you need:
|
|
110
|
-
|
|
111
|
-
```jsonc
|
|
112
|
-
{
|
|
113
|
-
"options": {
|
|
114
|
-
"classifiers": [
|
|
115
|
-
{ "type": "static" },
|
|
116
|
-
{ "type": "decision", "model": "typesafe-ai/jev-latest" },
|
|
117
|
-
{ "type": "llm" }
|
|
118
|
-
]
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
An entry can allow, deny, or ask, which stops the pipeline. An entry that abstains
|
|
124
|
-
hands the action to the next entry. If nothing decides, the action remains `ask`.
|
|
125
|
-
|
|
126
|
-
## Time limit and failures
|
|
127
|
-
|
|
128
|
-
A review never blocks a permission prompt indefinitely. One deadline covers every
|
|
129
|
-
entry in the pipeline, including both model calls of an `llm` entry:
|
|
130
|
-
|
|
131
|
-
```jsonc
|
|
132
|
-
{ "options": { "reviewTimeoutMs": 60000 } }
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
`reviewTimeoutMs` accepts 1000–120000 and defaults to 60000. When the deadline
|
|
136
|
-
passes, provider and context requests are aborted and the action is asked with
|
|
137
|
-
`Auto-mode review deadline exceeded`. Interrupting a session also abandons its
|
|
138
|
-
review, so a stopped turn cannot later receive an automatic approval.
|
|
139
|
-
|
|
140
|
-
Failures are reported with a bounded cause — `deadline`, `timeout`, `cancelled`,
|
|
141
|
-
`provider`, `invalid-output`, or `context` — in the audit log and in OTLP
|
|
142
|
-
(`error.kind`), while the user-facing message stays generic.
|
|
143
|
-
|
|
144
|
-
A typed decision reviewer that is unavailable or returns a malformed response
|
|
145
|
-
abstains, so the next entry runs. An `llm` entry behaves differently: a timeout
|
|
146
|
-
or provider error abstains, but an unparseable final answer is an `ask` (manual
|
|
147
|
-
approval) with `invalid-output`, and the pipeline stops there.
|
|
148
|
-
|
|
149
|
-
## Use it for specific agents
|
|
150
|
-
|
|
151
|
-
OpenCode allows everything by default. To review only the `build` agent, set its
|
|
152
|
-
permissions and leave global permissions unchanged:
|
|
153
|
-
|
|
154
|
-
```jsonc
|
|
155
|
-
{
|
|
156
|
-
"agents": {
|
|
157
|
-
"build": {
|
|
158
|
-
"permissions": [
|
|
159
|
-
{ "action": "*", "resource": "*", "effect": "ask" }
|
|
33
|
+
"$schema": "https://opencode.ai/config.json",
|
|
34
|
+
"plugins": [{
|
|
35
|
+
"package": "@mortz.prk/opencode-auto-mode",
|
|
36
|
+
"options": {
|
|
37
|
+
"classifiers": [
|
|
38
|
+
{ "type": "static" },
|
|
39
|
+
{ "type": "llm", "model": "<provider>/<reviewer-model>" }
|
|
160
40
|
]
|
|
161
41
|
}
|
|
162
|
-
}
|
|
42
|
+
}]
|
|
163
43
|
}
|
|
164
44
|
```
|
|
165
45
|
|
|
166
|
-
|
|
46
|
+
The `static` stage handles low-risk reads and eligible in-project edits without a
|
|
47
|
+
model. Remove the `llm` stage if you want a static-only setup; anything static
|
|
48
|
+
cannot resolve then asks you.
|
|
167
49
|
|
|
168
|
-
|
|
169
|
-
It is enabled by default. Set `tui: false` in plugin options to hide it.
|
|
170
|
-
Invalid options show `auto-mode: invalid config` in the same place and raise an
|
|
171
|
-
error toast when the configuration is loaded. OpenCode's own permission dialog
|
|
172
|
-
handles manual approvals.
|
|
50
|
+
For native typed decisions, add a `decision` stage between `static` and `llm`:
|
|
173
51
|
|
|
174
|
-
|
|
175
|
-
|
|
52
|
+
```jsonc
|
|
53
|
+
{ "type": "decision", "model": "<provider>/<decision-model>", "minConfidence": 0.9 }
|
|
54
|
+
```
|
|
176
55
|
|
|
177
|
-
|
|
56
|
+
Only add this stage if your OpenCode provider supports native decisions. The
|
|
57
|
+
plugin has no HTTP decision adapter or custom endpoint option.
|
|
178
58
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
59
|
+
Keep the plugin registration in global config only. Do not add the full plugin to
|
|
60
|
+
project config, `cli.json`, or `.opencode/plugins/` as well: the plugin reads its
|
|
61
|
+
safety options from exactly one global registration. The server registration
|
|
62
|
+
loads the TUI part automatically. Desktop loads the experimental GUI part from
|
|
63
|
+
global config and needs the built `dist/gui-experimental.cjs` bundle; in a local
|
|
64
|
+
checkout, run `bun run build:gui` first.
|
|
183
65
|
|
|
184
|
-
|
|
185
|
-
part, so disabling the GUI never disables automatic review.
|
|
66
|
+
## Verify
|
|
186
67
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
reviews do not appear in the panel.
|
|
68
|
+
Restart nothing for a config-only change. OpenCode watches config files and
|
|
69
|
+
reloads plugins after a save; `/reload` forces a full reload if needed.
|
|
190
70
|
|
|
191
|
-
|
|
192
|
-
your TUI configuration lives in `cli.json`, add the package to the global file as
|
|
193
|
-
well:
|
|
71
|
+
Run:
|
|
194
72
|
|
|
195
|
-
```
|
|
196
|
-
|
|
197
|
-
"plugins": [{ "package": "@mortz.prk/opencode-auto-mode", "options": { "classifiers": [{ "type": "static" }] } }]
|
|
198
|
-
}
|
|
73
|
+
```text
|
|
74
|
+
/auto-mode status
|
|
199
75
|
```
|
|
200
76
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
77
|
+
A working install reports `Auto mode: on`, today's allow/ask/deny counts, active
|
|
78
|
+
reviews, and breaker state. With invalid configuration, the command reports
|
|
79
|
+
`Auto mode: invalid` and `Review disabled: <error>`; the desktop pill reads
|
|
80
|
+
`Invalid config — review disabled`. A storage or status failure is not a config
|
|
81
|
+
error: the command appends its error and the pill reads `Review unavailable` or
|
|
82
|
+
`status unavailable`. Fix the strict option shapes below rather than expecting a
|
|
83
|
+
migration hint.
|
|
204
84
|
|
|
205
|
-
##
|
|
85
|
+
## Use
|
|
206
86
|
|
|
207
|
-
|
|
87
|
+
- `/auto-mode` toggles the current session.
|
|
88
|
+
- `/auto-mode on` enables review and resets the breaker.
|
|
89
|
+
- `/auto-mode off` restores the native host effect.
|
|
90
|
+
- `/auto-mode status` reports mode, counts, active reviews, and breaker state.
|
|
208
91
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
"classifiers": [
|
|
213
|
-
{
|
|
214
|
-
"type": "llm",
|
|
215
|
-
"policy": {
|
|
216
|
-
"trusted": ["the repository and its CI configuration"],
|
|
217
|
-
"blocks": ["publish packages", "change production infrastructure"],
|
|
218
|
-
"exceptions": ["run the repository's test and lint scripts"]
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
]
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
```
|
|
92
|
+
Configure native permissions for ordinary safe work and leave residual risky
|
|
93
|
+
operations as `ask`. Keep hard denials in user-owned global `policies`
|
|
94
|
+
(`tool.use`); the plugin does not install or rewrite native rules.
|
|
225
95
|
|
|
226
|
-
|
|
227
|
-
`exceptions` describes narrow acceptable cases. Omitted lists use the built-in
|
|
228
|
-
policy; an empty list clears that list. Policies are independent between
|
|
229
|
-
reviewers and do not apply to static checks or replace OpenCode permissions.
|
|
96
|
+
## Options
|
|
230
97
|
|
|
231
|
-
|
|
98
|
+
Unknown keys are rejected. `classifiers` is required and must contain 1–64
|
|
99
|
+
ordered stages, with at most one `static` stage.
|
|
232
100
|
|
|
233
|
-
| Option | Default |
|
|
101
|
+
| Option | Default | Contract |
|
|
234
102
|
| --- | --- | --- |
|
|
235
|
-
| `classifiers` |
|
|
236
|
-
| `reviewTimeoutMs` | `
|
|
237
|
-
| `
|
|
238
|
-
| `
|
|
239
|
-
| `
|
|
240
|
-
|
|
241
|
-
Static entries accept `allowInProjectEdits`. Decision entries accept
|
|
242
|
-
`minConfidence`, `timeoutMs`, `apiKeyEnv`, and `endpoint` (loopback `laya` only;
|
|
243
|
-
defaults to `http://127.0.0.1:8000/v1/systemone`). OpenCode reloads the plugin
|
|
244
|
-
when you save, so option changes apply without a restart; run `/reload` in the
|
|
245
|
-
TUI if a change does not take effect. Invalid options disable automatic approval
|
|
246
|
-
and are reported in the TUI and over the status RPC.
|
|
247
|
-
|
|
248
|
-
## Data that leaves the machine
|
|
249
|
-
|
|
250
|
-
Reviewers see the proposed action, its resources, the session directory, the
|
|
251
|
-
requesting agent, the pending permission `metadata` and `source`, and a bounded
|
|
252
|
-
transcript of recent user messages and tool inputs (12 KB, with the head and tail
|
|
253
|
-
of each tool input). Nothing else about the session is sent.
|
|
254
|
-
|
|
255
|
-
* A model reviewer receives that material through OpenCode's own session model or
|
|
256
|
-
the `model` you configure.
|
|
257
|
-
* A typed decision reviewer receives it at the configured provider endpoint.
|
|
258
|
-
* Audit records and OTLP log records keep the action, agent, up to 16 resources,
|
|
259
|
-
a short reason, and, on failure, a short error detail. Both pass the record
|
|
260
|
-
through one sanitizer, which bounds these fields — 128 characters for the
|
|
261
|
-
action and agent, 64 for the stage, 512 for each resource, reason, and error
|
|
262
|
-
detail — but does **not** redact text.
|
|
263
|
-
* OTLP exports logs and traces as separate streams (`/v1/logs` and `/v1/traces`).
|
|
264
|
-
Trace root attributes use the same sanitizer, so their action, agent, and stage
|
|
265
|
-
are bounded like the log fields. Each reviewer attempt adds a child span with
|
|
266
|
-
its model and outcome; the `model` attribute is not clipped. Traces do not
|
|
267
|
-
include resources, reasons, or error-detail text.
|
|
268
|
-
* Text in every stream is unredacted, including the action and agent. Traces
|
|
269
|
-
carry the action and agent; logs carry those plus resources, reasons, and
|
|
270
|
-
error details. These fields may be sensitive, and resources or reasons can
|
|
271
|
-
contain inline file contents or shell commands, including credentials such as
|
|
272
|
-
an `Authorization` header. Audit JSONL keeps the same unredacted fields as OTLP
|
|
273
|
-
logs. Enable `auditLog` or `otlp` only if you are comfortable storing or
|
|
274
|
-
exporting that material.
|
|
275
|
-
* Records do not include the reviewer prompt, the session transcript, or API keys
|
|
276
|
-
configured for providers.
|
|
277
|
-
|
|
278
|
-
## Platform limits
|
|
279
|
-
|
|
280
|
-
* Windows always asks for shell actions; the guard only parses POSIX syntax.
|
|
281
|
-
* Reviews happen inside the OpenCode server, so a headless or remote TUI behaves
|
|
282
|
-
the same as a local one. The desktop GUI needs OpenCode 2.2.0+.
|
|
283
|
-
|
|
284
|
-
## Auditing
|
|
285
|
-
|
|
286
|
-
Enable the audit log in plugin options:
|
|
287
|
-
|
|
288
|
-
```jsonc
|
|
289
|
-
{
|
|
290
|
-
"options": {
|
|
291
|
-
"auditLog": true
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
```
|
|
295
|
-
|
|
296
|
-
Decisions are appended as JSON lines to
|
|
297
|
-
`$XDG_STATE_HOME/opencode-auto-mode/decisions.jsonl`. Replay recorded tool calls
|
|
298
|
-
without executing anything (run from a checkout; it reads the OpenCode database
|
|
299
|
-
read-only and needs `--config` if your global config is not at a default path):
|
|
103
|
+
| `classifiers` | required | Ordered `static`, `decision`, and `llm` stages. No default list and no unconfigured mode. |
|
|
104
|
+
| `reviewTimeoutMs` | `15000` | Whole-review deadline; integer 1–120000. |
|
|
105
|
+
| `breaker` | `3 / 10 / 50` | `{consecutiveDenies, windowDenies, windowSize}`. All three keys are required when supplied. |
|
|
106
|
+
| `tui` | `true` | Show or hide the TUI indicator. UI-only; review still runs server-side. |
|
|
107
|
+
| `audit` | `{enabled:false,maxBytes:10485760}` | Object only. Local audit is off by default. |
|
|
300
108
|
|
|
301
|
-
|
|
302
|
-
bun scripts/replay.ts [--days 14] [--session ID] [--agent build|any] [--json]
|
|
303
|
-
```
|
|
304
|
-
|
|
305
|
-
To send decisions through OpenCode's OTLP telemetry, configure OTLP for OpenCode
|
|
306
|
-
with its standard environment variables, then enable the plugin flag. This sends
|
|
307
|
-
correlated decision logs and review traces, including model reviewer attempts:
|
|
109
|
+
Stage shapes:
|
|
308
110
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
111
|
+
| Stage | Shape |
|
|
112
|
+
| --- | --- |
|
|
113
|
+
| `static` | `{ "type": "static", "allowInProjectEdits"?: boolean }` |
|
|
114
|
+
| `decision` | `{ "type": "decision", "model": "<provider>/<model>", "timeoutMs"?: number, "minConfidence"?: number }` |
|
|
115
|
+
| `llm` | `{ "type": "llm", "model": "<provider>/<model>", "timeoutMs"?: number }` |
|
|
116
|
+
|
|
117
|
+
## How review works
|
|
118
|
+
|
|
119
|
+
1. Native permissions, policies, and saved approvals produce `allow`, `ask`, or
|
|
120
|
+
`deny`. A native **deny is final** and is never reviewed or weakened.
|
|
121
|
+
2. With mode on, native allows and asks are reviewed. Saved **Always** entries
|
|
122
|
+
are re-reviewed but never deleted or rewritten.
|
|
123
|
+
3. Built-in guards run first: protected paths and credentials, risky shell
|
|
124
|
+
structure, recursive external deletion, history changes, and ambiguous syntax.
|
|
125
|
+
These guards are not user-extensible.
|
|
126
|
+
4. Configured classifiers run in array order. A static stage may resolve low-risk
|
|
127
|
+
work; a decision stage uses OpenCode's native decision API; an LLM stage uses
|
|
128
|
+
its explicit reviewer. The first decisive allow or deny stops the chain.
|
|
129
|
+
5. Exhaustion, unusable context, interruption, failure, or timeout resolves to
|
|
130
|
+
**ask**. A deny ends with `Continue with a safer alternative.`
|
|
131
|
+
|
|
132
|
+
Mode off restores the original host effect. A paused breaker turns a reviewed
|
|
133
|
+
native allow into `ask`; an existing native ask keeps its prompt.
|
|
134
|
+
|
|
135
|
+
Assuming classifiers: [static, decision, llm] (array order is authoritative).
|
|
136
|
+
|
|
137
|
+
```mermaid
|
|
138
|
+
flowchart TD
|
|
139
|
+
A[permission evaluate<br/>allow / ask / deny] --> B{native deny?}
|
|
140
|
+
B -- yes --> B1[final, untouched<br/>audit only]
|
|
141
|
+
B -- no --> C{config valid &<br/>session on?}
|
|
142
|
+
C -- invalid --> C1[passthrough<br/>review disabled]
|
|
143
|
+
C -- off --> C2[restore native<br/>effect + message]
|
|
144
|
+
C -- paused --> C3[allow → ask<br/>ask → passthrough]
|
|
145
|
+
|
|
146
|
+
C -- on --> D[context fetch<br/>size ≤ 64 KiB]
|
|
147
|
+
D --> E[Code Mode hash identity<br/>execute:sha256]
|
|
148
|
+
E --> S
|
|
149
|
+
|
|
150
|
+
subgraph S [scoreRequest — every hand-written safety check lives here]
|
|
151
|
+
S1[shell: guardShell + visibleShellDeny<br/>parse / operand roles / recursive delete]
|
|
152
|
+
S2[tiers: protected paths, credential names,<br/>symlink traversal, project scope]
|
|
153
|
+
S3[read/edit/write: URL, scope, safeTarget]
|
|
154
|
+
S4[webfetch/websearch/execute/unknown → HIGH]
|
|
155
|
+
S5[git history → evaluateVcsGate]
|
|
156
|
+
S1 --> S2 --> S3 --> S4 --> S5
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
S -- deny --> X1[DENY]
|
|
160
|
+
S -- ask --> X2[ASK]
|
|
161
|
+
S -- low / medium / high --> R
|
|
162
|
+
|
|
163
|
+
subgraph R [runClassifiers]
|
|
164
|
+
R0{score deny/ask?} -- yes --> X1
|
|
165
|
+
R0 -- no --> R1[static: low → ALLOW<br/>else abstain]
|
|
166
|
+
R1 -- abstain --> R2[decision: native generate.decisions<br/>confidence ≥ 0.9]
|
|
167
|
+
R2 -- ask / low confidence --> R3[llm: native generate.text]
|
|
168
|
+
R2 -- allow / deny --> T[terminal]
|
|
169
|
+
R3 -- allow / deny --> T
|
|
170
|
+
R3 -- ask / unresolved --> X2
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
X1 --> F[modes.finish<br/>counts + breaker]
|
|
174
|
+
X2 --> F
|
|
175
|
+
T --> F
|
|
176
|
+
F --> G[apply effect to host]
|
|
326
177
|
```
|
|
327
178
|
|
|
328
|
-
|
|
329
|
-
|
|
179
|
+
## Safety limits
|
|
180
|
+
|
|
181
|
+
- **Native Auto/YOLO is client-local.** If enabled, OpenCode may auto-approve an
|
|
182
|
+
`ask` without a dialog, including a plugin ask. Plugin checks still run and
|
|
183
|
+
plugin denies still block, but the plugin cannot read, change, or synchronize
|
|
184
|
+
that switch. Disable client Auto/YOLO if you need prompts.
|
|
185
|
+
- **Invalid configuration acts disabled.** The plugin performs no review and
|
|
186
|
+
native behavior applies. That is passthrough, not fail-closed.
|
|
187
|
+
- Coverage is limited to the permission `evaluate` hook and the Code Mode
|
|
188
|
+
`execute.before` bridge. Native path grants are not an OS sandbox.
|
|
189
|
+
- Code Mode stays HIGH: JavaScript can hide egress behind aliases and computed
|
|
190
|
+
URLs. Its permission identity is the SHA-256 of the exact UTF-8 source. Old
|
|
191
|
+
raw-code approvals generally do not match.
|
|
192
|
+
- Other project plugins can affect residual effects; only run code you trust.
|
|
193
|
+
|
|
194
|
+
## Audit
|
|
195
|
+
|
|
196
|
+
Audit is the only plugin-generated telemetry and is **off by default**. It is
|
|
197
|
+
local and unsafe: raw records are unredacted and can contain secrets, file
|
|
198
|
+
contents, prompts, and model outputs. If enabled, the fixed path is
|
|
199
|
+
`$XDG_STATE_HOME/opencode-auto-mode/decisions.jsonl` (or
|
|
200
|
+
`~/.local/state/opencode-auto-mode/decisions.jsonl` when `XDG_STATE_HOME` is not
|
|
201
|
+
absolute). Rotated archives are retained; delete them manually. See
|
|
202
|
+
[README.llm.md](README.llm.md#audit-and-replay) before enabling it.
|
|
203
|
+
|
|
204
|
+
## More documentation
|
|
205
|
+
|
|
206
|
+
- [README.llm.md](README.llm.md) — installation instructions for an AI agent,
|
|
207
|
+
full options, migration from 1.0.x, audit, and troubleshooting.
|
|
208
|
+
- [CONTRIBUTING.md](CONTRIBUTING.md) — repository map, local development,
|
|
209
|
+
tests, and release checks. It ships with the package.
|
|
330
210
|
|
|
331
211
|
## Credit
|
|
332
212
|
|
|
333
213
|
Created originally by [Andrew Mellen (mellena1)](https://github.com/mellena1) in
|
|
334
|
-
[mellena1/opencode-auto-mode](https://github.com/mellena1/opencode-auto-mode)
|
|
335
|
-
|
|
336
|
-
project retains the original [MIT license](LICENSE).
|
|
214
|
+
[mellena1/opencode-auto-mode](https://github.com/mellena1/opencode-auto-mode).
|
|
215
|
+
This independent project retains the original [MIT license](LICENSE).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:`Module`}});let e=require("solid-js"),t=require("@opencode/plugin/gui"),n=require("@opencode/plugin/rpc");var r={type:`object`,properties:{enabled:{type:`boolean`},mode:{type:`string`,enum:[`on`,`off`,`paused`,`invalid`]},configError:{type:`string`,maxLength:200},errorKind:{type:`string`,enum:[`config`,`storage`,`status`]},reviewTimeoutMs:{type:`number`},reviewing:{type:`array`,maxItems:256,items:{type:`object`,properties:{sessionID:{type:`string`},reviewID:{type:`string`},startedAt:{type:`number`}},required:[`sessionID`,`reviewID`,`startedAt`],additionalProperties:!1}},counts:{type:`object`,properties:{allow:{type:`integer`,minimum:0},ask:{type:`integer`,minimum:0},deny:{type:`integer`,minimum:0}},required:[`allow`,`ask`,`deny`],additionalProperties:!1},breaker:{type:`boolean`}},required:[`enabled`,`mode`,`reviewTimeoutMs`,`reviewing`,`counts`],additionalProperties:!1},i=n.Rpc.define({id:`opencode-auto-mode`,methods:{status:{input:{type:`object`,properties:{sessionID:{type:`string`,minLength:1,maxLength:256}},additionalProperties:!1},output:r},setMode:{input:{type:`object`,properties:{sessionID:{type:`string`,minLength:1,maxLength:256},mode:{type:`string`,enum:[`on`,`off`,`resume`]}},required:[`sessionID`,`mode`],additionalProperties:!1},output:r}},events:{decision:{schema:{type:`object`,properties:{reviewID:{type:`string`},sessionID:{type:`string`},action:{type:`string`,maxLength:128},decision:{type:`string`,maxLength:64},stage:{type:`string`,maxLength:64},reason:{type:`string`,maxLength:200},durationMs:{type:`number`},at:{type:`number`},errorKind:{type:`string`,maxLength:64}},required:[`reviewID`,`sessionID`,`action`,`decision`,`stage`,`reason`,`durationMs`,`at`],additionalProperties:!1}}}});function a(e,t){return JSON.stringify([e,t.directory,t.workspaceID])}function o(e){if(!(e?.location===void 0||!e.id||e.id.length>256||!e.location.directory))return{key:a(e.key,e.location),sessionID:e.id,ownServer:e.server.builtin,location:{...e.location}}}var s={enabled:!1,mode:`invalid`,configError:`status unavailable`,errorKind:`status`,reviewTimeoutMs:0,reviewing:[],counts:{allow:0,ask:0,deny:0}},c=5e3;function l(e,t,n){let r=new AbortController,i=new Promise((e,n)=>{r.signal.addEventListener(`abort`,()=>n(Error(t)),{once:!0})}),a=setTimeout(()=>r.abort(),e),o=Promise.resolve().then(()=>{if(r.signal.aborted)throw Error(t);return n(r.signal)});return{abort:()=>r.abort(),done:Promise.race([o,i]).finally(()=>clearTimeout(a))}}function u(e,t,n=c){let r,i,a=0,o=!1,u=()=>{i?.request.abort(),i=void 0};return Object.assign(c=>{if(o||(r!==c?.key&&(r=c?.key,a++,u(),t(void 0)),c===void 0||!c.ownServer))return Promise.resolve();if(i!==void 0)return i.done;let d=a,f=l(n,`status request aborted`,t=>e(c,t)),p=f.done.then(e=>{!o&&d===a&&t(e??{...s},c)},()=>{!o&&d===a&&t({...s},c)}).finally(()=>{i?.request===f&&(i=void 0)});return i={request:f,done:p},p},{invalidate(){a++,u()},dispose(){o=!0,a++,u()}})}function d(e,t,n,r=()=>{},i=c){let a=new Map,o=!1,s=Number.isFinite(i)&&i>0?i:c;return{pending:e=>a.has(e.key),run(i,c){if(o||i===void 0||!i.ownServer||c===void 0)return Promise.resolve();let u=a.get(i.key);if(u)return u.done;let d=l(s,`mode change aborted`,t=>e(i,c,t)),f=d.done.then(e=>{!o&&e!==void 0&&t(i,e)},()=>{o||n()}).finally(()=>{a.get(i.key)?.request===d&&a.delete(i.key),o||r()});return a.set(i.key,{request:d,done:f}),r(),f},dispose(){o=!0;for(let e of a.values())e.request.abort();a.clear()}}}function f(e){switch(e?.mode){case`on`:return e.enabled?`off`:`on`;case`off`:return`on`;case`paused`:return`resume`;default:return}}function p(e){if(e!==void 0)switch(e.mode){case`invalid`:return e.errorKind===`status`||e.errorKind===void 0&&e.configError===`status unavailable`?`status unavailable`:e.errorKind===`config`?`Invalid config — review disabled`:`Review unavailable`;case`paused`:return`paused (breaker)`;case`off`:return`off`;case`on`:return e.enabled?`auto`:`off`}}function m(e){if(e?.mode===`invalid`&&e.configError!==void 0&&e.configError!==`status unavailable`)return`${p(e)}: ${e.configError}`}function h(e){if(e!==void 0)return e.mode===`invalid`?`danger`:e.mode===`paused`?`warning`:e.mode===`on`&&e.enabled?`success`:void 0}function g(e,t,n=Date.now){let r=new Set,i=0,a=0;return o=>{let s=e();if(s===void 0||!s.ownServer||o.location.directory!==s.location.directory||o.location.workspaceID!==s.location.workspaceID||o.data===null||typeof o.data!=`object`)return;let c=o.data;if(c.sessionID!==s.sessionID||typeof c.reviewID!=`string`||c.reviewID.length>256)return;let l=c.stage===`breaker`?`breaker`:c.decision===`deny`?`deny`:void 0;if(l===void 0)return;let u=JSON.stringify([s.key,c.reviewID,l]);if(r.has(u))return;r.add(u),r.size>128&&r.delete(r.values().next().value);let d=n();d-i>=5e3&&(i=d,a=0),!(l===`deny`&&a>=4)&&(a++,t({kind:l,message:l===`breaker`?`Auto mode paused (breaker)`:`Auto mode denied`}))}}function _(e){let t,n,r,i=!1,a=()=>{n?.abort(),r?.(),n=void 0,r=void 0};return{update(o,s){if(i||o===t)return;a(),t=o;let c=new AbortController;n=c;try{r=s(t=>{c.signal.aborted||e(t)},c.signal)}catch{a(),t=void 0}},dispose(){i=!0,a(),t=void 0}}}var v=1e3,y=t.Plugin.define({setup(n){let[r,a]=(0,e.createSignal)(void 0),[s,c]=(0,e.createSignal)(0),l=()=>o(n.sessions.current()),y=u((e,t)=>n.client.rpc(i).status({sessionID:e.sessionID},{location:e.location,signal:t}),(e,t)=>{e===void 0?a(void 0):t!==void 0&&l()?.key===t.key&&a({key:t.key,value:e})}),b=d((e,t,r)=>(y.invalidate(),n.client.rpc(i).setMode({sessionID:e.sessionID,mode:t},{location:e.location,signal:r})),(e,t)=>{l()?.key===e.key&&a({key:e.key,value:t})},()=>n.toast.show({title:`Auto-mode`,description:`Could not change auto mode. Try again.`,variant:`error`,duration:5e3}),()=>c(e=>e+1)),x=_(g(l,e=>{n.toast.show({title:`Auto-mode`,description:e.message,variant:e.kind===`deny`?`error`:`default`,duration:5e3})})),S=()=>{let e=n.client;x.update(e,(t,n)=>e.rpc(i).events.on(`decision`,t,{signal:n}));let t=l();t!==void 0&&b.pending(t)||y(t)};(0,t.createKeyed)(()=>l()?.key,S,{otherwise:S});let C=setInterval(S,v);(0,e.onCleanup)(()=>{clearInterval(C),b.dispose(),y.dispose(),x.dispose()}),n.add(t.ComposerPill,{id:`status`,order:20,show:e=>{s();let t=o(e.session),n=r(),i=t?.ownServer&&n?.key===t.key?n.value:void 0,a=f(i),c=t!==void 0&&b.pending(t),l=c?`changing auto mode…`:p(i)??`status unavailable`,u=m(i);return{label:l,tone:h(i),title:c?`Auto-mode: change pending`:u===void 0?a===void 0?`Auto-mode unavailable`:`Auto-mode: ${a}`:`Auto-mode: ${u}`,run:!c&&a!==void 0&&t?.ownServer?()=>{b.run(t,a)}:void 0}}})}});exports.default=y;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mortz.prk/opencode-auto-mode",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "Configurable automatic permission review for OpenCode v2.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Andrew Mellen",
|
|
@@ -18,7 +18,10 @@
|
|
|
18
18
|
"license": "MIT",
|
|
19
19
|
"files": [
|
|
20
20
|
"src",
|
|
21
|
-
"dist"
|
|
21
|
+
"dist",
|
|
22
|
+
"README.md",
|
|
23
|
+
"README.llm.md",
|
|
24
|
+
"CONTRIBUTING.md"
|
|
22
25
|
],
|
|
23
26
|
"exports": {
|
|
24
27
|
".": {
|
|
@@ -36,7 +39,8 @@
|
|
|
36
39
|
"./gui-experimental": "./dist/gui-experimental.cjs"
|
|
37
40
|
},
|
|
38
41
|
"scripts": {
|
|
39
|
-
"lint": "eslint . --max-warnings=0",
|
|
42
|
+
"lint": "eslint . --max-warnings=0 && node scripts/complexity-lint.js",
|
|
43
|
+
"lint:complexity": "node scripts/complexity-lint.js",
|
|
40
44
|
"typecheck": "tsc --noEmit",
|
|
41
45
|
"test": "bun test",
|
|
42
46
|
"test:integration": "bun run integration/opencode.ts",
|
|
@@ -44,10 +48,8 @@
|
|
|
44
48
|
"prepublishOnly": "bun run build:gui"
|
|
45
49
|
},
|
|
46
50
|
"dependencies": {
|
|
47
|
-
"@opencode/ai": "2.0.30",
|
|
48
51
|
"@opencode/plugin": "2.2.0",
|
|
49
52
|
"@opencode/schema": "2.2.0",
|
|
50
|
-
"effect": "4.0.1",
|
|
51
53
|
"shell-quote": "1.12.0"
|
|
52
54
|
},
|
|
53
55
|
"peerDependencies": {
|
|
@@ -59,16 +61,15 @@
|
|
|
59
61
|
"@eslint/js": "^10.0.1",
|
|
60
62
|
"@opencode/theme": "2.2.0",
|
|
61
63
|
"@opentui/core": "0.7.0",
|
|
62
|
-
"@opentui/keymap": "^0.6.6",
|
|
63
64
|
"@opentui/solid": "0.7.0",
|
|
64
65
|
"@types/node": "^26.6.4",
|
|
65
66
|
"@types/shell-quote": "1.7.5",
|
|
66
67
|
"@typescript/native": "npm:typescript@^7.0.2",
|
|
67
68
|
"eslint": "^10.12.0",
|
|
68
|
-
"solid-js": "1.9.
|
|
69
|
+
"solid-js": "1.9.17",
|
|
69
70
|
"typescript": "npm:@typescript/typescript6@^6.0.2",
|
|
70
71
|
"typescript-eslint": "^8.71.1",
|
|
71
|
-
"vite": "^
|
|
72
|
+
"vite": "^8.3.5",
|
|
72
73
|
"vite-plugin-solid": "^2.11.0"
|
|
73
74
|
}
|
|
74
75
|
}
|