@antonbabenko/deliberation-mcp 3.5.2 → 3.5.4

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.
Files changed (2) hide show
  1. package/dist/index.js +141 -14
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1591,16 +1591,17 @@ var require_antigravity = __commonJS({
1591
1591
  },
1592
1592
  async ask(req) {
1593
1593
  const started = Date.now();
1594
+ const includeDirs = (req.files || []).filter((f) => f.dir).map((f) => f.dir);
1594
1595
  const args = bridge.buildAgyArgs({
1595
1596
  prompt: req.prompt,
1596
1597
  model,
1597
1598
  sandbox: "read-only",
1598
1599
  developerInstructions: req.developerInstructions,
1599
- includeDirs: (req.files || []).filter((f) => f.dir).map((f) => f.dir)
1600
+ includeDirs
1600
1601
  });
1601
1602
  try {
1602
- const out = await bridge.runGemini(args, req.cwd, req.timeoutMs, void 0);
1603
- return { provider: "gemini", model, text: out.response || "", threadId: out.threadId, isError: false, ms: Date.now() - started, reasoningEffort: null };
1603
+ const out = await bridge.runGemini(args, req.cwd, req.timeoutMs, void 0, { readOnly: true, includeDirs });
1604
+ return { provider: "gemini", model, text: out.response || "", threadId: out.threadId, isError: false, ms: Date.now() - started, reasoningEffort: null, ...out.workspaceMutated ? { workspaceMutated: true } : {} };
1604
1605
  } catch (e) {
1605
1606
  const err = (
1606
1607
  /** @type {any} */
@@ -1634,9 +1635,12 @@ var require_codex = __commonJS({
1634
1635
  if (s.includes("rate")) return { errorKind: "rate-limit", retryable: true };
1635
1636
  return { errorKind: "unknown", retryable: false };
1636
1637
  }
1638
+ function codexExecArgs() {
1639
+ return ["exec", "--sandbox", "read-only", "--skip-git-repo-check"];
1640
+ }
1637
1641
  function defaultRun({ prompt, cwd, timeoutMs }) {
1638
1642
  return new Promise((resolve) => {
1639
- const child = spawn("codex", ["exec", "--skip-git-repo-check"], { cwd: cwd || process.cwd() });
1643
+ const child = spawn("codex", codexExecArgs(), { cwd: cwd || process.cwd() });
1640
1644
  let stdout = "", stderr = "", settled = false;
1641
1645
  const timer = timeoutMs ? setTimeout(() => child.kill("SIGKILL"), timeoutMs) : null;
1642
1646
  if (timer) timer.unref();
@@ -1693,7 +1697,7 @@ ${req.prompt}` : req.prompt;
1693
1697
  }
1694
1698
  };
1695
1699
  }
1696
- module2.exports = { makeCodexProvider, classifyCodex };
1700
+ module2.exports = { makeCodexProvider, classifyCodex, codexExecArgs };
1697
1701
  }
1698
1702
  });
1699
1703
 
@@ -2085,18 +2089,102 @@ var require_gemini = __commonJS({
2085
2089
  function goDuration(ms) {
2086
2090
  return Math.ceil(ms / 1e3) + "s";
2087
2091
  }
2092
+ var READ_ONLY_GUARD = "[ADVISORY MODE - READ-ONLY] You are being consulted for an advisory second opinion. Do NOT modify, create, or delete any files. Do NOT run commands that write to the workspace. Do NOT git add, commit, or push. Any such action is a critical failure - respond with analysis only.";
2093
+ function applyReadOnlyGuard(prompt, readOnly) {
2094
+ return readOnly ? `${READ_ONLY_GUARD}
2095
+
2096
+ ${prompt}` : prompt;
2097
+ }
2088
2098
  function buildAgyArgs(req) {
2089
2099
  const args = [];
2100
+ const readOnly = req.sandbox !== "workspace-write";
2090
2101
  if (req.sandbox === "workspace-write") args.push("--dangerously-skip-permissions");
2091
2102
  else args.push("--sandbox");
2092
2103
  for (const d of req.includeDirs || []) args.push("--add-dir", d);
2093
- let prompt = req.prompt;
2104
+ let prompt = applyReadOnlyGuard(req.prompt, readOnly);
2094
2105
  if (req.developerInstructions) prompt = `${req.developerInstructions}
2095
2106
 
2096
2107
  ${prompt}`;
2097
2108
  args.push("-p", prompt);
2098
2109
  return args;
2099
2110
  }
2111
+ var ADVISORY_ENV_SCRUB = ["GITHUB_TOKEN", "GH_TOKEN", "GIT_ASKPASS", "SSH_AUTH_SOCK"];
2112
+ function advisoryEnv(env) {
2113
+ const out = { ...env };
2114
+ delete out.DELIBERATION_DISABLE_OS_SANDBOX;
2115
+ for (const k of ADVISORY_ENV_SCRUB) delete out[k];
2116
+ return out;
2117
+ }
2118
+ function seatbeltLiteral(p) {
2119
+ return String(p).replace(/\\/g, "\\\\").replace(/"/g, '\\"');
2120
+ }
2121
+ function buildSeatbeltProfile(o) {
2122
+ const gem = seatbeltLiteral(path.join(o.home, ".gemini"));
2123
+ const tmp = seatbeltLiteral(o.tmpdir);
2124
+ return [
2125
+ "(version 1)",
2126
+ "(allow default)",
2127
+ "(deny file-write*)",
2128
+ "(allow file-write*",
2129
+ ` (subpath "${gem}")`,
2130
+ ` (subpath "${tmp}")`,
2131
+ ' (subpath "/private/var/folders")',
2132
+ ' (subpath "/private/tmp")',
2133
+ ' (literal "/dev/null")',
2134
+ ' (literal "/dev/dtracehelper")',
2135
+ ' (regex #"^/dev/tty"))'
2136
+ ].join("\n");
2137
+ }
2138
+ function buildSpawnCommand(o) {
2139
+ const platform = o.platform || process.platform;
2140
+ const unwrapped = { cmd: o.bin, argv: o.args, osSandbox: false };
2141
+ if (!o.readOnly || o.disabled === true || platform !== "darwin" || !o.sandboxExecPath) {
2142
+ return unwrapped;
2143
+ }
2144
+ const profile = buildSeatbeltProfile({ home: o.home, tmpdir: o.tmpdir });
2145
+ return { cmd: o.sandboxExecPath, argv: ["-p", profile, o.bin, ...o.args], osSandbox: true };
2146
+ }
2147
+ function resolveSandboxExecPath() {
2148
+ try {
2149
+ fs.accessSync("/usr/bin/sandbox-exec", fs.constants.X_OK);
2150
+ return "/usr/bin/sandbox-exec";
2151
+ } catch (_) {
2152
+ return null;
2153
+ }
2154
+ }
2155
+ function captureGitState(cwd) {
2156
+ const opts = { cwd, encoding: "utf8", timeout: 1e4, stdio: ["ignore", "pipe", "ignore"] };
2157
+ try {
2158
+ const head = execFileSync("git", ["rev-parse", "HEAD"], opts).trim();
2159
+ const status = execFileSync("git", ["status", "--porcelain"], opts);
2160
+ return { head, status };
2161
+ } catch (_) {
2162
+ return null;
2163
+ }
2164
+ }
2165
+ function diffGitState(pre, post) {
2166
+ if (!pre || !post) return false;
2167
+ return pre.head !== post.head || pre.status !== post.status;
2168
+ }
2169
+ function captureRoots(dirs) {
2170
+ const seen = /* @__PURE__ */ new Set();
2171
+ const states = [];
2172
+ for (const d of dirs) {
2173
+ if (!d || seen.has(d)) continue;
2174
+ seen.add(d);
2175
+ states.push([d, captureGitState(d)]);
2176
+ }
2177
+ return states;
2178
+ }
2179
+ function rootsMutated(preStates) {
2180
+ for (const [dir, pre] of preStates) {
2181
+ if (diffGitState(pre, captureGitState(dir))) return true;
2182
+ }
2183
+ return false;
2184
+ }
2185
+ function workspaceMutationWarning(dir) {
2186
+ return "WORKSPACE MUTATION DETECTED\nThis advisory (read-only) run changed the workspace at " + dir + " (git HEAD and/or working-tree status changed during the run). Review `git status` / `git log` before continuing - nothing was auto-reverted, and the result below should be treated as tainted.\n---\n";
2187
+ }
2100
2188
  function sendResponse(id, result) {
2101
2189
  process.stdout.write(JSON.stringify({
2102
2190
  jsonrpc: "2.0",
@@ -2150,11 +2238,13 @@ ${prompt}`;
2150
2238
  return null;
2151
2239
  }
2152
2240
  }
2153
- async function runGemini(args, cwd, timeoutMs, recoveryGraceMs) {
2241
+ async function runGemini(args, cwd, timeoutMs, recoveryGraceMs, opts = {}) {
2154
2242
  return new Promise((resolve, reject) => {
2155
2243
  const t = typeof timeoutMs === "number" && timeoutMs > 0 ? timeoutMs : DEFAULT_TIMEOUT_MS;
2156
2244
  const effCwd = cwd || process.cwd();
2157
2245
  const spawnStartMs = Date.now();
2246
+ const readOnly = opts.readOnly === true;
2247
+ const preRoots = readOnly ? captureRoots([effCwd, ...opts.includeDirs || []]) : [];
2158
2248
  const disableRecovery = process.env.GEMINI_DISABLE_TIMEOUT_RECOVERY === "1";
2159
2249
  const grace = disableRecovery ? 0 : typeof recoveryGraceMs === "number" && recoveryGraceMs >= 0 ? recoveryGraceMs : DEFAULT_RECOVERY_GRACE_MS;
2160
2250
  let killed = false;
@@ -2165,8 +2255,27 @@ ${prompt}`;
2165
2255
  const head = ptIdx >= 0 ? args.slice(0, ptIdx) : args;
2166
2256
  const tail = ptIdx >= 0 ? args.slice(ptIdx) : [];
2167
2257
  const agyArgs = [...head, "--print-timeout", goDuration(t + grace + 3e4), ...tail];
2168
- const agyProcess = spawn(AGY_BIN, agyArgs, {
2169
- env: process.env,
2258
+ const spawnPlan = buildSpawnCommand({
2259
+ bin: AGY_BIN,
2260
+ args: agyArgs,
2261
+ readOnly,
2262
+ platform: process.platform,
2263
+ home: os.homedir(),
2264
+ tmpdir: (() => {
2265
+ try {
2266
+ return fs.realpathSync(os.tmpdir());
2267
+ } catch (_) {
2268
+ return os.tmpdir();
2269
+ }
2270
+ })(),
2271
+ sandboxExecPath: readOnly ? resolveSandboxExecPath() : null,
2272
+ disabled: process.env.DELIBERATION_DISABLE_OS_SANDBOX === "1"
2273
+ });
2274
+ if (spawnPlan.osSandbox) {
2275
+ process.stderr.write("[deliberation] agy read-only run wrapped in sandbox-exec (workspace writes denied)\n");
2276
+ }
2277
+ const agyProcess = spawn(spawnPlan.cmd, spawnPlan.argv, {
2278
+ env: readOnly ? advisoryEnv(process.env) : process.env,
2170
2279
  shell: false,
2171
2280
  cwd: effCwd,
2172
2281
  // agy -p (print mode) waits for stdin EOF before returning; if the stdin
@@ -2274,7 +2383,9 @@ ${prompt}`;
2274
2383
  process.stderr.write(
2275
2384
  "[deliberation] recovered agy answer via stdout drain after soft timeout (" + Math.round((Date.now() - spawnStartMs) / 1e3) + "s)\n"
2276
2385
  );
2277
- return resolve({ response: out, threadId: resolveConversationId(effCwd) || "unknown", recovered: true });
2386
+ const mutated = readOnly && rootsMutated(preRoots);
2387
+ const text = mutated ? workspaceMutationWarning(effCwd) + out : out;
2388
+ return resolve({ response: text, threadId: resolveConversationId(effCwd) || "unknown", recovered: true, ...mutated ? { workspaceMutated: true } : {} });
2278
2389
  }
2279
2390
  return finishTimeout();
2280
2391
  }
@@ -2287,7 +2398,9 @@ ${prompt}`;
2287
2398
  "[deliberation] no conversation id found for cwd " + effCwd + '; returning threadId:"unknown" (resume will be unavailable)\n'
2288
2399
  );
2289
2400
  }
2290
- return resolve({ response: out, threadId: threadId || "unknown" });
2401
+ const mutated = readOnly && rootsMutated(preRoots);
2402
+ const text = mutated ? workspaceMutationWarning(effCwd) + out : out;
2403
+ return resolve({ response: text, threadId: threadId || "unknown", ...mutated ? { workspaceMutated: true } : {} });
2291
2404
  }
2292
2405
  let message;
2293
2406
  if (trimmedErr) message = trimmedErr;
@@ -2454,19 +2567,27 @@ ${prompt}`;
2454
2567
  return;
2455
2568
  }
2456
2569
  agyArgs.push("--conversation", threadId2, ...sandboxFlags, ...addDirFlags);
2457
- agyArgs.push("-p", args.prompt);
2570
+ agyArgs.push("-p", applyReadOnlyGuard(args.prompt, args.sandbox !== "workspace-write"));
2458
2571
  } else {
2459
2572
  if (shouldRespond) sendError(id, -32601, `Tool not found: ${name}`);
2460
2573
  return;
2461
2574
  }
2462
2575
  const timeoutMs = typeof args.timeout === "number" && args.timeout > 0 ? args.timeout : DEFAULT_TIMEOUT_MS;
2463
2576
  const recoveryGraceMs = typeof args["recovery-grace"] === "number" && args["recovery-grace"] >= 0 ? args["recovery-grace"] : DEFAULT_RECOVERY_GRACE_MS;
2464
- const { response, threadId, recovered } = await runGemini(agyArgs, args.cwd, timeoutMs, recoveryGraceMs);
2577
+ const readOnly = args.sandbox !== "workspace-write";
2578
+ const { response, threadId, recovered, workspaceMutated } = await runGemini(
2579
+ agyArgs,
2580
+ args.cwd,
2581
+ timeoutMs,
2582
+ recoveryGraceMs,
2583
+ { readOnly, includeDirs: args["include-directories"] || [] }
2584
+ );
2465
2585
  if (shouldRespond) {
2466
2586
  sendResponse(id, {
2467
2587
  content: [{ type: "text", text: response }],
2468
2588
  threadId,
2469
- ...recovered ? { recovered: true } : {}
2589
+ ...recovered ? { recovered: true } : {},
2590
+ ...workspaceMutated ? { workspaceMutated: true } : {}
2470
2591
  });
2471
2592
  }
2472
2593
  } catch (e) {
@@ -2532,6 +2653,12 @@ ${prompt}`;
2532
2653
  module2.exports.stdoutIsError = stdoutIsError;
2533
2654
  module2.exports.runGemini = runGemini;
2534
2655
  module2.exports.buildAgyArgs = buildAgyArgs;
2656
+ module2.exports.READ_ONLY_GUARD = READ_ONLY_GUARD;
2657
+ module2.exports.applyReadOnlyGuard = applyReadOnlyGuard;
2658
+ module2.exports.advisoryEnv = advisoryEnv;
2659
+ module2.exports.buildSeatbeltProfile = buildSeatbeltProfile;
2660
+ module2.exports.buildSpawnCommand = buildSpawnCommand;
2661
+ module2.exports.diffGitState = diffGitState;
2535
2662
  }
2536
2663
  }
2537
2664
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antonbabenko/deliberation-mcp",
3
- "version": "3.5.2",
3
+ "version": "3.5.4",
4
4
  "description": "Deliberation for Claude Code and any MCP host - GPT, Gemini, Grok, and OpenRouter expert subagents.",
5
5
  "mcpName": "io.github.antonbabenko/deliberation",
6
6
  "repository": { "type": "git", "url": "git+https://github.com/antonbabenko/deliberation.git", "directory": "server/mcp" },