symbiot 0.50.1 → 0.50.2
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 +5 -0
- package/agents.mjs +23 -4
- package/handover.mjs +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
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.50.2
|
|
6
|
+
|
|
7
|
+
- **An allow list is one click.** When an agent needs permissions it doesn't have, it proposes them, as narrow as the task needs, in `.symbiot/allowlist.proposed.json` and offers it as a choice. Pick it in the app and Symbiot turns it on for that folder only and runs the agent again: no copying files, no running claude in a terminal. A list that would let an agent run anything at all (a shell, sudo, rm) is never turned on by a click.
|
|
8
|
+
- **Fixed:** a flag could end up in the agent command as a rule (`Bash(--allowedtools:*)`). It can't any more, and an old one is cleaned out on the next grant.
|
|
9
|
+
|
|
5
10
|
## 0.50.1
|
|
6
11
|
|
|
7
12
|
- **A permission you pick is given.** When an agent asks for one ("let agents read ~/.config/symbiot/screens") and you choose it in the app, Symbiot grants it then and there (as Settings' grant boxes do) and the agent carries on, instead of leaving it as a step for you to do by hand. Steps only you can do, like pasting your own message, are still yours.
|
package/agents.mjs
CHANGED
|
@@ -46,6 +46,7 @@ function setHandoffCmd(cmd) {
|
|
|
46
46
|
// looked bare and became Bash(Bashnpm install:*:*).
|
|
47
47
|
function grantRule(tool) {
|
|
48
48
|
let t = String(tool || "").trim().replace(/^["'`]+|["'`]+$/g, "").trim();
|
|
49
|
+
if (/^-/.test(t)) return ""; // a flag (--allowedTools) is never a tool
|
|
49
50
|
if (/^[A-Za-z]+\(.*\)$/.test(t)) return t;
|
|
50
51
|
t = t.replace(/[()"'`]/g, "").replace(/:\*$/, "").trim();
|
|
51
52
|
return t ? `Bash(${t}:*)` : "";
|
|
@@ -62,7 +63,7 @@ function grantAgent({ tool, dir } = {}) {
|
|
|
62
63
|
if (!isClaudeCmd(cmd)) return { error: "Grants apply to the Claude agent command. Pick a Claude preset first, or edit the command directly." };
|
|
63
64
|
if (tool) cmd = allowTool(cmd, grantRule(tool));
|
|
64
65
|
if (dir) { const d = String(dir).trim(); if (d && !cmd.includes(`--add-dir "${d}"`)) cmd += ` --add-dir "${d}"`; }
|
|
65
|
-
cmd = cmd.replace(/\s+/g, " ").trim();
|
|
66
|
+
cmd = cmd.replace(/\s*"Bash\(-[^"]*\)"/g, "").replace(/\s+/g, " ").trim(); // and drop any flag that got in as a rule before
|
|
66
67
|
cfg.agentCmd = cmd; saveConfig(cfg);
|
|
67
68
|
return { ok: true, cmd };
|
|
68
69
|
}
|
|
@@ -577,9 +578,27 @@ function agentQuestions(path, repo) {
|
|
|
577
578
|
// say-so. "let agents read ~/x" / "allow /x" becomes --add-dir, "let agents run
|
|
578
579
|
// \`tool\`" a grant for that tool (never sudo/rm), as Settings' grant boxes do. What's
|
|
579
580
|
// granted isn't a step left for the user. Returns what was granted, or null.
|
|
580
|
-
|
|
581
|
+
// An allow list an agent proposed (.symbiot/allowlist.proposed.json) is turned on for
|
|
582
|
+
// that folder only (.claude/settings.local.json, merged), unless it would let an agent
|
|
583
|
+
// run anything at all (a shell, sudo, rm, Bash(*)): then it stays the user's step.
|
|
584
|
+
const WIDE_RULE = /^Bash(\((\*|sudo|su|rm|bash|sh|zsh|dash|dd|mkfs|chmod|chown|eval|exec)\b[^)]*\))?$|^Bash\(\*/i;
|
|
585
|
+
function installAllowlist(path) {
|
|
586
|
+
let p; try { p = JSON.parse(readFileSync(join(path, ".symbiot", "allowlist.proposed.json"), "utf8")).permissions || {}; } catch { return null; }
|
|
587
|
+
const list = (x) => (Array.isArray(x) ? x.filter((r) => typeof r === "string" && r.trim()).map((r) => r.trim()) : []);
|
|
588
|
+
const allow = list(p.allow), deny = list(p.deny), dirs = list(p.additionalDirectories);
|
|
589
|
+
if (!allow.length && !dirs.length) return null;
|
|
590
|
+
if (allow.some((r) => WIDE_RULE.test(r))) return null;
|
|
591
|
+
const f = join(path, ".claude", "settings.local.json");
|
|
592
|
+
let cur = {}; try { cur = JSON.parse(readFileSync(f, "utf8")); } catch {}
|
|
593
|
+
const perm = cur.permissions || {}, merge = (a, b) => [...new Set([...list(a), ...b])];
|
|
594
|
+
cur.permissions = { ...perm, allow: merge(perm.allow, allow), deny: merge(perm.deny, deny), additionalDirectories: merge(perm.additionalDirectories, dirs) };
|
|
595
|
+
try { mkdirSync(join(path, ".claude"), { recursive: true }); writeFileSync(f, JSON.stringify(cur, null, 2) + "\n", { mode: 0o600 }); } catch { return null; }
|
|
596
|
+
return [`the allow list for this folder (${allow.length} rule${allow.length === 1 ? "" : "s"})`];
|
|
597
|
+
}
|
|
598
|
+
function grantFromStep(step, path = "") {
|
|
581
599
|
const t = String(step || "");
|
|
582
|
-
if (
|
|
600
|
+
if (path && /allowlist\.proposed\.json|settings\.local\.json|press allow|\ballow list\b/i.test(t)) { const g = installAllowlist(path); if (g) return g; }
|
|
601
|
+
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;
|
|
583
602
|
const got = [];
|
|
584
603
|
const dir = (t.match(/(~\/[^\s,;)`'"]+|\/(?:home|Users|opt|srv|mnt|media|tmp)\/[^\s,;)`'"]+)/) || [])[1];
|
|
585
604
|
if (dir) { const d = dir.replace(/^~(?=\/)/, homedir()).replace(/[.]$/, ""); const r = grantAgent({ dir: d }); if (r && r.ok) got.push(d); }
|
|
@@ -609,7 +628,7 @@ function answerQuestions(path, answers, opts = {}) {
|
|
|
609
628
|
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] }
|
|
610
629
|
: { step: (youOpts(x.q).map(stepText)[0] || x.a).trim(), named: [x.a, ...youOpts(x.q)].join(" ") });
|
|
611
630
|
const granted = [];
|
|
612
|
-
const yours = steps.map((s) => s.step).filter(Boolean).filter((st) => { const g = grantFromStep(st); if (g) granted.push(...g); return !g; });
|
|
631
|
+
const yours = steps.map((s) => s.step).filter(Boolean).filter((st) => { const g = grantFromStep(st, path); if (g) granted.push(...g); return !g; });
|
|
613
632
|
if (granted.length) out.granted = granted;
|
|
614
633
|
if (yours.length) {
|
|
615
634
|
const step = yours.join(" "), files = namedFiles(steps.map((s) => s.named).join(" "), path);
|
package/handover.mjs
CHANGED
|
@@ -21,7 +21,7 @@ function handoverRules(lanes = [], here = "") {
|
|
|
21
21
|
}
|
|
22
22
|
// The rule for options in QUESTIONS.md: the user gets only what no agent can do,
|
|
23
23
|
// and the option says which kind it is.
|
|
24
|
-
const ONLY_YOU = "- Only what no agent can do is the user's: their body (a phone in their hand, a cable, which network they're on), their identity or secrets (signing in, a 2FA code, a token from a provider's console) or a decision that's theirs (closing an account, spending money, sending something in their name). Start such an option with `👤 You (only you: <which>):`, e.g. `👤 You (only you: your Meta token): paste it into .env`. Anything an agent could do, yours or another lane's, is never a 👤 step: do it, or hand it over. An option where picking it is enough starts with `🤖 Agent:`. Tag actions only: an option that just reports what the user saw or decides (`it notified me`, `not tried yet`) gets no tag.";
|
|
24
|
+
const ONLY_YOU = "- Only what no agent can do is the user's: their body (a phone in their hand, a cable, which network they're on), their identity or secrets (signing in, a 2FA code, a token from a provider's console) or a decision that's theirs (closing an account, spending money, sending something in their name). Start such an option with `👤 You (only you: <which>):`, e.g. `👤 You (only you: your Meta token): paste it into .env`. Anything an agent could do, yours or another lane's, is never a 👤 step: do it, or hand it over. An option where picking it is enough starts with `🤖 Agent:`. Tag actions only: an option that just reports what the user saw or decides (`it notified me`, `not tried yet`) gets no tag. A permission your run doesn't have (a command, a folder) is never a chore for the user: write the rules, as narrow as the task needs, to `.symbiot/allowlist.proposed.json` (`{\"permissions\": {\"allow\": [...], \"deny\": [...], \"additionalDirectories\": [...]}}`) and offer `👤 You (only you: a permission): allow the list in .symbiot/allowlist.proposed.json`. Picking it in Symbiot turns it on for this folder and runs you again; don't ask them to copy files or run claude in a terminal for it.";
|
|
25
25
|
|
|
26
26
|
// What an agent handed over: [{ lane, text }] from its .symbiot/HANDOFF.md.
|
|
27
27
|
function parseHandoffs(md) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "symbiot",
|
|
3
|
-
"version": "0.50.
|
|
3
|
+
"version": "0.50.2",
|
|
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": {
|