cookbook-bridge 0.1.2 → 0.1.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/cookbook.mjs +1 -1
- package/hands.mjs +61 -5
- package/local.mjs +4 -1
- package/package.json +1 -1
package/cookbook.mjs
CHANGED
|
@@ -246,7 +246,7 @@ export async function fetchHands(cfg) {
|
|
|
246
246
|
if (res.status === 404) return { supported: false, calls: [], grants: [] };
|
|
247
247
|
if (!res.ok) throw new Error(`hands ${res.status}`);
|
|
248
248
|
const j = await res.json().catch(() => ({}));
|
|
249
|
-
return { supported: true, calls: j.calls ?? [], grants: j.grants ?? [] };
|
|
249
|
+
return { supported: true, calls: j.calls ?? [], grants: j.grants ?? [], awaiting: j.awaiting ?? [] };
|
|
250
250
|
}
|
|
251
251
|
|
|
252
252
|
/** Claim one call before running it. The server CASes on status, so two Bridges on
|
package/hands.mjs
CHANGED
|
@@ -97,7 +97,7 @@ function collapseHome(text, home) {
|
|
|
97
97
|
const h = String(home ?? "").replace(/\/+$/, "");
|
|
98
98
|
if (!h || h.length < 4) return text;
|
|
99
99
|
const esc = h.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
100
|
-
return text.replace(new RegExp(esc, "g"), "~");
|
|
100
|
+
return text.replace(new RegExp(esc + "(?=/|$)", "g"), "~");
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
/** Redact secrets out of anything this machine produced. Pure. */
|
|
@@ -181,7 +181,10 @@ const NEVER_READ = Object.freeze([
|
|
|
181
181
|
/(^|\/)id_(rsa|ed25519|ecdsa|dsa)(\.pub)?$/i,
|
|
182
182
|
/(^|\/)local\.json$/i, // the Bridge Local loopback token
|
|
183
183
|
/(^|\/)\.git\/config$/i, // can carry credentials in a remote URL
|
|
184
|
-
|
|
184
|
+
// `credentials.json` AND its dotfile spelling `.credentials.json` — the anchor
|
|
185
|
+
// used to accept only `/` before the word, so a dotfile in a granted project
|
|
186
|
+
// folder walked through the wall (ultrareview #123, bug_003).
|
|
187
|
+
/(^|[/.])credentials?(\.[A-Za-z0-9]+)?$/i,
|
|
185
188
|
]);
|
|
186
189
|
|
|
187
190
|
/** Every spelling of a path this filesystem might consider the same file. */
|
|
@@ -691,6 +694,13 @@ const VERBS = {
|
|
|
691
694
|
if (ext === ".json") {
|
|
692
695
|
try { JSON.parse(content); } catch (e) { return { error: `That isn't valid JSON, so it wasn't written: ${e.message.slice(0, 120)}` }; }
|
|
693
696
|
}
|
|
697
|
+
// The setup allowlist carries two TOML files (Codex). The "won't parse ⇒ not
|
|
698
|
+
// written" promise covered only JSON until ultrareview #123 (bug_008) — this is
|
|
699
|
+
// a structural check, not a full parser: unbalanced quotes/brackets, lines that
|
|
700
|
+
// are neither a table header nor `key = value`, unterminated multi-line strings.
|
|
701
|
+
if (ext === ".toml" && !tomlLooksValid(content)) {
|
|
702
|
+
return { error: "That doesn't look like valid TOML (unbalanced quotes/brackets or a malformed line), so it wasn't written." };
|
|
703
|
+
}
|
|
694
704
|
if (content.includes("\0")) return { error: "Content contains a null byte." };
|
|
695
705
|
|
|
696
706
|
let backup = null;
|
|
@@ -713,9 +723,15 @@ const VERBS = {
|
|
|
713
723
|
if (!r.ok) return { error: r.error };
|
|
714
724
|
const name = String(args.backup ?? "");
|
|
715
725
|
if (!BACKUP_RE.test(name)) return { error: "Pass the backup filename this session created (….bak-chef-<timestamp>)." };
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
726
|
+
// write_file placed the backup next to the RESOLVED file (r.real), so a
|
|
727
|
+
// symlinked setup file — dotfile managers do this constantly — keeps its backup
|
|
728
|
+
// in the target directory. Look there first, then beside the symlink itself
|
|
729
|
+
// (ultrareview #123, bug_009: restore used to look only beside the symlink and
|
|
730
|
+
// report "isn't there any more" about a backup that existed).
|
|
731
|
+
const candidates = [path.join(path.dirname(r.real), name), path.join(path.dirname(r.path), name)];
|
|
732
|
+
if (candidates.some((c) => path.basename(c) !== name)) return { error: "Invalid backup name." };
|
|
733
|
+
const backupPath = candidates.find((c) => fs.existsSync(c));
|
|
734
|
+
if (!backupPath) return { error: "That backup isn't there any more." };
|
|
719
735
|
try { fs.copyFileSync(backupPath, r.path); } catch (e) { return { error: `restore failed: ${e.code || e.message}` }; }
|
|
720
736
|
return { path: args.path, restored_from: name };
|
|
721
737
|
},
|
|
@@ -863,6 +879,46 @@ export function describeCall(call) {
|
|
|
863
879
|
case "run": return `run ${a.template}`;
|
|
864
880
|
case "doctor": return "run the setup doctor";
|
|
865
881
|
case "env": return "look at what's installed";
|
|
882
|
+
// The three verbs a host must approve are the three that used to render as a
|
|
883
|
+
// bare verb name (ultrareview #123, bug_007). Say WHAT, not just which.
|
|
884
|
+
case "write_file": return `write ${a.path}${typeof a.content === "string" ? ` (${a.content.length} chars)` : ""}`;
|
|
885
|
+
case "restore_backup": return `restore ${a.path} from ${a.backup ?? "its backup"}`;
|
|
886
|
+
case "open_url": {
|
|
887
|
+
try { const u = new URL(String(a.url ?? "")); return `open ${u.host}${u.pathname === "/" ? "" : u.pathname} in your browser`; }
|
|
888
|
+
catch { return `open ${a.url ?? "a page"} in your browser`; }
|
|
889
|
+
}
|
|
866
890
|
default: return String(call?.verb ?? "?");
|
|
867
891
|
}
|
|
868
892
|
}
|
|
893
|
+
|
|
894
|
+
/**
|
|
895
|
+
* Structural TOML sanity check for write_file. Deliberately NOT a parser: it
|
|
896
|
+
* rejects the ways a distracted agent breaks a config (unbalanced quotes or
|
|
897
|
+
* brackets, an unterminated multi-line string, a line that is neither a table
|
|
898
|
+
* header nor `key = value`) and accepts anything that has that shape.
|
|
899
|
+
*/
|
|
900
|
+
export function tomlLooksValid(text) {
|
|
901
|
+
let multi = null; // the open multi-line string delimiter, if any
|
|
902
|
+
let depth = 0; // open [ / { across lines (multi-line arrays and inline tables)
|
|
903
|
+
for (const raw of String(text).split(/\r?\n/)) {
|
|
904
|
+
const line = raw.trim();
|
|
905
|
+
if (multi) { if (line.includes(multi)) multi = null; continue; }
|
|
906
|
+
if (!line || line.startsWith("#")) continue;
|
|
907
|
+
if (depth > 0) {
|
|
908
|
+
for (const ch of line) { if (ch === "[" || ch === "{") depth++; else if (ch === "]" || ch === "}") depth--; if (depth < 0) return false; }
|
|
909
|
+
continue;
|
|
910
|
+
}
|
|
911
|
+
if (/^\[\[?[^\]]+\]\]?\s*(#.*)?$/.test(line)) continue;
|
|
912
|
+
const m = /^[A-Za-z0-9_\-."']+\s*=\s*(.+)$/.exec(line);
|
|
913
|
+
if (!m) return false;
|
|
914
|
+
const v = m[1].trim();
|
|
915
|
+
if (v.startsWith('"""') || v.startsWith("'''")) {
|
|
916
|
+
const q = v.slice(0, 3);
|
|
917
|
+
if (!(v.length > 3 && v.endsWith(q))) multi = q;
|
|
918
|
+
continue;
|
|
919
|
+
}
|
|
920
|
+
if (((v.match(/(?<!\\)"/g) || []).length) % 2 !== 0) return false;
|
|
921
|
+
for (const ch of v) { if (ch === "[" || ch === "{") depth++; else if (ch === "]" || ch === "}") depth--; if (depth < 0) return false; }
|
|
922
|
+
}
|
|
923
|
+
return !multi && depth === 0;
|
|
924
|
+
}
|
package/local.mjs
CHANGED
|
@@ -352,7 +352,10 @@ export function createLocalServer(deps) {
|
|
|
352
352
|
// picks it up immediately (no restart, so a host can close the door NOW).
|
|
353
353
|
if (route === "POST /hosting") {
|
|
354
354
|
const body = (await readBody(req)) ?? {};
|
|
355
|
-
|
|
355
|
+
// A security toggle whose default is OFF must not fail open: an empty or
|
|
356
|
+
// malformed body used to mean "turn it on" (ultrareview #123, bug_011).
|
|
357
|
+
if (typeof body.enabled !== "boolean") return json(res, 400, { ok: false, error: "`enabled` must be true or false" });
|
|
358
|
+
const enabled = body.enabled;
|
|
356
359
|
cfg.hosting = { ...(cfg.hosting ?? {}), enabled };
|
|
357
360
|
try {
|
|
358
361
|
saveConfigPatch((raw) => { raw.hosting = { ...(raw.hosting ?? {}), enabled }; });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cookbook-bridge",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Run your own Claude, Codex and Gemini subscriptions against your Cookbook workspaces. One approval connects every agent CLI on your machine, with a receipt for every run.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|