symbiot 0.51.0 → 0.51.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/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  What each Symbiot release brought, newest first. When Approve bumps the version, it writes that release here from the tasks it approved, worded as release notes by your connected AI (or in the tasks' own words without one). The app shows the releases newer than yours under "What's new" when it offers an update, and once more after you update.
4
4
 
5
+ ## 0.51.1
6
+
7
+ - **Allowing an agent's list works when the list is fine.** An exact command such as `rm -r ~/dailify/site` no longer counts as dangerous; only Bash with no command, a shell, sudo, or a wildcard `rm`/`dd`/`chmod` does. Before, one such rule refused the whole list, silently, and the agent asked you again and again.
8
+ - **When a list is too wide, you're told why.** The note names the rules, the agent is asked for a narrower list, and it starts again, instead of waiting on a click that can't work.
9
+ - **An agent can never edit Symbiot's own settings.** Every list Symbiot turns on also blocks editing `config.json`, where what agents may do is kept.
10
+
5
11
  ## 0.51.0
6
12
 
7
13
  What your agents built on the symbiot repo yesterday, merged with everything since:
package/agents.mjs CHANGED
@@ -625,23 +625,32 @@ function agentQuestions(path, repo) {
625
625
  // An allow list an agent proposed (.symbiot/allowlist.proposed.json) is turned on for
626
626
  // that folder only (.claude/settings.local.json, merged), unless it would let an agent
627
627
  // run anything at all (a shell, sudo, rm, Bash(*)): then it stays the user's step.
628
- const WIDE_RULE = /^Bash(\((\*|sudo|su|rm|bash|sh|zsh|dash|dd|mkfs|chmod|chown|eval|exec)\b[^)]*\))?$|^Bash\(\*/i;
628
+ // A rule is wide when it lets an agent run anything: Bash with no command, a shell
629
+ // (bash, sh, eval…) or sudo in any form, or a destructive command (rm, dd, chmod…)
630
+ // with a wildcard. The same command spelled out exactly (rm -r ~/x/site) is narrow.
631
+ const ANY_RULE = /^Bash(\(\s*\*.*\))?$|^Bash\(\s*(sudo|su|bash|sh|zsh|dash|fish|eval|exec|env|xargs)\b/i;
632
+ const HARM_RULE = /^Bash\(\s*(rm|dd|mkfs|chmod|chown|shred|truncate)\b/i;
633
+ const wideRule = (r) => ANY_RULE.test(r) || (HARM_RULE.test(r) && /\*/.test(r));
634
+ // Whatever an agent is allowed, it never edits Symbiot's own settings: that's where
635
+ // what agents may do is kept, so an agent could widen its own permissions there.
636
+ const GUARD = () => { const c = join(homedir(), ".config", "symbiot", "config.json"); return [`Edit(${c})`, `Write(${c})`]; };
629
637
  function installAllowlist(path) {
630
638
  let p; try { p = JSON.parse(readFileSync(join(path, ".symbiot", "allowlist.proposed.json"), "utf8")).permissions || {}; } catch { return null; }
631
639
  const list = (x) => (Array.isArray(x) ? x.filter((r) => typeof r === "string" && r.trim()).map((r) => r.trim()) : []);
632
640
  const allow = list(p.allow), deny = list(p.deny), dirs = list(p.additionalDirectories);
633
641
  if (!allow.length && !dirs.length) return null;
634
- if (allow.some((r) => WIDE_RULE.test(r))) return null;
642
+ const wide = allow.filter(wideRule);
643
+ if (wide.length) return { refused: wide };
635
644
  const f = join(path, ".claude", "settings.local.json");
636
645
  let cur = {}; try { cur = JSON.parse(readFileSync(f, "utf8")); } catch {}
637
646
  const perm = cur.permissions || {}, merge = (a, b) => [...new Set([...list(a), ...b])];
638
- cur.permissions = { ...perm, allow: merge(perm.allow, allow), deny: merge(perm.deny, deny), additionalDirectories: merge(perm.additionalDirectories, dirs) };
647
+ cur.permissions = { ...perm, allow: merge(perm.allow, allow), deny: merge(perm.deny, [...deny, ...GUARD()]), additionalDirectories: merge(perm.additionalDirectories, dirs) };
639
648
  try { mkdirSync(join(path, ".claude"), { recursive: true }); writeFileSync(f, JSON.stringify(cur, null, 2) + "\n", { mode: 0o600 }); } catch { return null; }
640
649
  return [`the allow list for this folder (${allow.length} rule${allow.length === 1 ? "" : "s"})`];
641
650
  }
642
- function grantFromStep(step, path = "") {
651
+ function grantFromStep(step, path = "", notes = {}) {
643
652
  const t = String(step || "");
644
- if (path && /allowlist\.proposed\.json|settings\.local\.json|press allow|\ballow list\b/i.test(t)) { const g = installAllowlist(path); if (g) return g; }
653
+ if (path && /allowlist\.proposed\.json|settings\.local\.json|press allow|\ballow list\b/i.test(t)) { const g = installAllowlist(path); if (g && g.refused) { notes.refused = g.refused; return null; } if (g) return g; }
645
654
  if (!/only you: a permission|only you: what (this|the) agent|\b(let|allow) (the )?agents?\b|\ballow (access|reading)\b/i.test(t)) return null;
646
655
  const got = [];
647
656
  const dir = (t.match(/(~\/[^\s,;)`'"]+|\/(?:home|Users|opt|srv|mnt|media|tmp)\/[^\s,;)`'"]+)/) || [])[1];
@@ -671,9 +680,18 @@ function answerQuestions(path, answers, opts = {}) {
671
680
  const youOpts = (q) => ((asked.get(qKey(q)) || {}).options || []).filter(yourStep).map((o) => o.split("🤖")[0]);
672
681
  const steps = rows.filter((x) => yourStep(x.a) || holdAnswer(x.a)).map((x) => yourStep(x.a) ? { step: stepText(x.a), named: x.a.split("🤖")[0] }
673
682
  : { step: (youOpts(x.q).map(stepText)[0] || x.a).trim(), named: [x.a, ...youOpts(x.q)].join(" ") });
674
- const granted = [];
675
- const yours = steps.map((s) => s.step).filter(Boolean).filter((st) => { const g = grantFromStep(st, path); if (g) granted.push(...g); return !g; });
683
+ const granted = [], notes = {};
684
+ let yours = steps.map((s) => s.step).filter(Boolean).filter((st) => { const g = grantFromStep(st, path, notes); if (g) granted.push(...g); return !g; });
676
685
  if (granted.length) out.granted = granted;
686
+ // A list too wide to turn on by a click: say which rules, tell the agent to ask for
687
+ // less, and don't leave it as the user's step (picking it again would do nothing).
688
+ if (notes.refused) {
689
+ const which = notes.refused.slice(0, 3).map((r) => "`" + r + "`").join(", ");
690
+ try { writeFileSync(join(path, ".symbiot", "ANSWERS.md"), readSymbiot(path, "ANSWERS.md").replace(/\s*$/, "\n") + `\n### Symbiot didn't turn your allow list on\nIt has ${which}, which would let you run anything (a shell, sudo, or a wildcard rm/dd/chmod). Write a narrower .symbiot/allowlist.proposed.json: each such command spelled out exactly, the paths it touches named, and ask again.\n_${day}_\n`); } catch {}
691
+ out.refused = notes.refused;
692
+ yours = yours.filter((st) => !/allowlist\.proposed\.json|settings\.local\.json|press allow|\ballow list\b/i.test(st));
693
+ out.note = `Not turned on: the list has ${which}, which would let an agent run anything. Your agent has been asked for a narrower one` + (opts.rerun ? " and is starting again." : ".");
694
+ }
677
695
  if (yours.length) {
678
696
  const step = yours.join(" "), files = namedFiles(steps.map((s) => s.named).join(" "), path);
679
697
  try { writeFileSync(join(path, ".symbiot", WAITING), JSON.stringify({ step, files: files.map((f) => ({ ...f, sig: fileSig(f.path) })), cmd: runCmd(path), at: Date.now(), rerun: !!opts.rerun })); } catch {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "symbiot",
3
- "version": "0.51.0",
3
+ "version": "0.51.1",
4
4
  "description": "Your week, written from your real work. Reads your local git and writes it up with the AI of your choice — Claude, OpenAI, Gemini, or a local model.",
5
5
  "type": "module",
6
6
  "bin": {