@pify/yolo 0.3.0 → 0.4.0
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/README.md +22 -12
- package/extensions/yolo.ts +54 -26
- package/package.json +1 -1
- package/src/modes.ts +78 -0
- package/src/types.ts +2 -1
package/README.md
CHANGED
|
@@ -1,26 +1,35 @@
|
|
|
1
1
|
# @pify/yolo
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A safety gradient for [pi](https://github.com/earendil-works/pi), from auto-approve-everything to ask-about-anything — with an undo trail so YOLO never means unrecoverable.
|
|
4
4
|
|
|
5
5
|
Part of the [Pify suite](https://github.com/pifydev). Install with [`pify install yolo`](https://github.com/pifydev/cli) or `pi install npm:@pify/yolo`.
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## Four modes, one command (v0.4)
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
`/yolo <mode>` moves along a gradient. Two things hold in **every** mode, which is what makes the gradient safe to move along: catastrophic commands block, and secret material asks.
|
|
10
|
+
|
|
11
|
+
| Mode | Catastrophic | Built-in destructive | Your `.pi/yolo.json` ask-rules | Everything else |
|
|
12
|
+
|---|---|---|---|---|
|
|
13
|
+
| `⚡ yolo` | blocked | runs | runs | runs |
|
|
14
|
+
| `⚙ auto` | blocked | runs | asks | runs |
|
|
15
|
+
| `🛡 approve` *(default)* | blocked | asks | asks | runs |
|
|
16
|
+
| `🔒 strict` | blocked | asks | asks | asks unless plainly read-only |
|
|
17
|
+
|
|
18
|
+
Bare `/yolo` still flips between `yolo` and `approve` — the two ends people actually toggle between. Sessions saved before v0.4 carried `guard`; that is what `approve` is now called, and they reopen there.
|
|
19
|
+
|
|
20
|
+
**Behaviour change in v0.4**: `yolo` used to stand the *whole* gate down, catastrophic patterns included, which contradicted the rules' own claim that the floor is never overridable. The floor now holds in yolo mode too. If you were relying on `rm -rf /` auto-approving, you were relying on a bug.
|
|
10
21
|
|
|
11
22
|
| Tier | Examples | Behavior |
|
|
12
23
|
|---|---|---|
|
|
13
|
-
| **BLOCK** | `rm -rf /`, `rm -rf ~`, `rm -rf .git`, `mkfs`, `dd of=/dev/…`, fork bomb, `> /dev/sda` | Refused outright. Never overridable — not
|
|
24
|
+
| **BLOCK** | `rm -rf /`, `rm -rf ~`, `rm -rf .git`, `mkfs`, `dd of=/dev/…`, fork bomb, `> /dev/sda` | Refused outright, in every mode. Never overridable — not by user rules, not by a mode. |
|
|
14
25
|
| **ASK** | `rm -rf <path>`, `git push --force`, `git reset --hard`, `git clean -f`, `curl \| sh`, `find -delete`, `chmod 777`, history rewrites | Confirmation dialog with the command shown. Denials can carry your reason back to the agent. |
|
|
15
|
-
| ALLOW | everything else | Runs untouched. |
|
|
16
|
-
|
|
17
|
-
**⚡ yolo** (`/yolo`) — the gate stands down and everything auto-approves. The trail keeps recording.
|
|
26
|
+
| ALLOW | everything else | Runs untouched (unless you are in `strict`). |
|
|
18
27
|
|
|
19
28
|
Fail-closed everywhere: rule-evaluation errors block; ASK without a UI (headless/CI) denies.
|
|
20
29
|
|
|
21
30
|
## Secret files (v0.2)
|
|
22
31
|
|
|
23
|
-
Credentials are the one thing
|
|
32
|
+
Credentials are the one thing no mode waves through — auto-approving speed is worth it, auto-approving your AWS keys into a prompt is not. Any `read`/`edit`/`write` on secret material, and any bash command that names it, asks first in every mode:
|
|
24
33
|
|
|
25
34
|
`.env` (and `.env.*`, but not `.env.example`/`.sample`/`.template`) · `~/.ssh/*` and `id_rsa`/`id_ed25519`-style keys (`.pub` halves are fine) · `.aws/credentials` · `.pi/agent/auth.json`, `.claude/.credentials.json` · `.npmrc`, `.pypirc`, `.netrc`, `.git-credentials` · `~/.config/gh/hosts.yml` · `*.pem`, `*.key`, `*.p12`, `*.pfx` · `secrets.json`/`credentials.yaml`
|
|
26
35
|
|
|
@@ -39,7 +48,7 @@ That covers what `/yolo undo` can't: damage done by a command rather than by an
|
|
|
39
48
|
|
|
40
49
|
## The undo trail
|
|
41
50
|
|
|
42
|
-
Always on, in
|
|
51
|
+
Always on, in every mode:
|
|
43
52
|
|
|
44
53
|
- Every `edit`/`write` saves the file's **pre-image** first (per-project trail under the agent dir — survives restarts).
|
|
45
54
|
- Risky bash commands are logged with cwd, timestamp, and git HEAD.
|
|
@@ -66,7 +75,7 @@ Measured over OpenRouter on six commands (three genuinely destructive, three rea
|
|
|
66
75
|
| GPT-5.6 terra / sol | 5/6 | 1 |
|
|
67
76
|
| Claude Opus 5 | 4/6 | 1 |
|
|
68
77
|
| Gemini 3.1 Pro | 2/6 | 4 |
|
|
69
|
-
| Qwen3 235B |
|
|
78
|
+
| Qwen3 235B | 2/6 | 4 |
|
|
70
79
|
|
|
71
80
|
Every miss fell back to *allow* — no run ever downgraded a command the rules had already flagged. Weaker models simply give you less extra protection.
|
|
72
81
|
|
|
@@ -86,8 +95,9 @@ Every miss fell back to *allow* — no run ever downgraded a command the rules h
|
|
|
86
95
|
## Commands
|
|
87
96
|
|
|
88
97
|
```
|
|
89
|
-
/yolo #
|
|
90
|
-
/yolo
|
|
98
|
+
/yolo # flip between yolo and approve
|
|
99
|
+
/yolo strict # or: yolo | auto | approve | strict (v0.4)
|
|
100
|
+
/yolo status # mode, the other modes, rule count, trail size
|
|
91
101
|
/yolo trail # recent trail entries
|
|
92
102
|
/yolo undo 3 # restore the newest 3 file pre-images
|
|
93
103
|
/yolo classifier on # let a model flag unfamiliar commands (v0.3)
|
package/extensions/yolo.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @pify/yolo — one toggle to auto-approve everything, with an undo trail.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* catastrophic patterns BLOCK
|
|
6
|
-
* ones ASK with the command shown (denial reasons flow back to
|
|
7
|
-
* everything else runs
|
|
8
|
-
*
|
|
4
|
+
* A four-mode gradient (v0.4): yolo · auto · approve (default) · strict.
|
|
5
|
+
* Bash runs through a three-tier rule set — catastrophic patterns BLOCK,
|
|
6
|
+
* destructive ones ASK with the command shown (denial reasons flow back to
|
|
7
|
+
* the agent), everything else runs — and the mode decides how much of that
|
|
8
|
+
* to relax or tighten. Two invariants hold in every mode: the catastrophic
|
|
9
|
+
* floor blocks, and secret material asks. In EVERY mode
|
|
9
10
|
* edit/write saves a pre-image first and risky bash commands are logged,
|
|
10
11
|
* so /yolo undo can walk file changes back even after a restart.
|
|
11
12
|
*
|
|
@@ -13,7 +14,7 @@
|
|
|
13
14
|
* headless ASK becomes deny. User rules in .pi/yolo.json (wildcard,
|
|
14
15
|
* last-match-wins) can retune ASK/ALLOW but never the catastrophic floor.
|
|
15
16
|
*
|
|
16
|
-
* v0.2 adds two things
|
|
17
|
+
* v0.2 adds two things no mode stands down for:
|
|
17
18
|
* secret material (.env, ssh keys, cloud/registry credentials) asks before
|
|
18
19
|
* any read/edit/write or naming command, and every risky bash command gets a
|
|
19
20
|
* `git stash create` checkpoint recorded on the trail so command damage —
|
|
@@ -46,8 +47,19 @@ import {
|
|
|
46
47
|
} from "../src/classify.ts";
|
|
47
48
|
import { evaluateCommand, evaluatePath, parseUserRules } from "../src/rules.ts";
|
|
48
49
|
import { formatTrail, readManifest, recordBash, recordPreImage, trailDir, undo } from "../src/trail.ts";
|
|
50
|
+
import {
|
|
51
|
+
MODES,
|
|
52
|
+
MODE_BADGES,
|
|
53
|
+
MODE_LABELS,
|
|
54
|
+
askTitle,
|
|
55
|
+
normalizeMode,
|
|
56
|
+
resolveAction,
|
|
57
|
+
} from "../src/modes.ts";
|
|
49
58
|
import { isRecord, type Mode, type UserRule } from "../src/types.ts";
|
|
50
59
|
|
|
60
|
+
/** Today's guard, under its new name. */
|
|
61
|
+
const DEFAULT_MODE: Mode = "approve";
|
|
62
|
+
|
|
51
63
|
const MODE_ENTRY = "yolo-mode";
|
|
52
64
|
const CLASSIFIER_ENTRY = "yolo-classifier";
|
|
53
65
|
/** In front of every bash call: a slow answer costs seconds, not minutes. */
|
|
@@ -56,7 +68,7 @@ const CLASSIFY_TIMEOUT_MS = 20_000;
|
|
|
56
68
|
type UiContext = ExtensionContext;
|
|
57
69
|
|
|
58
70
|
export default function yolo(pi: ExtensionAPI) {
|
|
59
|
-
let mode: Mode =
|
|
71
|
+
let mode: Mode = DEFAULT_MODE;
|
|
60
72
|
/** Opt-in: layer 3 costs a model call on unfamiliar commands. */
|
|
61
73
|
let classifierEnabled = false;
|
|
62
74
|
let userRules: UserRule[] = [];
|
|
@@ -64,7 +76,7 @@ export default function yolo(pi: ExtensionAPI) {
|
|
|
64
76
|
|
|
65
77
|
function updateFooter(ctx: UiContext): void {
|
|
66
78
|
if (!ctx.hasUI) return;
|
|
67
|
-
ctx.ui.setStatus("yolo", mode
|
|
79
|
+
ctx.ui.setStatus("yolo", MODE_BADGES[mode]);
|
|
68
80
|
}
|
|
69
81
|
|
|
70
82
|
function setMode(ctx: UiContext, next: Mode): void {
|
|
@@ -73,9 +85,10 @@ export default function yolo(pi: ExtensionAPI) {
|
|
|
73
85
|
updateFooter(ctx);
|
|
74
86
|
if (ctx.hasUI) {
|
|
75
87
|
ctx.ui.notify(
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
88
|
+
[
|
|
89
|
+
MODE_LABELS[next],
|
|
90
|
+
"Catastrophic commands block and secrets ask in every mode. The undo trail keeps recording.",
|
|
91
|
+
].join("\n"),
|
|
79
92
|
next === "yolo" ? "warning" : "info",
|
|
80
93
|
);
|
|
81
94
|
}
|
|
@@ -245,7 +258,14 @@ export default function yolo(pi: ExtensionAPI) {
|
|
|
245
258
|
if (escalated.rule) verdict = { action: "ask", rule: escalated.rule };
|
|
246
259
|
}
|
|
247
260
|
|
|
248
|
-
|
|
261
|
+
// The mode decides what the verdict means: it may relax the destructive
|
|
262
|
+
// tier (auto/yolo) or tighten the allow tier (strict), but never touches
|
|
263
|
+
// the catastrophic floor or the secret gate.
|
|
264
|
+
const action = resolveAction({
|
|
265
|
+
mode,
|
|
266
|
+
verdict,
|
|
267
|
+
obviouslySafe: !needsClassification(command),
|
|
268
|
+
});
|
|
249
269
|
|
|
250
270
|
// Log risky commands, with a checkpoint of the tree as it was.
|
|
251
271
|
if (dir && verdict.action !== "allow") {
|
|
@@ -253,12 +273,9 @@ export default function yolo(pi: ExtensionAPI) {
|
|
|
253
273
|
recordBash(dir, command, ctx.cwd, gitHead(ctx.cwd), now, gitCheckpoint(ctx.cwd, now));
|
|
254
274
|
}
|
|
255
275
|
|
|
256
|
-
|
|
257
|
-
if (mode === "yolo" && !touchesSecret) return undefined;
|
|
258
|
-
|
|
259
|
-
if (verdict.action === "allow") return undefined;
|
|
276
|
+
if (action === "allow") return undefined;
|
|
260
277
|
|
|
261
|
-
if (
|
|
278
|
+
if (action === "block") {
|
|
262
279
|
return {
|
|
263
280
|
block: true,
|
|
264
281
|
reason: `yolo guard blocked this command (${verdict.rule}) — catastrophic patterns are never auto-approved. Do not retry it; choose a safer approach.`,
|
|
@@ -273,7 +290,7 @@ export default function yolo(pi: ExtensionAPI) {
|
|
|
273
290
|
};
|
|
274
291
|
}
|
|
275
292
|
const approved = await ctx.ui.confirm(
|
|
276
|
-
|
|
293
|
+
askTitle(mode, verdict),
|
|
277
294
|
`${command}\n\nRule: ${verdict.rule}. Run it?`,
|
|
278
295
|
);
|
|
279
296
|
if (approved) return undefined;
|
|
@@ -293,12 +310,13 @@ export default function yolo(pi: ExtensionAPI) {
|
|
|
293
310
|
pi.on("session_start", async (_event, ctx) => {
|
|
294
311
|
dir = trailDir(getAgentDir(), ctx.cwd);
|
|
295
312
|
loadUserRules(ctx.cwd);
|
|
296
|
-
mode =
|
|
313
|
+
mode = DEFAULT_MODE;
|
|
297
314
|
classifierEnabled = false;
|
|
298
315
|
for (const entry of ctx.sessionManager.getBranch()) {
|
|
299
316
|
const e = entry as { type?: string; customType?: string; data?: unknown };
|
|
300
317
|
if (e.type === "custom" && e.customType === MODE_ENTRY && isRecord(e.data)) {
|
|
301
|
-
|
|
318
|
+
const restored = normalizeMode(e.data.mode);
|
|
319
|
+
if (restored) mode = restored;
|
|
302
320
|
}
|
|
303
321
|
if (e.type === "custom" && e.customType === CLASSIFIER_ENTRY && isRecord(e.data)) {
|
|
304
322
|
if (typeof e.data.enabled === "boolean") classifierEnabled = e.data.enabled;
|
|
@@ -308,12 +326,13 @@ export default function yolo(pi: ExtensionAPI) {
|
|
|
308
326
|
});
|
|
309
327
|
|
|
310
328
|
pi.on("session_tree", async (_event, ctx) => {
|
|
311
|
-
mode =
|
|
329
|
+
mode = DEFAULT_MODE;
|
|
312
330
|
classifierEnabled = false;
|
|
313
331
|
for (const entry of ctx.sessionManager.getBranch()) {
|
|
314
332
|
const e = entry as { type?: string; customType?: string; data?: unknown };
|
|
315
333
|
if (e.type === "custom" && e.customType === MODE_ENTRY && isRecord(e.data)) {
|
|
316
|
-
|
|
334
|
+
const restored = normalizeMode(e.data.mode);
|
|
335
|
+
if (restored) mode = restored;
|
|
317
336
|
}
|
|
318
337
|
if (e.type === "custom" && e.customType === CLASSIFIER_ENTRY && isRecord(e.data)) {
|
|
319
338
|
if (typeof e.data.enabled === "boolean") classifierEnabled = e.data.enabled;
|
|
@@ -329,25 +348,34 @@ export default function yolo(pi: ExtensionAPI) {
|
|
|
329
348
|
// ── Command ──────────────────────────────────────────────────────────
|
|
330
349
|
|
|
331
350
|
pi.registerCommand("yolo", {
|
|
332
|
-
description: "
|
|
351
|
+
description: "Safety gradient: /yolo [yolo|auto|approve|strict|status|trail|undo [n]|classifier on|off]",
|
|
333
352
|
handler: async (args, ctx) => {
|
|
334
353
|
const [route, countRaw] = (args ?? "").trim().toLowerCase().split(/\s+/);
|
|
335
354
|
switch (route || "toggle") {
|
|
336
355
|
case "toggle":
|
|
337
|
-
|
|
356
|
+
// The bare command still flips between the two ends people use.
|
|
357
|
+
setMode(ctx, mode === "yolo" ? DEFAULT_MODE : "yolo");
|
|
338
358
|
return;
|
|
339
359
|
case "on":
|
|
340
360
|
setMode(ctx, "yolo");
|
|
341
361
|
return;
|
|
342
362
|
case "off":
|
|
343
|
-
|
|
363
|
+
case "guard":
|
|
364
|
+
setMode(ctx, DEFAULT_MODE);
|
|
365
|
+
return;
|
|
366
|
+
case "yolo":
|
|
367
|
+
case "auto":
|
|
368
|
+
case "approve":
|
|
369
|
+
case "strict":
|
|
370
|
+
setMode(ctx, route as Mode);
|
|
344
371
|
return;
|
|
345
372
|
case "status": {
|
|
346
373
|
if (!ctx.hasUI) return;
|
|
347
374
|
const entries = readManifest(dir);
|
|
348
375
|
ctx.ui.notify(
|
|
349
376
|
[
|
|
350
|
-
`Mode: ${mode
|
|
377
|
+
`Mode: ${MODE_LABELS[mode]}`,
|
|
378
|
+
`Other modes: ${MODES.filter((m) => m !== mode).join(" · ")} (/yolo <mode>)`,
|
|
351
379
|
`User rules: ${userRules.length} (.pi/yolo.json)`,
|
|
352
380
|
`AI classifier: ${classifierEnabled ? "on" : "off"} (/yolo classifier on)`,
|
|
353
381
|
`Trail: ${entries.length} entries — /yolo trail to view, /yolo undo [n] to restore`,
|
package/package.json
CHANGED
package/src/modes.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The four-mode gradient (zhushanwen). One toggle was too blunt: "guard"
|
|
3
|
+
* asked about every `rm -rf build`, and "yolo" stood the whole gate down,
|
|
4
|
+
* including the catastrophic floor the rules call non-negotiable. These are
|
|
5
|
+
* the four positions people actually want between those extremes.
|
|
6
|
+
*
|
|
7
|
+
* Two invariants hold in every mode, which is what makes the gradient safe
|
|
8
|
+
* to move along: the catastrophic tier always blocks, and secret material
|
|
9
|
+
* always asks.
|
|
10
|
+
*/
|
|
11
|
+
import type { GuardAction } from "./classify.ts";
|
|
12
|
+
import type { Mode, RuleHit } from "./types.ts";
|
|
13
|
+
|
|
14
|
+
export const MODES: readonly Mode[] = ["yolo", "auto", "approve", "strict"];
|
|
15
|
+
|
|
16
|
+
export const MODE_LABELS: Record<Mode, string> = {
|
|
17
|
+
yolo: "⚡ YOLO — only catastrophic commands and secrets stop you",
|
|
18
|
+
auto: "⚙ auto — built-in destructive commands run; your own ask-rules still ask",
|
|
19
|
+
approve: "🛡 approve — catastrophic blocks, destructive asks (default)",
|
|
20
|
+
strict: "🔒 strict — anything not plainly read-only asks first",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const MODE_BADGES: Record<Mode, string | undefined> = {
|
|
24
|
+
yolo: "⚡ YOLO",
|
|
25
|
+
auto: "⚙ auto",
|
|
26
|
+
// The default needs no badge; a permanent one just becomes furniture.
|
|
27
|
+
approve: undefined,
|
|
28
|
+
strict: "🔒 strict",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export function isMode(value: unknown): value is Mode {
|
|
32
|
+
return typeof value === "string" && (MODES as readonly string[]).includes(value);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Pre-v0.4 sessions stored "guard"; it is what "approve" is now called. */
|
|
36
|
+
export function normalizeMode(value: unknown): Mode | null {
|
|
37
|
+
if (value === "guard") return "approve";
|
|
38
|
+
return isMode(value) ? value : null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface ResolveInput {
|
|
42
|
+
mode: Mode;
|
|
43
|
+
verdict: RuleHit;
|
|
44
|
+
/** The command is on the read-only list (see classify.ts). */
|
|
45
|
+
obviouslySafe: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The effective action for a command, given the deterministic verdict and the
|
|
50
|
+
* current mode. Modes may relax the destructive tier and tighten the allow
|
|
51
|
+
* tier; they can never touch the two invariants.
|
|
52
|
+
*/
|
|
53
|
+
export function resolveAction({ mode, verdict, obviouslySafe }: ResolveInput): GuardAction {
|
|
54
|
+
// Invariant 1: catastrophic patterns block everywhere, yolo included. The
|
|
55
|
+
// rules call this floor non-negotiable, so a mode must not be a way under it.
|
|
56
|
+
if (verdict.action === "block") return "block";
|
|
57
|
+
// Invariant 2: credentials are never auto-approved (cc-safety-net pillar 2).
|
|
58
|
+
if (verdict.rule.startsWith("secret:")) return "ask";
|
|
59
|
+
|
|
60
|
+
switch (mode) {
|
|
61
|
+
case "yolo":
|
|
62
|
+
return "allow";
|
|
63
|
+
case "auto":
|
|
64
|
+
// A rule the user wrote by hand is an instruction, not a default.
|
|
65
|
+
return verdict.action === "ask" && verdict.rule.startsWith("user:") ? "ask" : "allow";
|
|
66
|
+
case "approve":
|
|
67
|
+
return verdict.action;
|
|
68
|
+
case "strict":
|
|
69
|
+
return verdict.action === "allow" && obviouslySafe ? "allow" : "ask";
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Explains a mode-driven decision in the confirmation dialog. */
|
|
74
|
+
export function askTitle(mode: Mode, verdict: RuleHit): string {
|
|
75
|
+
if (verdict.rule.startsWith("secret:")) return "Command touches secret material";
|
|
76
|
+
if (mode === "strict" && verdict.action === "allow") return "Strict mode — unrecognised command";
|
|
77
|
+
return "Destructive command";
|
|
78
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* No imports from pi packages: src/ typechecks and runs standalone.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
/** Four positions on the safety gradient; see src/modes.ts (v0.4). */
|
|
7
|
+
export type Mode = "yolo" | "auto" | "approve" | "strict";
|
|
7
8
|
|
|
8
9
|
export type RuleAction = "allow" | "ask" | "block";
|
|
9
10
|
|