@zuzuucodes/cli 1.4.0 → 1.6.0
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/bin/zuzuu.mjs +20 -4
- package/package.json +1 -1
- package/web-app/dist/auth.js +91 -0
- package/web-app/dist/server.js +16 -79
- package/web-app/dist/zuzuu-cli.js +124 -0
- package/web-app/dist/{zuzuu-api.js → zuzuu-routes.js} +158 -133
- package/web-app/web-dist/assets/CommandPalette-DhBdR7X3.js +45 -0
- package/web-app/web-dist/assets/DiffTab-CqxwSjI2.js +1 -0
- package/web-app/web-dist/assets/EditorPane-94QPFR9R.js +41 -0
- package/web-app/web-dist/assets/MonacoFile-D76epTrG.js +1 -0
- package/web-app/web-dist/assets/angular-html-BVBpGdXr.js +1 -0
- package/web-app/web-dist/assets/{angular-ts-CD_OonCa.js → angular-ts-BfdufMKP.js} +1 -1
- package/web-app/web-dist/assets/{apl-uOGC3x4e.js → apl-DWBSSoBH.js} +1 -1
- package/web-app/web-dist/assets/{astro-B6ybQmWG.js → astro-3LtMP0Sq.js} +1 -1
- package/web-app/web-dist/assets/{blade-B1QGRlVx.js → blade-llJRbbtR.js} +1 -1
- package/web-app/web-dist/assets/c-Wt1voDr2.js +1 -0
- package/web-app/web-dist/assets/{cobol-BgqgtYWn.js → cobol-x_HIyl2P.js} +1 -1
- package/web-app/web-dist/assets/{coffee-0wIRKYlr.js → coffee-CThvmt4R.js} +1 -1
- package/web-app/web-dist/assets/cpp-NtAeskI3.js +1 -0
- package/web-app/web-dist/assets/{crystal-CyTK3qFN.js → crystal-DNu_sX0G.js} +1 -1
- package/web-app/web-dist/assets/css-DJp_X0uY.js +1 -0
- package/web-app/web-dist/assets/{cssMode-B9jnrWOz.js → cssMode-ByQBaInt.js} +1 -1
- package/web-app/web-dist/assets/dist-DQqjtuhV.js +153 -0
- package/web-app/web-dist/assets/{edge-CvML9pwC.js → edge-ozw5tpLl.js} +1 -1
- package/web-app/web-dist/assets/{editor.api2-BmGoRSl4.js → editor.api2-C7skgoRB.js} +1 -1
- package/web-app/web-dist/assets/{elixir-CrjqTiSc.js → elixir-VhA6FeZt.js} +1 -1
- package/web-app/web-dist/assets/{elm-C4JtJ0Au.js → elm-dREJmIFz.js} +1 -1
- package/web-app/web-dist/assets/{erb-Cmeb-29V.js → erb-CIg6G69l.js} +1 -1
- package/web-app/web-dist/assets/{freemarker2-B5LAi19B.js → freemarker2-CBBwP9JV.js} +1 -1
- package/web-app/web-dist/assets/{git-rebase-CXqdToiP.js → git-rebase-B44mJPta.js} +1 -1
- package/web-app/web-dist/assets/{glimmer-js-Kq-kdTyV.js → glimmer-js-vH_gHG0-.js} +1 -1
- package/web-app/web-dist/assets/{glimmer-ts-D0RKLJNf.js → glimmer-ts--abOzSAQ.js} +1 -1
- package/web-app/web-dist/assets/glsl-Dv5r7kPw.js +1 -0
- package/web-app/web-dist/assets/graphql-CB4jsw2E.js +1 -0
- package/web-app/web-dist/assets/{hack-trjVF3Po.js → hack-DvEYX148.js} +1 -1
- package/web-app/web-dist/assets/haml-zE6W3STP.js +1 -0
- package/web-app/web-dist/assets/{handlebars-B8_x7Zx7.js → handlebars-CzBR2SDs.js} +1 -1
- package/web-app/web-dist/assets/{handlebars-g7ZhGhI_.js → handlebars-tXdfxEd6.js} +1 -1
- package/web-app/web-dist/assets/html-C8UlPnhE.js +1 -0
- package/web-app/web-dist/assets/{html-CfvRMgoC.js → html-DgPn1QYH.js} +1 -1
- package/web-app/web-dist/assets/{html-derivative-BYX_F_XH.js → html-derivative-CY6NRz-J.js} +1 -1
- package/web-app/web-dist/assets/{htmlMode-Bi8vSvwb.js → htmlMode-BtdIDgA2.js} +1 -1
- package/web-app/web-dist/assets/{http-BIVDpHT-.js → http-Cyd7bS_S.js} +1 -1
- package/web-app/web-dist/assets/{hurl-CFsshMju.js → hurl-CWPsiEpf.js} +1 -1
- package/web-app/web-dist/assets/index-B27_WOhS.css +2 -0
- package/web-app/web-dist/assets/index-De6DWTZM.js +7 -0
- package/web-app/web-dist/assets/java-CGc3VwQr.js +1 -0
- package/web-app/web-dist/assets/{javascript-Bxx2wV4w.js → javascript-5m05n-Be.js} +1 -1
- package/web-app/web-dist/assets/javascript-CUt1pgmJ.js +1 -0
- package/web-app/web-dist/assets/{jinja-_ZS5zWwe.js → jinja-CD-Z-FLd.js} +1 -1
- package/web-app/web-dist/assets/{jison-D8mMEpcs.js → jison-imPNup1l.js} +1 -1
- package/web-app/web-dist/assets/json-Bg9ijW3F.js +1 -0
- package/web-app/web-dist/assets/{jsonMode-C6ELX5GM.js → jsonMode-BG32YnTY.js} +1 -1
- package/web-app/web-dist/assets/jsx-CY6oMTks.js +1 -0
- package/web-app/web-dist/assets/{julia-D4h2DZrs.js → julia-Dc3O-irA.js} +1 -1
- package/web-app/web-dist/assets/{just-bMqQi3xg.js → just-BhOq_Kbv.js} +1 -1
- package/web-app/web-dist/assets/{latex-DThYi3CX.js → latex-Cu4Y1d5w.js} +1 -1
- package/web-app/web-dist/assets/lib-KIOQTlcs.js +1 -0
- package/web-app/web-dist/assets/{liquid-CUjzzP4r.js → liquid-3ZnQzTbs.js} +1 -1
- package/web-app/web-dist/assets/{liquid-CesB-zzl.js → liquid-CvXMrjlQ.js} +1 -1
- package/web-app/web-dist/assets/{lspLanguageFeatures-gTnJsses.js → lspLanguageFeatures-6KXALSrl.js} +1 -1
- package/web-app/web-dist/assets/lua-BjLEUjKY.js +1 -0
- package/web-app/web-dist/assets/{marko-yoGoLK2m.js → marko-DvhNOisQ.js} +1 -1
- package/web-app/web-dist/assets/{mdc-BvtXU6eH.js → mdc-Bm9TpL1X.js} +1 -1
- package/web-app/web-dist/assets/{mdx-DrXGQbNB.js → mdx-DffTEkNE.js} +1 -1
- package/web-app/web-dist/assets/{monaco-setup-CsR6EfHe.js → monaco-setup-DM3A5_VI.js} +3 -3
- package/web-app/web-dist/assets/{nginx-DoUz032F.js → nginx-Bhc82uuv.js} +1 -1
- package/web-app/web-dist/assets/{nim-B0Pl8B4R.js → nim-DXTVBFnF.js} +1 -1
- package/web-app/web-dist/assets/{perl-D2tfAALb.js → perl-C7veXV9z.js} +1 -1
- package/web-app/web-dist/assets/{php-BImCcX5X.js → php-BRiuMnnr.js} +1 -1
- package/web-app/web-dist/assets/{pug-BcnpC8P_.js → pug-C5hz5LQ7.js} +1 -1
- package/web-app/web-dist/assets/{python-ypRCBnvu.js → python-DyLAD3Wt.js} +1 -1
- package/web-app/web-dist/assets/{qml-DFDAunHY.js → qml-BdUV3aTS.js} +1 -1
- package/web-app/web-dist/assets/r-8R7vtdQc.js +1 -0
- package/web-app/web-dist/assets/{razor-aqrhpwqZ.js → razor-C49xQTPQ.js} +1 -1
- package/web-app/web-dist/assets/{razor-1_376SZM.js → razor-DRL52XO2.js} +1 -1
- package/web-app/web-dist/assets/react-vendor-CCIEwYL0.js +9 -0
- package/web-app/web-dist/assets/regexp-Omp9DhTb.js +1 -0
- package/web-app/web-dist/assets/{rst-2vG6f11Y.js → rst-BHX71KW9.js} +1 -1
- package/web-app/web-dist/assets/{ruby-Dj6bCFXR.js → ruby-B--HzjGU.js} +1 -1
- package/web-app/web-dist/assets/{sas-BhVZ4qL2.js → sas-DrLaYOK_.js} +1 -1
- package/web-app/web-dist/assets/scss-DdSxiZKl.js +1 -0
- package/web-app/web-dist/assets/shellscript-DwcUjJBL.js +1 -0
- package/web-app/web-dist/assets/{shellsession-CyO2fnhB.js → shellsession-CPZkydE6.js} +1 -1
- package/web-app/web-dist/assets/{soy-DIkw6E88.js → soy-Br5FhD7c.js} +1 -1
- package/web-app/web-dist/assets/sql-DNssxck8.js +1 -0
- package/web-app/web-dist/assets/{stata-DvkM932O.js → stata-DXn1tqOr.js} +1 -1
- package/web-app/web-dist/assets/{surrealql-B4-Q8tqV.js → surrealql-IeLNQw0f.js} +1 -1
- package/web-app/web-dist/assets/{svelte-p6yBy-Ki.js → svelte-DOdLCIlh.js} +1 -1
- package/web-app/web-dist/assets/{templ-C7EkuiZr.js → templ-CIwIngms.js} +1 -1
- package/web-app/web-dist/assets/{tex-DkmD8uFC.js → tex-D8QMumu5.js} +1 -1
- package/web-app/web-dist/assets/{ts-tags-U-hncHg4.js → ts-tags-BMVY4q-l.js} +1 -1
- package/web-app/web-dist/assets/{tsMode-a8OvovQd.js → tsMode-BndVBac5.js} +1 -1
- package/web-app/web-dist/assets/tsx-5Eka4NBX.js +1 -0
- package/web-app/web-dist/assets/{twig-CU0OP-IA.js → twig-C8o_5mgw.js} +1 -1
- package/web-app/web-dist/assets/{typescript-DnLjiKtn.js → typescript-B1w9vqKF.js} +1 -1
- package/web-app/web-dist/assets/typescript-DOu2WMV5.js +1 -0
- package/web-app/web-dist/assets/{vue-Db7nY3ba.js → vue-BU18DNDL.js} +1 -1
- package/web-app/web-dist/assets/{vue-html-BvAbiAw1.js → vue-html-BeluIYX0.js} +1 -1
- package/web-app/web-dist/assets/{vue-vine-BEaIQIlA.js → vue-vine-DGUAbOCX.js} +1 -1
- package/web-app/web-dist/assets/{xml-an4Nuuqq.js → xml-D8uAlVv5.js} +1 -1
- package/web-app/web-dist/assets/xml-DIqSwXR3.js +1 -0
- package/web-app/web-dist/assets/{xsl-D3NQgH22.js → xsl-Ct_-YIAy.js} +1 -1
- package/web-app/web-dist/assets/xterm-B1ffpRuj.js +36 -0
- package/web-app/web-dist/assets/xterm-addons-psDEiUMC.js +136 -0
- package/web-app/web-dist/assets/{yaml-Diiu6O9P.js → yaml-Bb7jXyQv.js} +1 -1
- package/web-app/web-dist/assets/yaml-DTtCYNlS.js +1 -0
- package/web-app/web-dist/index.html +6 -3
- package/zuzuu/actions/convert.mjs +10 -9
- package/zuzuu/actions/dispatch.mjs +12 -7
- package/zuzuu/actions/inbox.mjs +5 -5
- package/zuzuu/actions/manifest.mjs +48 -30
- package/zuzuu/actions/schema.mjs +9 -3
- package/zuzuu/actions/trail.mjs +1 -1
- package/zuzuu/commands/act-author.mjs +23 -13
- package/zuzuu/commands/act.mjs +4 -6
- package/zuzuu/commands/capture.mjs +2 -2
- package/zuzuu/commands/code.mjs +2 -2
- package/zuzuu/commands/digest.mjs +2 -2
- package/zuzuu/commands/distill.mjs +15 -16
- package/zuzuu/commands/doctor.mjs +41 -19
- package/zuzuu/commands/enable.mjs +1 -1
- package/zuzuu/commands/eval.mjs +3 -36
- package/zuzuu/commands/explain.mjs +4 -4
- package/zuzuu/commands/faculty.mjs +158 -0
- package/zuzuu/commands/generation.mjs +5 -8
- package/zuzuu/commands/hook.mjs +14 -12
- package/zuzuu/commands/inbox.mjs +1 -6
- package/zuzuu/commands/init.mjs +18 -4
- package/zuzuu/commands/knowledge.mjs +1 -1
- package/zuzuu/commands/migrations/home.mjs +96 -0
- package/zuzuu/commands/migrations/index.mjs +48 -0
- package/zuzuu/commands/migrations/items.mjs +360 -0
- package/zuzuu/commands/migrations/proposals.mjs +100 -0
- package/zuzuu/commands/proposals.mjs +131 -0
- package/zuzuu/commands/review.mjs +13 -227
- package/zuzuu/commands/session.mjs +8 -2
- package/zuzuu/commands/sessions.mjs +159 -0
- package/zuzuu/commands/status.mjs +3 -3
- package/zuzuu/commands/trace.mjs +1 -1
- package/zuzuu/{capture-core.mjs → core/capture-core.mjs} +3 -3
- package/zuzuu/{store.mjs → core/store.mjs} +1 -1
- package/zuzuu/digest/compose.mjs +96 -0
- package/zuzuu/eval/score.mjs +14 -1
- package/zuzuu/faculties/actions/index.mjs +283 -0
- package/zuzuu/faculties/guardrails/index.mjs +320 -0
- package/zuzuu/faculties/instructions/index.mjs +288 -0
- package/zuzuu/faculties/knowledge/index.mjs +185 -0
- package/zuzuu/faculties/memory/index.mjs +124 -0
- package/zuzuu/faculty/envelope.mjs +290 -0
- package/zuzuu/faculty/generation/read.mjs +206 -0
- package/zuzuu/faculty/generation/write.mjs +207 -0
- package/zuzuu/faculty/items.mjs +81 -0
- package/zuzuu/faculty/module.mjs +74 -0
- package/zuzuu/faculty/pending.mjs +63 -0
- package/zuzuu/faculty/registry.mjs +204 -18
- package/zuzuu/faculty/render.mjs +59 -0
- package/zuzuu/faculty/trail.mjs +1 -1
- package/zuzuu/guardrails/engine.mjs +137 -0
- package/zuzuu/{scaffold.mjs → home/scaffold.mjs} +110 -39
- package/zuzuu/knowledge/items.mjs +56 -91
- package/zuzuu/live/install.mjs +1 -1
- package/zuzuu/live/live-store.mjs +2 -2
- package/zuzuu/live/reconcile.mjs +2 -2
- package/zuzuu/sessions/git.mjs +47 -0
- package/zuzuu/{session-git.mjs → sessions/session-git.mjs} +5 -43
- package/web-app/web-dist/assets/DiffTab-BuWonUNJ.js +0 -1
- package/web-app/web-dist/assets/MonacoFile-CL3DhFKG.js +0 -1
- package/web-app/web-dist/assets/angular-html-CmT26mqM.js +0 -1
- package/web-app/web-dist/assets/c-BvoqrSVH.js +0 -1
- package/web-app/web-dist/assets/cpp-BXsk94m0.js +0 -1
- package/web-app/web-dist/assets/css-Z8oOGxII.js +0 -1
- package/web-app/web-dist/assets/dist-ChcDQ_7s.js +0 -153
- package/web-app/web-dist/assets/glsl-KwyfU2aa.js +0 -1
- package/web-app/web-dist/assets/graphql-DSeOUAa2.js +0 -1
- package/web-app/web-dist/assets/haml-azVoxQRV.js +0 -1
- package/web-app/web-dist/assets/html-D_7P5S4m.js +0 -1
- package/web-app/web-dist/assets/index--5yy8RbA.js +0 -267
- package/web-app/web-dist/assets/index-BVG4hgk7.css +0 -2
- package/web-app/web-dist/assets/java-D4RbCvBe.js +0 -1
- package/web-app/web-dist/assets/javascript-Cb010CKM.js +0 -1
- package/web-app/web-dist/assets/json-DWgqV4D1.js +0 -1
- package/web-app/web-dist/assets/jsx-CZjSJa1f.js +0 -1
- package/web-app/web-dist/assets/lua-TGj_6NzO.js +0 -1
- package/web-app/web-dist/assets/r-fCpuAR7u.js +0 -1
- package/web-app/web-dist/assets/regexp-B4yxx-Ty.js +0 -1
- package/web-app/web-dist/assets/scss-QdjMO_xV.js +0 -1
- package/web-app/web-dist/assets/shellscript-BnlgeVVx.js +0 -1
- package/web-app/web-dist/assets/sql-DGnQv6iD.js +0 -1
- package/web-app/web-dist/assets/tsx-MJ0-9sYG.js +0 -1
- package/web-app/web-dist/assets/typescript-C17ZkDe8.js +0 -1
- package/web-app/web-dist/assets/xml-CA9lHFQV.js +0 -1
- package/web-app/web-dist/assets/yaml-CwRYMJka.js +0 -1
- package/zuzuu/actions/adapter.mjs +0 -130
- package/zuzuu/commands/migrate.mjs +0 -225
- package/zuzuu/digest.mjs +0 -149
- package/zuzuu/faculty/generation.mjs +0 -392
- package/zuzuu/guardrails/adapter.mjs +0 -134
- package/zuzuu/guardrails.mjs +0 -89
- package/zuzuu/instructions/adapter.mjs +0 -93
- package/zuzuu/knowledge/adapter.mjs +0 -99
- package/zuzuu/memory/adapter.mjs +0 -121
- package/zuzuu/miners/actions.mjs +0 -118
- package/zuzuu/miners/guardrails.mjs +0 -179
- package/zuzuu/miners/instructions.mjs +0 -157
- package/zuzuu/miners/knowledge.mjs +0 -25
- package/zuzuu/miners/memory.mjs +0 -27
- package/zuzuu/miners/registry.mjs +0 -31
- /package/web-app/web-dist/assets/{chunk-QTnfLwEv.js → rolldown-runtime-QTnfLwEv.js} +0 -0
- /package/zuzuu/{session.mjs → core/session.mjs} +0 -0
- /package/zuzuu/{inject.mjs → home/inject.mjs} +0 -0
|
@@ -1,392 +0,0 @@
|
|
|
1
|
-
// zuzuu/faculty/generation.mjs — the generation core (WS3-T1).
|
|
2
|
-
//
|
|
3
|
-
// A *generation* is an immutable, content-addressed snapshot of the agent's
|
|
4
|
-
// pinned faculties (the lockfile). Minting freezes the current faculty state;
|
|
5
|
-
// rollback restores any past generation by *content* (we copy each pinned item's
|
|
6
|
-
// bytes into generations/snapshots/<id>/ at mint time, so a rollback works even
|
|
7
|
-
// for items that were never committed). Identity: Agent → Generation → Run —
|
|
8
|
-
// rollback = flip the active pointer + restore content; never `git revert`.
|
|
9
|
-
//
|
|
10
|
-
// Layout under .zuzuu/:
|
|
11
|
-
// generations/active {active: "gen_NNN"} — the live pointer
|
|
12
|
-
// generations/<id>.json the lockfile (content-addressed manifest)
|
|
13
|
-
// generations/snapshots/<id>/<faculty>/... pinned item bytes (rollback source)
|
|
14
|
-
|
|
15
|
-
import { createHash } from 'node:crypto';
|
|
16
|
-
import { join, dirname } from 'node:path';
|
|
17
|
-
import {
|
|
18
|
-
existsSync, readFileSync, writeFileSync, readdirSync, statSync, mkdirSync, renameSync,
|
|
19
|
-
} from 'node:fs';
|
|
20
|
-
import { reindex } from '../knowledge/index.mjs';
|
|
21
|
-
|
|
22
|
-
/** Hex sha256 of a string or Buffer. */
|
|
23
|
-
export function sha256(buf) {
|
|
24
|
-
return createHash('sha256').update(buf).digest('hex');
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const read = (p) => readFileSync(p, 'utf8');
|
|
28
|
-
const readJson = (p) => JSON.parse(read(p));
|
|
29
|
-
const writeJson = (p, obj) => {
|
|
30
|
-
mkdirSync(dirname(p), { recursive: true });
|
|
31
|
-
writeFileSync(p, JSON.stringify(obj, null, 2) + '\n');
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
// --- paths ------------------------------------------------------------------
|
|
35
|
-
|
|
36
|
-
const generationsDir = (agentDir) => join(agentDir, 'generations');
|
|
37
|
-
const snapshotsDir = (agentDir) => join(generationsDir(agentDir), 'snapshots');
|
|
38
|
-
const activePath = (agentDir) => join(generationsDir(agentDir), 'active');
|
|
39
|
-
const lockfilePath = (agentDir, id) => join(generationsDir(agentDir), `${id}.json`);
|
|
40
|
-
const agentJsonPath = (agentDir) => join(agentDir, 'agent.json');
|
|
41
|
-
|
|
42
|
-
// --- faculty file enumeration (the pinned set) ------------------------------
|
|
43
|
-
// Each entry: { id, faculty, src (absolute live path), rel (path under the
|
|
44
|
-
// faculty snapshot dir), hash }. `rel` is what we mirror into snapshots/<id>/.
|
|
45
|
-
|
|
46
|
-
function sortDirents(dir) {
|
|
47
|
-
if (!existsSync(dir)) return [];
|
|
48
|
-
return readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function knowledgeFiles(agentDir) {
|
|
52
|
-
const dir = join(agentDir, 'knowledge', 'items');
|
|
53
|
-
return sortDirents(dir)
|
|
54
|
-
.filter((e) => e.isFile() && e.name.endsWith('.md'))
|
|
55
|
-
.map((e) => {
|
|
56
|
-
const src = join(dir, e.name);
|
|
57
|
-
return { id: e.name.replace(/\.md$/, ''), faculty: 'knowledge', src, rel: e.name, hash: sha256(readFileSync(src)) };
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function actionFiles(agentDir) {
|
|
62
|
-
const dir = join(agentDir, 'actions');
|
|
63
|
-
return sortDirents(dir)
|
|
64
|
-
.filter((e) => e.isDirectory() && e.name !== 'inbox' && e.name !== 'proposals')
|
|
65
|
-
.map((e) => {
|
|
66
|
-
const adir = join(dir, e.name);
|
|
67
|
-
// Hash the dir's defining files concatenated (action.json + run.mjs/SKILL.md).
|
|
68
|
-
const parts = ['action.json', 'run.mjs', 'SKILL.md']
|
|
69
|
-
.map((f) => join(adir, f))
|
|
70
|
-
.filter((p) => existsSync(p));
|
|
71
|
-
const concat = Buffer.concat(parts.map((p) => readFileSync(p)));
|
|
72
|
-
return {
|
|
73
|
-
id: e.name, faculty: 'actions', files: parts.map((p) => p.slice(adir.length + 1)),
|
|
74
|
-
adir, hash: parts.length ? sha256(concat) : null,
|
|
75
|
-
};
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function memoryFiles(agentDir) {
|
|
80
|
-
const dir = join(agentDir, 'memory', 'entries');
|
|
81
|
-
return sortDirents(dir)
|
|
82
|
-
.filter((e) => e.isFile() && e.name.endsWith('.md'))
|
|
83
|
-
.map((e) => {
|
|
84
|
-
const src = join(dir, e.name);
|
|
85
|
-
return { id: e.name.replace(/\.md$/, ''), faculty: 'memory', src, rel: e.name, hash: sha256(readFileSync(src)) };
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function registryHash(agentDir) {
|
|
90
|
-
const dir = join(agentDir, 'knowledge', 'registry');
|
|
91
|
-
const files = sortDirents(dir).filter((e) => e.isFile() && e.name.endsWith('.json'));
|
|
92
|
-
if (!files.length) return null;
|
|
93
|
-
return sha256(Buffer.concat(files.map((e) => readFileSync(join(dir, e.name)))));
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function fileHashOrNull(p) {
|
|
97
|
-
return existsSync(p) ? sha256(readFileSync(p)) : null;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Snapshot the current faculty state → the `faculties` manifest object.
|
|
102
|
-
* Tolerates missing files (empty arrays / null hashes).
|
|
103
|
-
*/
|
|
104
|
-
export function snapshotFaculties(agentDir) {
|
|
105
|
-
return {
|
|
106
|
-
knowledge: {
|
|
107
|
-
items: knowledgeFiles(agentDir).map(({ id, hash }) => ({ id, hash })),
|
|
108
|
-
registryHash: registryHash(agentDir),
|
|
109
|
-
},
|
|
110
|
-
actions: {
|
|
111
|
-
items: actionFiles(agentDir).map(({ id, hash }) => ({ id, hash })),
|
|
112
|
-
},
|
|
113
|
-
guardrails: {
|
|
114
|
-
rulesHash: fileHashOrNull(join(agentDir, 'guardrails', 'rules.json')),
|
|
115
|
-
},
|
|
116
|
-
instructions: {
|
|
117
|
-
projectHash: fileHashOrNull(join(agentDir, 'instructions', 'project.md')),
|
|
118
|
-
},
|
|
119
|
-
memory: {
|
|
120
|
-
items: memoryFiles(agentDir).map(({ id, hash }) => ({ id, hash })),
|
|
121
|
-
},
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// --- agent identity ---------------------------------------------------------
|
|
126
|
-
|
|
127
|
-
/** Stable agent id derived from the repo root: agt_<first12 of sha256(root)>. */
|
|
128
|
-
export function agentId(agentDir) {
|
|
129
|
-
// agentDir is the .zuzuu/ dir; the repo root is its parent.
|
|
130
|
-
const root = dirname(agentDir);
|
|
131
|
-
return 'agt_' + sha256(root).slice(0, 12);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/** Add/repair the agent block in agent.json (bump to v2), preserving other fields. */
|
|
135
|
-
export function ensureAgent(agentDir) {
|
|
136
|
-
const path = agentJsonPath(agentDir);
|
|
137
|
-
const m = existsSync(path) ? readJson(path) : {};
|
|
138
|
-
const id = agentId(agentDir);
|
|
139
|
-
if (!m.agent || !m.agent.id) {
|
|
140
|
-
m.agent = { id, createdAt: new Date().toISOString() };
|
|
141
|
-
}
|
|
142
|
-
m.version = 2;
|
|
143
|
-
writeJson(path, m);
|
|
144
|
-
return m.agent;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// --- generation read/list ---------------------------------------------------
|
|
148
|
-
|
|
149
|
-
/** The active generation id, or null. */
|
|
150
|
-
export function activeGeneration(agentDir) {
|
|
151
|
-
const p = activePath(agentDir);
|
|
152
|
-
if (!existsSync(p)) return null;
|
|
153
|
-
try { return readJson(p).active ?? null; } catch { return null; }
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/** All generation ids in ascending order. */
|
|
157
|
-
export function listGenerations(agentDir) {
|
|
158
|
-
const dir = generationsDir(agentDir);
|
|
159
|
-
if (!existsSync(dir)) return [];
|
|
160
|
-
return readdirSync(dir)
|
|
161
|
-
.filter((f) => /^gen_\d+\.json$/.test(f))
|
|
162
|
-
.map((f) => f.replace(/\.json$/, ''))
|
|
163
|
-
.sort();
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/** Read one lockfile, or null. */
|
|
167
|
-
export function readGeneration(agentDir, id) {
|
|
168
|
-
const p = lockfilePath(agentDir, id);
|
|
169
|
-
return existsSync(p) ? readJson(p) : null;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/** Item-list faculties carry {id,hash}[]; single-file faculties a *Hash scalar. */
|
|
173
|
-
const HASH_KEYS = { knowledge: 'registryHash', instructions: 'projectHash', guardrails: 'rulesHash' };
|
|
174
|
-
|
|
175
|
-
/** Diff two item-manifest arrays → {added, changed, removed} (id lists). */
|
|
176
|
-
function diffItems(parentItems = [], childItems = []) {
|
|
177
|
-
const p = new Map(parentItems.map((i) => [i.id, i.hash]));
|
|
178
|
-
const c = new Map(childItems.map((i) => [i.id, i.hash]));
|
|
179
|
-
const added = [], changed = [], removed = [];
|
|
180
|
-
for (const [id, hash] of c) {
|
|
181
|
-
if (!p.has(id)) added.push(id);
|
|
182
|
-
else if (p.get(id) !== hash) changed.push(id);
|
|
183
|
-
}
|
|
184
|
-
for (const id of p.keys()) if (!c.has(id)) removed.push(id);
|
|
185
|
-
return { added: added.sort(), changed: changed.sort(), removed: removed.sort() };
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* Per-faculty diff of generation `id` against its forkedFrom parent (pure).
|
|
190
|
-
* For item-list faculties (knowledge/actions/memory) reports added/changed/removed
|
|
191
|
-
* id lists. For hash-only faculties (guardrails/instructions, and knowledge's
|
|
192
|
-
* registry) reports a `changed` boolean when the scalar hash differs. When there
|
|
193
|
-
* is no parent (forkedFrom null), everything present counts as added.
|
|
194
|
-
* Returns null for an unknown id.
|
|
195
|
-
*/
|
|
196
|
-
export function diffGenerations(agentDir, id) {
|
|
197
|
-
const child = readGeneration(agentDir, id);
|
|
198
|
-
if (!child) return null;
|
|
199
|
-
const parent = child.forkedFrom ? readGeneration(agentDir, child.forkedFrom) : null;
|
|
200
|
-
const cf = child.faculties || {};
|
|
201
|
-
const pf = parent?.faculties || {};
|
|
202
|
-
const faculties = {};
|
|
203
|
-
for (const f of ['knowledge', 'actions', 'memory']) {
|
|
204
|
-
faculties[f] = diffItems(pf[f]?.items, cf[f]?.items);
|
|
205
|
-
// knowledge also has a registry hash
|
|
206
|
-
if (f === 'knowledge') {
|
|
207
|
-
faculties[f].registryChanged = (cf.knowledge?.registryHash ?? null) !== (pf.knowledge?.registryHash ?? null);
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
for (const f of ['guardrails', 'instructions']) {
|
|
211
|
-
const key = HASH_KEYS[f];
|
|
212
|
-
faculties[f] = { changed: (cf[f]?.[key] ?? null) !== (pf[f]?.[key] ?? null) };
|
|
213
|
-
}
|
|
214
|
-
return {
|
|
215
|
-
id,
|
|
216
|
-
forkedFrom: child.forkedFrom ?? null,
|
|
217
|
-
mintedFrom: Array.isArray(child.mintedFrom) ? child.mintedFrom : [],
|
|
218
|
-
mintedAt: child.mintedAt ?? null,
|
|
219
|
-
faculties,
|
|
220
|
-
};
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
function nextGenId(agentDir) {
|
|
224
|
-
const ids = listGenerations(agentDir);
|
|
225
|
-
const max = ids.reduce((m, id) => Math.max(m, parseInt(id.slice(4), 10) || 0), 0);
|
|
226
|
-
return 'gen_' + String(max + 1).padStart(3, '0');
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// --- mint -------------------------------------------------------------------
|
|
230
|
-
|
|
231
|
-
function copySnapshot(agentDir, id) {
|
|
232
|
-
const base = join(snapshotsDir(agentDir), id);
|
|
233
|
-
for (const it of knowledgeFiles(agentDir)) {
|
|
234
|
-
const dest = join(base, 'knowledge', it.rel);
|
|
235
|
-
mkdirSync(dirname(dest), { recursive: true });
|
|
236
|
-
writeFileSync(dest, readFileSync(it.src));
|
|
237
|
-
}
|
|
238
|
-
for (const it of memoryFiles(agentDir)) {
|
|
239
|
-
const dest = join(base, 'memory', it.rel);
|
|
240
|
-
mkdirSync(dirname(dest), { recursive: true });
|
|
241
|
-
writeFileSync(dest, readFileSync(it.src));
|
|
242
|
-
}
|
|
243
|
-
for (const a of actionFiles(agentDir)) {
|
|
244
|
-
for (const rel of a.files) {
|
|
245
|
-
const dest = join(base, 'actions', a.id, rel);
|
|
246
|
-
mkdirSync(dirname(dest), { recursive: true });
|
|
247
|
-
writeFileSync(dest, readFileSync(join(a.adir, rel)));
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
// single-file faculties
|
|
251
|
-
const rules = join(agentDir, 'guardrails', 'rules.json');
|
|
252
|
-
if (existsSync(rules)) {
|
|
253
|
-
const dest = join(base, 'guardrails', 'rules.json');
|
|
254
|
-
mkdirSync(dirname(dest), { recursive: true });
|
|
255
|
-
writeFileSync(dest, readFileSync(rules));
|
|
256
|
-
}
|
|
257
|
-
const proj = join(agentDir, 'instructions', 'project.md');
|
|
258
|
-
if (existsSync(proj)) {
|
|
259
|
-
const dest = join(base, 'instructions', 'project.md');
|
|
260
|
-
mkdirSync(dirname(dest), { recursive: true });
|
|
261
|
-
writeFileSync(dest, readFileSync(proj));
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
/**
|
|
266
|
-
* Mint a new generation: freeze the current faculty state into a content-addressed
|
|
267
|
-
* lockfile + a byte-for-byte snapshot, and make it active.
|
|
268
|
-
*/
|
|
269
|
-
export function mintGeneration(agentDir, { forkedFrom = null, mintedFrom = [] } = {}) {
|
|
270
|
-
const agent = ensureAgent(agentDir).id;
|
|
271
|
-
const id = nextGenId(agentDir);
|
|
272
|
-
const lockfile = {
|
|
273
|
-
id,
|
|
274
|
-
agent,
|
|
275
|
-
mintedAt: new Date().toISOString(),
|
|
276
|
-
forkedFrom,
|
|
277
|
-
mintedFrom,
|
|
278
|
-
faculties: snapshotFaculties(agentDir),
|
|
279
|
-
};
|
|
280
|
-
copySnapshot(agentDir, id);
|
|
281
|
-
writeJson(lockfilePath(agentDir, id), lockfile);
|
|
282
|
-
writeJson(activePath(agentDir), { active: id });
|
|
283
|
-
return lockfile;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
// --- rollback ---------------------------------------------------------------
|
|
287
|
-
|
|
288
|
-
function archive(agentDir, faculty, src) {
|
|
289
|
-
// Park (never delete) under <faculty>/_rolledback/<basename> — by basename so
|
|
290
|
-
// a restore is a simple, flat audit trail of what the rollback displaced.
|
|
291
|
-
const dest = join(agentDir, faculty, '_rolledback', src.slice(dirname(src).length + 1));
|
|
292
|
-
mkdirSync(dirname(dest), { recursive: true });
|
|
293
|
-
renameSync(src, dest);
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
/**
|
|
297
|
-
* Restore a past generation by content: write each snapshotted item back to its
|
|
298
|
-
* live faculty path; MOVE (never delete) active items absent from the target into
|
|
299
|
-
* <faculty>/_rolledback/; reindex knowledge; flip the active pointer.
|
|
300
|
-
*/
|
|
301
|
-
export function rollback(agentDir, id) {
|
|
302
|
-
const target = readGeneration(agentDir, id);
|
|
303
|
-
if (!target) throw new Error(`no generation '${id}'`);
|
|
304
|
-
const base = join(snapshotsDir(agentDir), id);
|
|
305
|
-
let restored = 0;
|
|
306
|
-
|
|
307
|
-
// 1) restore snapshotted knowledge items
|
|
308
|
-
const targetKnowledge = new Set((target.faculties.knowledge?.items ?? []).map((i) => i.id));
|
|
309
|
-
for (const i of target.faculties.knowledge?.items ?? []) {
|
|
310
|
-
const snap = join(base, 'knowledge', `${i.id}.md`);
|
|
311
|
-
if (existsSync(snap)) {
|
|
312
|
-
const dest = join(agentDir, 'knowledge', 'items', `${i.id}.md`);
|
|
313
|
-
mkdirSync(dirname(dest), { recursive: true });
|
|
314
|
-
writeFileSync(dest, readFileSync(snap));
|
|
315
|
-
restored++;
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
// archive live knowledge items not in the target
|
|
319
|
-
const kdir = join(agentDir, 'knowledge', 'items');
|
|
320
|
-
if (existsSync(kdir)) {
|
|
321
|
-
for (const e of readdirSync(kdir, { withFileTypes: true })) {
|
|
322
|
-
if (e.isFile() && e.name.endsWith('.md') && !targetKnowledge.has(e.name.replace(/\.md$/, ''))) {
|
|
323
|
-
archive(agentDir, 'knowledge', join(kdir, e.name));
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
// 2) restore snapshotted memory items + archive extras
|
|
329
|
-
const targetMemory = new Set((target.faculties.memory?.items ?? []).map((i) => i.id));
|
|
330
|
-
for (const i of target.faculties.memory?.items ?? []) {
|
|
331
|
-
const snap = join(base, 'memory', `${i.id}.md`);
|
|
332
|
-
if (existsSync(snap)) {
|
|
333
|
-
const dest = join(agentDir, 'memory', 'entries', `${i.id}.md`);
|
|
334
|
-
mkdirSync(dirname(dest), { recursive: true });
|
|
335
|
-
writeFileSync(dest, readFileSync(snap));
|
|
336
|
-
restored++;
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
const mdir = join(agentDir, 'memory', 'entries');
|
|
340
|
-
if (existsSync(mdir)) {
|
|
341
|
-
for (const e of readdirSync(mdir, { withFileTypes: true })) {
|
|
342
|
-
if (e.isFile() && e.name.endsWith('.md') && !targetMemory.has(e.name.replace(/\.md$/, ''))) {
|
|
343
|
-
archive(agentDir, 'memory', join(mdir, e.name));
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
// 3) restore snapshotted actions + archive extras
|
|
349
|
-
const targetActions = new Set((target.faculties.actions?.items ?? []).map((i) => i.id));
|
|
350
|
-
const asnap = join(base, 'actions');
|
|
351
|
-
if (existsSync(asnap)) {
|
|
352
|
-
for (const slugEnt of readdirSync(asnap, { withFileTypes: true })) {
|
|
353
|
-
if (!slugEnt.isDirectory()) continue;
|
|
354
|
-
const sdir = join(asnap, slugEnt.name);
|
|
355
|
-
for (const f of readdirSync(sdir)) {
|
|
356
|
-
const dest = join(agentDir, 'actions', slugEnt.name, f);
|
|
357
|
-
mkdirSync(dirname(dest), { recursive: true });
|
|
358
|
-
writeFileSync(dest, readFileSync(join(sdir, f)));
|
|
359
|
-
}
|
|
360
|
-
restored++;
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
const adir = join(agentDir, 'actions');
|
|
364
|
-
if (existsSync(adir)) {
|
|
365
|
-
for (const e of readdirSync(adir, { withFileTypes: true })) {
|
|
366
|
-
if (e.isDirectory() && e.name !== 'inbox' && e.name !== 'proposals' && e.name !== '_rolledback' && !targetActions.has(e.name)) {
|
|
367
|
-
archive(agentDir, 'actions', join(adir, e.name));
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
// 4) restore single-file faculties from the snapshot
|
|
373
|
-
const grules = join(base, 'guardrails', 'rules.json');
|
|
374
|
-
if (existsSync(grules)) {
|
|
375
|
-
const dest = join(agentDir, 'guardrails', 'rules.json');
|
|
376
|
-
mkdirSync(dirname(dest), { recursive: true });
|
|
377
|
-
writeFileSync(dest, readFileSync(grules));
|
|
378
|
-
restored++;
|
|
379
|
-
}
|
|
380
|
-
const proj = join(base, 'instructions', 'project.md');
|
|
381
|
-
if (existsSync(proj)) {
|
|
382
|
-
const dest = join(agentDir, 'instructions', 'project.md');
|
|
383
|
-
mkdirSync(dirname(dest), { recursive: true });
|
|
384
|
-
writeFileSync(dest, readFileSync(proj));
|
|
385
|
-
restored++;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
// 5) regenerate the derived knowledge index + flip the pointer
|
|
389
|
-
try { reindex(agentDir); } catch { /* derived index; tolerate absence of node:sqlite features */ }
|
|
390
|
-
writeJson(activePath(agentDir), { active: id });
|
|
391
|
-
return { ok: true, restored };
|
|
392
|
-
}
|
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
// zuzuu/guardrails/adapter.mjs
|
|
2
|
-
// The Guardrails faculty adapter (WS2-T4). Wraps the rules engine behind the
|
|
3
|
-
// faculty-spine adapter contract — { name, ingest, validate, apply, render } —
|
|
4
|
-
// so `zuzuu review` can surface and approve/reject rule proposals the same way it
|
|
5
|
-
// does Knowledge proposals.
|
|
6
|
-
//
|
|
7
|
-
// A guardrails proposal payload is a single rule record:
|
|
8
|
-
// { id, action: deny|ask|allow, tool, pattern, reason }
|
|
9
|
-
//
|
|
10
|
-
// apply: loads .zuzuu/guardrails/rules.json (seeding {version:1,rules:[]} if
|
|
11
|
-
// absent), appends the rule or replaces an existing one with the same id,
|
|
12
|
-
// then writes the file back.
|
|
13
|
-
//
|
|
14
|
-
// Registers itself on import.
|
|
15
|
-
|
|
16
|
-
import { join } from 'node:path';
|
|
17
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
18
|
-
import * as registry from '../faculty/registry.mjs';
|
|
19
|
-
|
|
20
|
-
const name = 'guardrails';
|
|
21
|
-
const VALID_ACTIONS = new Set(['deny', 'ask', 'allow']);
|
|
22
|
-
|
|
23
|
-
// ---------------------------------------------------------------------------
|
|
24
|
-
// helpers
|
|
25
|
-
// ---------------------------------------------------------------------------
|
|
26
|
-
|
|
27
|
-
function rulesPath(agentDir) {
|
|
28
|
-
return join(agentDir, 'guardrails', 'rules.json');
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function loadRulesFile(agentDir) {
|
|
32
|
-
const path = rulesPath(agentDir);
|
|
33
|
-
if (!existsSync(path)) return { version: 1, rules: [] };
|
|
34
|
-
try {
|
|
35
|
-
return JSON.parse(readFileSync(path, 'utf8'));
|
|
36
|
-
} catch {
|
|
37
|
-
return { version: 1, rules: [] };
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// ---------------------------------------------------------------------------
|
|
42
|
-
// adapter contract
|
|
43
|
-
// ---------------------------------------------------------------------------
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Ingest a raw rule object. Pass-through: rule fields are the payload.
|
|
47
|
-
* @param {string} agentDir
|
|
48
|
-
* @param {object} raw — expected shape: { id, action, tool, pattern, reason }
|
|
49
|
-
* or { payload: { ... } } from the spine
|
|
50
|
-
*/
|
|
51
|
-
function ingest(_agentDir, raw) {
|
|
52
|
-
const payload = raw?.payload ?? raw ?? {};
|
|
53
|
-
return { payload, analysis: {}, dedupeKey: payload.id };
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Validate a rule payload.
|
|
58
|
-
* @returns {{ok:boolean, errors:string[], warnings:string[]}}
|
|
59
|
-
*/
|
|
60
|
-
function validate(_agentDir, payload) {
|
|
61
|
-
const errors = [];
|
|
62
|
-
if (!payload?.id || typeof payload.id !== 'string' || !payload.id.trim()) {
|
|
63
|
-
errors.push('rule id is required (non-empty string slug)');
|
|
64
|
-
}
|
|
65
|
-
if (!VALID_ACTIONS.has(payload?.action)) {
|
|
66
|
-
errors.push(`action must be one of deny|ask|allow (got '${payload?.action}')`);
|
|
67
|
-
}
|
|
68
|
-
if (!payload?.tool || typeof payload.tool !== 'string') {
|
|
69
|
-
errors.push('tool is required (exact tool name or \'*\')');
|
|
70
|
-
}
|
|
71
|
-
if (typeof payload?.pattern !== 'string' || !payload.pattern) {
|
|
72
|
-
errors.push('pattern is required (a non-empty regex string)');
|
|
73
|
-
} else {
|
|
74
|
-
try {
|
|
75
|
-
new RegExp(payload.pattern); // eslint-disable-line no-new
|
|
76
|
-
} catch (e) {
|
|
77
|
-
errors.push(`pattern does not compile as a RegExp: ${e.message}`);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
if (!payload?.reason || !String(payload.reason).trim()) {
|
|
81
|
-
errors.push('reason is required (non-empty)');
|
|
82
|
-
}
|
|
83
|
-
return { ok: errors.length === 0, errors, warnings: [] };
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Apply an approved rule proposal: upsert into rules.json.
|
|
88
|
-
* @returns {{ok:boolean, action:string, itemIds:string[]}}
|
|
89
|
-
*/
|
|
90
|
-
function apply(agentDir, proposal) {
|
|
91
|
-
const rule = proposal?.payload ?? {};
|
|
92
|
-
const id = rule.id;
|
|
93
|
-
|
|
94
|
-
// Ensure the guardrails dir exists
|
|
95
|
-
mkdirSync(join(agentDir, 'guardrails'), { recursive: true });
|
|
96
|
-
|
|
97
|
-
const data = loadRulesFile(agentDir);
|
|
98
|
-
if (!Array.isArray(data.rules)) data.rules = [];
|
|
99
|
-
|
|
100
|
-
const idx = data.rules.findIndex((r) => r.id === id);
|
|
101
|
-
// Store only the canonical fields (id, action, tool, pattern, reason)
|
|
102
|
-
const entry = {
|
|
103
|
-
id: rule.id,
|
|
104
|
-
action: rule.action,
|
|
105
|
-
tool: rule.tool,
|
|
106
|
-
pattern: rule.pattern,
|
|
107
|
-
reason: rule.reason,
|
|
108
|
-
};
|
|
109
|
-
if (idx >= 0) {
|
|
110
|
-
data.rules[idx] = entry;
|
|
111
|
-
} else {
|
|
112
|
-
data.rules.push(entry);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
writeFileSync(rulesPath(agentDir), JSON.stringify(data, null, 2) + '\n');
|
|
116
|
-
return { ok: true, action: `added rule ${id}`, itemIds: [id] };
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Render a rule proposal for the human gate.
|
|
121
|
-
* @returns {{line:string, card:string}}
|
|
122
|
-
*/
|
|
123
|
-
function render(proposal) {
|
|
124
|
-
const r = proposal?.payload ?? {};
|
|
125
|
-
const summary = `${r.action ?? '?'} ${r.tool ?? '*'} /${r.pattern ?? ''}/ — ${r.reason ?? ''}`;
|
|
126
|
-
return {
|
|
127
|
-
line: `${r.id ?? ''} [rule] ${summary}`,
|
|
128
|
-
card: summary,
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
export const adapter = { name, ingest, validate, apply, render };
|
|
133
|
-
|
|
134
|
-
registry.register(adapter);
|
package/zuzuu/guardrails.mjs
DELETED
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
// The Guardrails faculty — v1 rule engine (pure; I/O lives in the hook command).
|
|
2
|
-
//
|
|
3
|
-
// Rules are DATA, not code: .zuzuu/guardrails/rules.json, ordered, declarative —
|
|
4
|
-
// a *definition* in the pin-definitions sense (versioned in git, graduates via
|
|
5
|
-
// proposals like every faculty's contents).
|
|
6
|
-
//
|
|
7
|
-
// { "version": 1,
|
|
8
|
-
// "rules": [ { "id": "no-root-wipe", "action": "deny",
|
|
9
|
-
// "tool": "Bash", // exact tool name, or "*"
|
|
10
|
-
// "pattern": "rm\\s+-rf\\s+/", // regex over the tool INPUT (stringified)
|
|
11
|
-
// "reason": "destructive root delete" } ] }
|
|
12
|
-
//
|
|
13
|
-
// Evaluation: collect every matching rule, then severity wins — deny > ask >
|
|
14
|
-
// allow (an explicit allow can whitelist past a later ask/deny only if it is
|
|
15
|
-
// NOT outweighed; severity beats file order so a sloppy rule ordering can never
|
|
16
|
-
// silently disarm a deny).
|
|
17
|
-
//
|
|
18
|
-
// FAIL-OPEN: any malformed rule/file yields { ok:false } and no decision — the
|
|
19
|
-
// host proceeds through its normal permission flow. A guardrail bug must never
|
|
20
|
-
// brick the agent; misses are logged, not fatal.
|
|
21
|
-
|
|
22
|
-
import { readFileSync } from 'node:fs';
|
|
23
|
-
|
|
24
|
-
const SEVERITY = { deny: 3, ask: 2, allow: 1 };
|
|
25
|
-
const ACTIONS = new Set(Object.keys(SEVERITY));
|
|
26
|
-
|
|
27
|
-
/** Parse + validate a rules file. Fail-open: returns ok:false on any problem. */
|
|
28
|
-
export function loadRules(path) {
|
|
29
|
-
try {
|
|
30
|
-
const data = JSON.parse(readFileSync(path, 'utf8'));
|
|
31
|
-
if (!Array.isArray(data.rules)) return { ok: false, rules: [], error: 'rules is not an array' };
|
|
32
|
-
const rules = [];
|
|
33
|
-
for (const r of data.rules) {
|
|
34
|
-
if (!r || typeof r !== 'object' || !ACTIONS.has(r.action) || typeof r.pattern !== 'string') {
|
|
35
|
-
return { ok: false, rules: [], error: `malformed rule: ${JSON.stringify(r).slice(0, 80)}` };
|
|
36
|
-
}
|
|
37
|
-
try {
|
|
38
|
-
rules.push({ id: String(r.id ?? `rule-${rules.length}`), action: r.action, tool: r.tool || '*', re: new RegExp(r.pattern, 'i'), reason: String(r.reason ?? '') });
|
|
39
|
-
} catch (e) {
|
|
40
|
-
return { ok: false, rules: [], error: `bad pattern in ${r.id}: ${e.message}` };
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
return { ok: true, rules };
|
|
44
|
-
} catch (e) {
|
|
45
|
-
return { ok: false, rules: [], error: e.message };
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Evaluate a tool call against loaded rules.
|
|
51
|
-
* @param {Array} rules from loadRules().rules
|
|
52
|
-
* @param {{tool:string, input:any}} call
|
|
53
|
-
* @returns {null | {action:'deny'|'ask'|'allow', rule:string, reason:string}}
|
|
54
|
-
* null = no rule matched → defer to the host's normal permission flow
|
|
55
|
-
*/
|
|
56
|
-
export function evaluate(rules, { tool, input }) {
|
|
57
|
-
const haystack = typeof input === 'string' ? input : JSON.stringify(input ?? {});
|
|
58
|
-
let winner = null;
|
|
59
|
-
for (const r of rules) {
|
|
60
|
-
if (r.tool !== '*' && r.tool !== tool) continue;
|
|
61
|
-
if (!r.re.test(haystack)) continue;
|
|
62
|
-
if (!winner || SEVERITY[r.action] > SEVERITY[winner.action]) {
|
|
63
|
-
winner = { action: r.action, rule: r.id, reason: r.reason || `matched guardrail ${r.id}` };
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
return winner;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Gemini CLI block shape: stdout JSON { decision: "deny", reason } (exit 0).
|
|
71
|
-
* Gemini has no "ask" decision → defer (null) so its own approval flow runs.
|
|
72
|
-
* Only an explicit deny blocks.
|
|
73
|
-
*/
|
|
74
|
-
export function toGeminiDecision(verdict) {
|
|
75
|
-
if (!verdict || verdict.action !== 'deny') return null;
|
|
76
|
-
return { decision: 'deny', reason: `guardrail ${verdict.rule}: ${verdict.reason}` };
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/** Map a verdict to Claude Code's PreToolUse hookSpecificOutput (verified schema). */
|
|
80
|
-
export function toPreToolUseDecision(verdict) {
|
|
81
|
-
if (!verdict || verdict.action === 'allow') return null; // no output → normal flow (fail-open / explicit allow)
|
|
82
|
-
return {
|
|
83
|
-
hookSpecificOutput: {
|
|
84
|
-
hookEventName: 'PreToolUse',
|
|
85
|
-
permissionDecision: verdict.action, // 'deny' | 'ask'
|
|
86
|
-
permissionDecisionReason: `guardrail ${verdict.rule}: ${verdict.reason}`,
|
|
87
|
-
},
|
|
88
|
-
};
|
|
89
|
-
}
|