z0gcode 0.2.0 → 0.3.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 CHANGED
@@ -11,6 +11,7 @@
11
11
  </p>
12
12
 
13
13
  <p align="center">
14
+ <a href="https://www.npmjs.com/package/z0gcode"><img src="https://img.shields.io/npm/v/z0gcode?color=A78BFF&label=npm" alt="npm"></a>
14
15
  <img src="https://img.shields.io/badge/license-MIT-A78BFF" alt="MIT license">
15
16
  <img src="https://img.shields.io/badge/node-18%2B-6B7080" alt="Node 18+">
16
17
  <img src="https://img.shields.io/badge/brain-0G_Compute_TEE-A78BFF" alt="0G Compute">
@@ -60,7 +61,7 @@ npm install && npm link # or run directly with: node bin/z0g.mjs
60
61
 
61
62
  ```bash
62
63
  z0g "add a /health endpoint to server.js and test it" # one-shot task
63
- z0g --auto "scaffold a Fastify app and run it" # --auto allows shell commands (on-chain is a separate --onchain opt-in)
64
+ z0g --auto "scaffold a Fastify app and run it" # auto mode: runs commands without asking (else it asks you first)
64
65
  z0g goal --auto "make the failing tests pass" # iterate until a verify command passes
65
66
  z0g --continue "now add input validation" # resume the most recent chat
66
67
  z0g --resume # pick a chat to resume (arrow-key picker)
@@ -74,14 +75,15 @@ z0g transcribe memo.mp3 # transcribe audio on 0
74
75
  z0g serve --mcp # expose z0gcode's 0G tools over MCP
75
76
  ```
76
77
 
77
- **In the REPL**, type `/` then **Tab** to autocomplete slash commands: `/chats`, `/new`, `/rename`, `/init`, `/goal`, `/model`, `/effort`, `/subagents`, `/onchain`, `/skills`, `/attest`, `/share`, `/pull`, `/mint`, `/plan`, `/verify`, `/clear`, `/help`, `/exit`. `/chats` opens an arrow-key session picker (type to search, `ctrl-r` rename, `ctrl-x` delete); `/new [title]` starts a chat and `/rename <title>` renames the current one. `/model` opens the model picker (saved to `~/.z0gcode/settings.json`); `/effort low|medium|high` (or `default`) tunes reasoning depth vs speed and cost; `/subagents on|off` toggles parallel subagents; `/onchain on|off` toggles gas-spending on-chain actions (off by default); `/skills` lists and toggles your skills; `/share [anchor]` exports the session (encrypted) to 0G Storage (and anchors it on 0G Chain), and `/pull <root>` fetches, verifies, and decrypts one back. A short intro animation and a "thinking on 0G" indicator play on a color TTY; set `Z0G_NO_ANIM=1` to disable. Each turn is separated by a divider carrying a running session token and cost counter.
78
+ **In the REPL**, type `/` then **Tab** to autocomplete slash commands: `/chats`, `/new`, `/rename`, `/init`, `/goal`, `/mode`, `/model`, `/effort`, `/subagents`, `/onchain`, `/skills`, `/attest`, `/share`, `/pull`, `/mint`, `/plan`, `/verify`, `/clear`, `/help`, `/exit`. `/chats` opens an arrow-key session picker (type to search, `ctrl-r` rename, `ctrl-x` delete); `/new [title]` starts a chat and `/rename <title>` renames the current one. `/mode ask|auto|plan` switches the permission mode; `/model` opens the model picker (saved to `~/.z0gcode/settings.json`); `/effort low|medium|high` (or `default`) tunes reasoning depth vs speed and cost; `/subagents on|off` toggles parallel subagents; `/onchain on|off` toggles gas-spending on-chain actions (off by default); `/skills` lists and toggles your skills; `/share [anchor]` exports the session (encrypted) to 0G Storage (and anchors it on 0G Chain), and `/pull <root>` fetches, verifies, and decrypts one back. A short intro animation and a "thinking on 0G" indicator play on a color TTY; set `Z0G_NO_ANIM=1` to disable. Each turn is separated by a divider carrying a running session token and cost counter.
78
79
 
79
80
  **Options:** `--auto`, `--onchain`, `--continue`, `--resume`, `--new`, `--model <id>`, `--effort low|medium|high`, `--no-subagents`, `--verify "<cmd>"`, `--auto-verify`, `--max-steps <n>`, `--cwd <dir>`, and `--json` (with `models`).
