symbiot 0.63.2 → 0.63.3
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 +10 -5
- 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.63.3
|
|
6
|
+
|
|
7
|
+
- **Allow works on a request that takes access away.** When an agent asked to lock itself out of something (a deny rule, with nothing new allowed), Symbiot dropped the request, so clicking Allow did nothing and the agent waited for good. Now it's turned on, without asking you, and the agent carries on.
|
|
8
|
+
- **A script in your project no longer reads as "outside your work".** Running `python3 supabase/scripts/x.py`, or checking a public web address, was taken for a folder elsewhere on your computer, so you got a permission card. Now it turns on by itself. A web request that sends something, and anything about a live project, still asks you.
|
|
9
|
+
|
|
5
10
|
## 0.63.2
|
|
6
11
|
|
|
7
12
|
- **Permission cards stop landing on you for Symbiot's own plumbing.** An agent connecting to a computer this one already connects to (your own server) no longer asks you, and a request too broad to allow safely goes back to the agent to spell out, not to you.
|
package/agents.mjs
CHANGED
|
@@ -1077,7 +1077,7 @@ function installAllowlist(path) {
|
|
|
1077
1077
|
let p; try { p = JSON.parse(readFileSync(join(path, ".symbiot", "allowlist.proposed.json"), "utf8")).permissions || {}; } catch { return null; }
|
|
1078
1078
|
const list = (x) => (Array.isArray(x) ? x.filter((r) => typeof r === "string" && r.trim()).map((r) => r.trim()) : []);
|
|
1079
1079
|
const allow = list(p.allow), deny = list(p.deny), dirs = list(p.additionalDirectories);
|
|
1080
|
-
if (!allow.length && !dirs.length) return null;
|
|
1080
|
+
if (!allow.length && !dirs.length && !deny.length) return null;
|
|
1081
1081
|
const wide = allow.filter(wideRule);
|
|
1082
1082
|
if (wide.length) return { refused: wide };
|
|
1083
1083
|
const f = join(path, ".claude", "settings.local.json");
|
|
@@ -1085,7 +1085,8 @@ function installAllowlist(path) {
|
|
|
1085
1085
|
const perm = cur.permissions || {}, merge = (a, b) => [...new Set([...list(a), ...b])];
|
|
1086
1086
|
cur.permissions = { ...perm, allow: merge(perm.allow, allow), deny: merge(perm.deny, [...deny, ...GUARD()]), additionalDirectories: merge(perm.additionalDirectories, dirs) };
|
|
1087
1087
|
try { mkdirSync(join(path, ".claude"), { recursive: true }); writeFileSync(f, JSON.stringify(cur, null, 2) + "\n", { mode: 0o600 }); } catch { return null; }
|
|
1088
|
-
|
|
1088
|
+
const n = allow.length + deny.length;
|
|
1089
|
+
return [`the allow list for this folder (${n} rule${n === 1 ? "" : "s"})`];
|
|
1089
1090
|
}
|
|
1090
1091
|
// ---- the owner's work, and allow lists inside it ---------------------------------------
|
|
1091
1092
|
// What the owner told Symbiot is their work: the folders it scans (Settings → Folders) and
|
|
@@ -1114,7 +1115,11 @@ function inWork(p, roots = workRoots()) {
|
|
|
1114
1115
|
// A Bash rule's own program, when it lives in a bin folder (~/.local/bin/adb), is a tool
|
|
1115
1116
|
// it runs, not a place it works in: only the paths after it count.
|
|
1116
1117
|
const BIN_PROGRAM = /^(Bash\(\s*)(?:~|\/)[^\s:()]*\/s?bin\/[^\s:()/]+/;
|
|
1117
|
-
|
|
1118
|
+
// A path starts a word: the "/scripts" in a relative supabase/scripts/x.py is in the lane, and
|
|
1119
|
+
// the "//host" in https://host is a site, not a folder. /dev/null is nobody's.
|
|
1120
|
+
const rulePaths = (r) => (String(r).replace(BIN_PROGRAM, "$1").match(/(?<=^|[\s(="'])(?:~|\/)[^\s:*()"',]*/g) || []).filter((x) => (x === "~" || x.length > 1) && !/^\/dev\/(null|std(in|out|err))$/.test(x));
|
|
1121
|
+
// curl or wget that sends something (a POST, a form, a file) goes out, like a push.
|
|
1122
|
+
const SENDS = /^Bash\(\s*(curl|wget)\b.*\s(-X\s*(POST|PUT|PATCH|DELETE)|-d|--data\S*|-F|--form|-T|--upload-file|--post-\w+)\b/i;
|
|
1118
1123
|
// An ssh, scp or rsync rule whose every remote (user@host, user@host:path) is a host this
|
|
1119
1124
|
// computer already connects to (it's in ~/.ssh/known_hosts) reaches the owner's own
|
|
1120
1125
|
// machine, not another one: it counts as their work. A wildcard host never does.
|
|
@@ -1130,7 +1135,7 @@ function allowlistInWork(path) {
|
|
|
1130
1135
|
const list = (x) => (Array.isArray(x) ? x.filter((r) => typeof r === "string" && r.trim()).map((r) => r.trim()) : []);
|
|
1131
1136
|
const allow = list(p.allow), dirs = list(p.additionalDirectories), roots = [...workRoots(), path];
|
|
1132
1137
|
const wide = allow.filter(wideRule); if (wide.length) return { why: "wide: " + wide.join(", ") };
|
|
1133
|
-
const pub = allow.filter((r) => PUBLISH_RULE.test(r) && !toKnownHost(r)); if (pub.length) return { why: "publishes or reaches another machine: " + pub.join(", ") };
|
|
1138
|
+
const pub = allow.filter((r) => (PUBLISH_RULE.test(r) || SENDS.test(r)) && !toKnownHost(r)); if (pub.length) return { why: "publishes or reaches another machine: " + pub.join(", ") };
|
|
1134
1139
|
const out = [...dirs, ...allow.flatMap(rulePaths)].filter((x) => !inWork(x, roots));
|
|
1135
1140
|
if (out.length) return { why: "outside your work: " + [...new Set(out)].slice(0, 3).join(", ") };
|
|
1136
1141
|
return { ok: true, allow, dirs };
|
|
@@ -1168,7 +1173,7 @@ function autoAllow(path) {
|
|
|
1168
1173
|
// recommended option is a step it takes itself (not 👤), and nothing in the question or
|
|
1169
1174
|
// that option is an approval, goes out, costs money, touches an account or a machine, or
|
|
1170
1175
|
// can't be undone, Symbiot picks it and the agent carries on. Everything else still comes to the user.
|
|
1171
|
-
const OUTWARD = /\b(approv\w*|publish\w*|releas\w*|push\w*|merg\w*|send\w*|sent|post\w*|repl\w*|e-?mail\w*|pay\w*|spend\w*|buy\w*|money|bill\w*|cost\w*|delet\w*|remov\w*|uninstall\w*|wip\w*|drop\w*|password\w*|2fa|token\w*|keys?|sign(?:ed|s)?[- ]?in|log(?:ged)?[- ]?in|accounts?|subscription|plan|tunnel\w*|prox\w*|ssh|vm|servers?|restart\w*|stop\w*|kill\w*|broker\w*|trad\w*|bank\w*|npm|deploy\w*|install\w*|phone|whatsapp|linkedin|github\.com)\b|\byour (?:default|settings?|profile)\b/i;
|
|
1176
|
+
const OUTWARD = /\b(approv\w*|publish\w*|releas\w*|push\w*|merg\w*|send\w*|sent|post\w*|repl\w*|e-?mail\w*|pay\w*|spend\w*|buy\w*|money|bill\w*|cost\w*|delet\w*|remov\w*|uninstall\w*|wip\w*|drop\w*|password\w*|2fa|token\w*|keys?|sign(?:ed|s)?[- ]?in|log(?:ged)?[- ]?in|accounts?|subscription|plan|tunnel\w*|prox\w*|ssh|vm|servers?|restart\w*|stop\w*|kill\w*|broker\w*|trad\w*|bank\w*|npm|deploy\w*|install\w*|phone|whatsapp|linkedin|github\.com|live|production|prod)\b|\byour (?:default|settings?|profile)\b/i;
|
|
1172
1177
|
function autoPick(q) {
|
|
1173
1178
|
const rec = (q.options || []).filter((o) => /\(recommended\b/i.test(o));
|
|
1174
1179
|
if (rec.length !== 1 || yourStep(rec[0])) return "";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "symbiot",
|
|
3
|
-
"version": "0.63.
|
|
3
|
+
"version": "0.63.3",
|
|
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": {
|