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 +6 -0
- package/agents.mjs +25 -7
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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": {
|