80
81
 
81
82
  ## Features
82
83
 
83
84
  **The agent**
84
- - Agentic loop with tools: `search_files` (regex + glob), `read_file`, `write_file`, `edit_file`, `list_dir`, `run_bash` (gated by `--auto`), `update_plan`, and `read_skill`.
85
+ - Agentic loop with tools: `search_files` (regex + glob), `read_file`, `write_file`, `edit_file`, `list_dir`, `run_bash` (gated by the permission mode below), `update_plan`, and `read_skill`.
86
+ - **Permission modes** (`/mode`, like Claude Code): **ask** (default) prompts before each shell command and on-chain action (`y` / `n` / `a` = always), and "always" choices are remembered in `~/.z0gcode/settings.json` so it never asks again for that program; **auto** (`--auto`) runs without asking; **plan** is read-only, the agent explores and proposes a plan and touches nothing until you switch to ask or auto.
85
87
  - Colored diffs for every change, an inference HUD (tokens, answering model, `0G Compute (TEE)`), and a visible planning checklist on multi-step tasks.
86
88
  - Streaming answers rendered as terminal markdown (bold, headings, lists, tables, inline code, and syntax-highlighted code blocks for JS/TS, Python, Solidity, Go, Rust, shell, JSON, and more); piped output stays raw and greppable.
87
89
  - Multiple chats per project, each isolating its own history, plan, and provenance under `.z0g/sessions/`. On open, an arrow-key picker (with search, rename, delete) resumes a chat; `--continue` resumes the most recent, `--resume` shows the picker, `/chats` switches mid-session. Plus a goal loop (`z0g goal` re-runs until a verify command passes) and auto-verify.
@@ -117,7 +119,7 @@ Every on-chain action, storage upload, chain deploy, the encrypted session `shar
117
119
 
118
120
  ## Roadmap
119
121
 
120
- Shipped: streaming with markdown rendering, multiple chat sessions per project (resume picker with search), planning, slash commands, the goal loop and auto-verify, in-agent `deploy_0g_chain` and `upload_0g_storage` (mainnet-verified, opt-in), the private verifiable session (`z0g share`, wallet-encrypted to 0G Storage + `--anchor` on 0G Chain, and `z0g pull` to fetch, verify, and decrypt, mainnet-verified), the session INFT (`z0g mint`, mainnet-verified), parallel subagents, media on 0G (image + transcription), MCP both ways, the model catalog and arrow-key picker, and user skills.
122
+ Shipped: streaming with markdown rendering, multiple chat sessions per project (resume picker with search), planning, slash commands, the goal loop and auto-verify, in-agent `deploy_0g_chain` and `upload_0g_storage` (mainnet-verified, opt-in), the private verifiable session (`z0g share`, wallet-encrypted to 0G Storage + `--anchor` on 0G Chain, and `z0g pull` to fetch, verify, and decrypt, mainnet-verified), the session INFT (`z0g mint`, mainnet-verified), parallel subagents, media on 0G (image + transcription), MCP both ways, the model catalog and arrow-key picker, user skills, and a published npm package (`npm i -g z0gcode`).
121
123
 
122
124
  Next:
123
125
  - **Share a session, three ways.** Today `z0g share` is a *private* backup: the bundle is encrypted to your wallet, so only you can `pull` it. The plan keeps that and adds two ways to actually hand a session to someone, without ever trusting the storage layer with a key:
@@ -128,7 +130,7 @@ Next:
128
130
  Together these are the on-ramp to full ERC-7857 authorized users.
129
131
  - Full ERC-7857 (encrypted metadata + oracle transfer) on top of the current session INFT.
130
132
  - Full TEE-quote verification of the provenance manifest (not just model + response id).
131
- - Publish `z0gcode` to npm; a shareable starter pack of user skills.
133
+ - A shareable starter pack of user skills.
132
134
 
133
135
  ## Development
134
136
 
package/bin/z0g.mjs CHANGED
@@ -19,7 +19,7 @@ import {
19
19
  } from "../src/sessions.mjs";
20
20
  import { loadPlan } from "../src/plan.mjs";
21
21
  import { loadMcp } from "../src/mcp.mjs";
22
- import { saveSetting } from "../src/settings.mjs";
22
+ import { saveSetting, loadSettings } from "../src/settings.mjs";
23
23
  import { fetchModels, orderChatModels } from "../src/models-info.mjs";
24
24
  import { discoverSkills, setSkillEnabled } from "../src/user-skills.mjs";
25
25
  import { generateImage, transcribeAudio } from "../src/media.mjs";
@@ -54,7 +54,7 @@ function helpText() {
54
54
  ["z0g serve --mcp", "Expose z0gcode's 0G tools as an MCP server"],
55
55
  ]],
56
56
  ["Options", [
57
- ["--auto", "Allow shell commands (run_bash)"],
57
+ ["--auto", "Start in auto mode (run commands without asking; else /mode ask prompts)"],
58
58
  ["--onchain", "Allow gas-spending on-chain actions (off by default)"],
59
59
  ["--continue", "Continue the saved session in this directory"],
60
60
  ["--model <id>", "Override the model (default " + CONFIG.model + ")"],
@@ -89,6 +89,7 @@ const SLASH_COMMANDS = [
89
89
  ["/rename", "Rename the current chat (/rename <title>)"],
90
90
  ["/init", "Analyze the project and write an AGENTS.md context file"],
91
91
  ["/model", "Pick the active 0G model (saved to settings)"],
92
+ ["/mode", "Permission mode: ask (approve each) | auto (run all) | plan (read-only)"],
92
93
  ["/effort", "Set reasoning effort (low|medium|high|default)"],
93
94
  ["/subagents", "Enable or disable parallel subagents (on|off)"],
94
95
  ["/onchain", "Enable or disable gas-spending on-chain actions (on|off)"],
@@ -761,6 +762,9 @@ async function repl(flags) {
761
762
  let effort = flags.effort;
762
763
  let subagents = flags.subagents;
763
764
  let onchain = flags.onchain;
765
+ // Permission mode: auto (run all) | ask (approve each) | plan (read-only).
766
+ let mode = flags.auto ? "auto" : "ask";
767
+ const allowedCmds = new Set(loadSettings(cwd).allowedCommands || []);
764
768
  let sessTokens = { in: 0, out: 0 };
765
769
  let priceMap = null;
766
770
  fetchModels(client).then((all) => { priceMap = Object.fromEntries(all.map((m) => [m.id, m])); }).catch(() => {});
@@ -782,6 +786,24 @@ async function repl(flags) {
782
786
  const activeModel = () => model || CONFIG.model;
783
787
  const activeEffort = () => (effort === "" ? null : (effort || CONFIG.effort));
784
788
  const activeOnchain = () => (onchain !== undefined ? onchain : CONFIG.onchain);
789
+ // Approve a gated action in "ask" mode. Remembers "always" choices in settings
790
+ // so it never asks again for that program (or for on-chain actions).
791
+ const approve = async (kind, desc) => {
792
+ const key = kind === "run_bash" ? "bash:" + (String(desc).trim().split(/\s+/)[0] || "?") : "@onchain";
793
+ if (allowedCmds.has(key)) return true;
794
+ rl.pause();
795
+ const label = kind === "run_bash" ? ui.strong(String(desc).slice(0, 80)) : "on-chain: " + desc;
796
+ console.log("\n " + ui.warn(ui.uiTTY ? "▲" : "!") + " " + (kind === "run_bash" ? "run " : "") + label);
797
+ const a = (await ask(" allow? " + ui.accent("y") + "es " + ui.accent("n") + "o " + ui.accent("a") + "lways ")).trim().toLowerCase();
798
+ rl.resume();
799
+ if (a === "a" || a === "always") {
800
+ allowedCmds.add(key);
801
+ saveSetting("allowedCommands", [...allowedCmds]);
802
+ ui.info("saved: won't ask again for " + (kind === "run_bash" ? key.slice(5) : "on-chain actions"));
803
+ return true;
804
+ }
805
+ return a === "y" || a === "yes";
806
+ };
785
807
  const costOf = () => {
786
808
  const m = priceMap && priceMap[activeModel()];
787
809
  if (!m || m.inPerM == null) return null;
@@ -799,13 +821,14 @@ async function repl(flags) {
799
821
  // Run one agent turn: hooks, the agent, then persist history + tokens.
800
822
  const runTurn = async (task) => {
801
823
  await runHooks(cwd, "preRun", hooks, flags.auto, task);
802
- const res = await runAgent({ client, task, cwd, sessionDir: sessionDirPath, allowBash: flags.auto, preferredModel: model, preferredEffort: effort, preferredSubagents: subagents, preferredOnchain: activeOnchain(), history, mcp });
824
+ const res = await runAgent({ client, task, cwd, sessionDir: sessionDirPath, preferredMode: mode, approve, preferredModel: model, preferredEffort: effort, preferredSubagents: subagents, preferredOnchain: activeOnchain(), history, mcp });
803
825
  history = res.messages;
804
826
  if (res.usageTotal) { sessTokens.in += res.usageTotal.prompt || 0; sessTokens.out += res.usageTotal.completion || 0; }
805
827
  await saveMessages(cwd, sessionId, history);
806
828
  await runHooks(cwd, "postRun", hooks, flags.auto, task);
807
829
  };
808
830
  ui.info("Interactive session. Type a task, or / then Tab for commands.");
831
+ ui.info("mode: " + ui.strong(mode) + (mode === "ask" ? " (I ask before running commands)" : mode === "plan" ? " (read-only until you /mode auto)" : " (runs commands without asking)") + " · /mode ask|auto|plan");
809
832
  if (customCmds.length) ui.info(customCmds.length + " custom command(s): " + customCmds.map((c) => "/" + c.name).join(", "));
810
833
  if (hasHooks(hooks) && !flags.auto) ui.info("hooks are configured; run with --auto to enable them");
811
834
  // Blinking block cursor at the prompt (matches the demo); restore on any exit.
@@ -903,6 +926,18 @@ async function repl(flags) {
903
926
  console.log(ui.warn("usage: /onchain on|off"));
904
927
  }
905
928
  }
929
+ else if (cmd === "mode") {
930
+ const a = arg.toLowerCase().trim();
931
+ if (["ask", "auto", "plan"].includes(a)) {
932
+ mode = a;
933
+ const desc = a === "auto" ? "runs commands without asking" : a === "plan" ? "read-only: explores and plans, no writes or commands" : "asks before each command / on-chain action";
934
+ ui.info("mode: " + ui.strong(a) + " · " + desc);
935
+ } else if (!a) {
936
+ ui.info("mode: " + ui.strong(mode) + " · usage: /mode ask|auto|plan" + (allowedCmds.size ? " · " + allowedCmds.size + " always-allowed" : ""));
937
+ } else {
938
+ console.log(ui.warn("usage: /mode ask|auto|plan"));
939
+ }
940
+ }
906
941
  else if (cmd === "skills") cmdSkills(cwd, arg);
907
942
  else if (cmd === "chats") {
908
943
  if (Array.isArray(history) && history.length) await saveMessages(cwd, sessionId, history);
@@ -967,7 +1002,7 @@ async function repl(flags) {
967
1002
  else { console.log(ui.section("Custom commands", customCmds.length + " loaded")); for (const c of customCmds) console.log(" " + ui.accent("/" + c.name) + " " + ui.muted(c.description)); }
968
1003
  }
969
1004
  else if (cmd === "goal") {
970
- await runGoal({ client, objective: arg, cwd, sessionId, sessionDir: sessionDirPath, allowBash: flags.auto, preferredModel: model, preferredEffort: effort, preferredSubagents: subagents, preferredOnchain: activeOnchain(), verifyCmd: flags.verify || detectVerifyCmd(cwd), maxIters: 3, history });
1005
+ await runGoal({ client, objective: arg, cwd, sessionId, sessionDir: sessionDirPath, preferredMode: mode, approve, preferredModel: model, preferredEffort: effort, preferredSubagents: subagents, preferredOnchain: activeOnchain(), verifyCmd: flags.verify || detectVerifyCmd(cwd), maxIters: 3, history });
971
1006
  history = await readMessages(cwd, sessionId) || history;
972
1007
  }
973
1008
  else if (customMap[cmd]) {
@@ -994,7 +1029,7 @@ async function main() {
994
1029
  if (flags.version) {
995
1030
  let v = "";
996
1031
  try { v = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version; } catch {}
997
- console.log("z0gcode " + (v || "0.2.0"));
1032
+ console.log("z0gcode " + (v || "0.3.0"));
998
1033
  return;
999
1034
  }
1000
1035
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "z0gcode",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "A terminal coding agent whose brain runs on 0G Compute: private, verifiable AI with on-chain provenance.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/agent.mjs CHANGED
@@ -57,22 +57,30 @@ function parseArgs(raw) {
57
57
  }
58
58
  }
59
59
 
60
- export async function runAgent({ client, task, cwd, sessionDir, allowBash, preferredModel, preferredEffort, preferredSubagents, preferredOnchain, onModel, history, mcp, quiet = false, toolNames = null, isSubagent = false }) {
60
+ export async function runAgent({ client, task, cwd, sessionDir, allowBash, preferredMode, approve, preferredModel, preferredEffort, preferredSubagents, preferredOnchain, onModel, history, mcp, quiet = false, toolNames = null, isSubagent = false }) {
61
61
  const q = !!quiet;
62
62
  // "" means an explicit unset (use the model's own default); undefined falls back.
63
63
  const effort = preferredEffort === "" ? null : (preferredEffort || CONFIG.effort);
64
64
  const subOn = preferredSubagents !== undefined ? preferredSubagents : CONFIG.subagents;
65
65
  const onchainOn = preferredOnchain !== undefined ? preferredOnchain : CONFIG.onchain;
66
66
  const provDir = sessionDir || path.join(cwd, ".z0g");
67
- const execute = makeExecutor({ cwd, allowBash, sessionDir: provDir, onchain: onchainOn });
67
+ // Permission mode: auto | ask | plan. Legacy: allowBash -> auto, else ask.
68
+ const mode = preferredMode || (allowBash ? "auto" : "ask");
69
+ const canBash = mode !== "plan"; // shell available in auto/ask (gated at exec)
70
+ const execute = makeExecutor({ cwd, allowBash, sessionDir: provDir, onchain: onchainOn, mode, approve });
68
71
  // Restrict the toolset for subagents (read-only), and drop spawn_subagents when
69
72
  // it is a subagent (no recursion) or the toggle is off. Drop on-chain tools when
70
73
  // the on-chain toggle is off so the agent never proposes a gas-spending action.
71
74
  let baseTools = toolNames ? TOOL_DEFS.filter((t) => toolNames.includes(t.function.name)) : TOOL_DEFS;
72
75
  if (isSubagent || !subOn) baseTools = baseTools.filter((t) => t.function.name !== "spawn_subagents" && t.function.name !== "spawn_write_subagents");
73
- // Write subagents edit files and run shell in worktrees, so require --auto.
74
- if (!allowBash) baseTools = baseTools.filter((t) => t.function.name !== "spawn_write_subagents");
76
+ // Write subagents edit files and run shell in worktrees, so they need shell.
77
+ if (!canBash) baseTools = baseTools.filter((t) => t.function.name !== "spawn_write_subagents");
75
78
  if (!onchainOn) baseTools = baseTools.filter((t) => t.function.name !== "upload_0g_storage" && t.function.name !== "deploy_0g_chain");
79
+ // Plan mode is read-only: drop every acting tool so the agent explores and plans.
80
+ if (mode === "plan") {
81
+ const PLAN_TOOLS = new Set(["read_file", "search_files", "list_dir", "read_skill", "update_plan", "spawn_subagents"]);
82
+ baseTools = baseTools.filter((t) => PLAN_TOOLS.has(t.function.name));
83
+ }
76
84
  const toolSet = !isSubagent && mcp?.tools?.length ? [...baseTools, ...mcp.tools] : baseTools;
77
85
  const models = modelChain(preferredModel);
78
86
  const prov = makeProvenance(provDir);
package/src/goal.mjs CHANGED
@@ -14,12 +14,12 @@ function runCmd(cmd, cwd) {
14
14
  });
15
15
  }
16
16
 
17
- export async function runGoal({ client, objective, cwd, sessionId, sessionDir, allowBash, preferredModel, preferredEffort, preferredSubagents, preferredOnchain, verifyCmd, maxIters = 3, history: historyParam = null }) {
17
+ export async function runGoal({ client, objective, cwd, sessionId, sessionDir, allowBash, preferredMode, approve, preferredModel, preferredEffort, preferredSubagents, preferredOnchain, verifyCmd, maxIters = 3, history: historyParam = null }) {
18
18
  let history = historyParam ?? null;
19
19
  let task = objective;
20
20
  for (let iter = 1; iter <= maxIters; iter++) {
21
21
  console.log(ui.section("Goal", "iteration " + iter + "/" + maxIters));
22
- const res = await runAgent({ client, task, cwd, sessionDir, allowBash, preferredModel, preferredEffort, preferredSubagents, preferredOnchain, history });
22
+ const res = await runAgent({ client, task, cwd, sessionDir, allowBash, preferredMode, approve, preferredModel, preferredEffort, preferredSubagents, preferredOnchain, history });
23
23
  history = res.messages;
24
24
  if (sessionId && Array.isArray(history)) {
25
25
  try { await saveMessages(cwd, sessionId, history); } catch {}
package/src/tools.mjs CHANGED
@@ -247,8 +247,20 @@ function safeResolve(cwd, p) {
247
247
  return abs;
248
248
  }
249
249
 
250
- export function makeExecutor({ cwd, allowBash, sessionDir, onchain = false }) {
250
+ export function makeExecutor({ cwd, allowBash, sessionDir, onchain = false, mode, approve }) {
251
251
  const planDir = sessionDir || path.join(cwd, ".z0g");
252
+ // Permission mode: "auto" runs everything, "ask" prompts via approve(), "plan"
253
+ // is read-only. Legacy callers pass allowBash -> auto/ask.
254
+ const effMode = mode || (allowBash ? "auto" : "ask");
255
+ const readOnly = effMode === "plan";
256
+ const planDeny = (what) => ({ ok: false, summary: "plan mode", content: `Read-only plan mode: I can explore and plan but will not ${what}. Switch with /mode auto or /mode ask to let me act.` });
257
+ // Approve a gated action (run_bash / on-chain). Returns true if allowed.
258
+ const permit = async (kind, desc) => {
259
+ if (readOnly) return false;
260
+ if (effMode === "auto") return true;
261
+ if (typeof approve === "function") return !!(await approve(kind, desc));
262
+ return false;
263
+ };
252
264
  return async function execute(name, args) {
253
265
  try {
254
266
  switch (name) {
@@ -263,6 +275,7 @@ export function makeExecutor({ cwd, allowBash, sessionDir, onchain = false }) {
263
275
  return { ok: true, summary: `search "${args.query}" (${res.count})`, content: res.text };
264
276
  }
265
277
  case "write_file": {
278
+ if (readOnly) return planDeny("write files");
266
279
  const abs = safeResolve(cwd, args.path);
267
280
  let before = "", existed = true;
268
281
  try { before = await fs.readFile(abs, "utf8"); } catch { before = ""; existed = false; }
@@ -272,6 +285,7 @@ export function makeExecutor({ cwd, allowBash, sessionDir, onchain = false }) {
272
285
  return { ok: true, summary: `wrote ${args.path} (${after.length} bytes)`, content: "OK", change: { path: args.path, before, after, created: !existed } };
273
286
  }
274
287
  case "edit_file": {
288
+ if (readOnly) return planDeny("edit files");
275
289
  const abs = safeResolve(cwd, args.path);
276
290
  const cur = await fs.readFile(abs, "utf8");
277
291
  if (!args.old_string || !cur.includes(args.old_string)) {
@@ -291,16 +305,23 @@ export function makeExecutor({ cwd, allowBash, sessionDir, onchain = false }) {
291
305
  return { ok: true, summary: `list ${args.path || "."} (${lines.length})`, content: lines.join("\n") || "(empty)" };
292
306
  }
293
307
  case "run_bash": {
294
- if (!allowBash) {
295
- return { ok: false, summary: "bash denied", content: "run_bash is disabled. Re-run z0gcode with --auto to allow shell commands." };
308
+ if (readOnly) return planDeny("run commands");
309
+ if (!(await permit("run_bash", args.command))) {
310
+ return { ok: false, summary: "bash not allowed", content: effMode === "ask" ? "You declined this command." : "run_bash needs approval: run z0g interactively (mode ask) and approve it, or with --auto." };
296
311
  }
297
312
  const out = await runBash(args.command, cwd);
298
313
  return { ok: out.code === 0, summary: `bash exit ${out.code}`, content: out.text };
299
314
  }
300
315
  case "upload_0g_storage": {
316
+ if (onchain && !(await permit("on-chain", "upload to 0G Storage"))) {
317
+ return { ok: false, summary: "on-chain not allowed", content: readOnly ? "Read-only plan mode." : "You declined the on-chain action." };
318
+ }
301
319
  return await uploadToStorage(cwd, args.path, onchain);
302
320
  }
303
321
  case "deploy_0g_chain": {
322
+ if (onchain && !(await permit("on-chain", "deploy to 0G Chain"))) {
323
+ return { ok: false, summary: "on-chain not allowed", content: readOnly ? "Read-only plan mode." : "You declined the on-chain action." };
324
+ }
304
325
  return await deployToChain(cwd, args, onchain);
305
326
  }
306
327
  case "update_plan": {