@zuzuucodes/cli 1.5.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 +12 -3
- 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} +46 -116
- 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-Dx3ub8Pk.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-DM6oHc7c.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-DflaUwqW.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-wbBeb0oN.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-DRwkDcoK.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/trail.mjs +1 -1
- package/zuzuu/commands/act.mjs +1 -1
- 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 +39 -4
- package/zuzuu/commands/enable.mjs +1 -1
- package/zuzuu/commands/eval.mjs +3 -36
- package/zuzuu/commands/faculty.mjs +102 -19
- package/zuzuu/commands/generation.mjs +3 -4
- package/zuzuu/commands/hook.mjs +7 -7
- package/zuzuu/commands/inbox.mjs +1 -6
- package/zuzuu/commands/init.mjs +5 -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/{migrate.mjs → migrations/items.mjs} +34 -246
- 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/{memory/adapter.mjs → faculties/memory/index.mjs} +37 -9
- package/zuzuu/faculty/generation/read.mjs +206 -0
- package/zuzuu/faculty/generation/write.mjs +207 -0
- package/zuzuu/faculty/items.mjs +11 -5
- 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.mjs → guardrails/engine.mjs} +1 -1
- package/zuzuu/{scaffold.mjs → home/scaffold.mjs} +12 -2
- 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-BpGp1akx.js +0 -1
- package/web-app/web-dist/assets/MonacoFile-CqbVacUZ.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-C6R6xoyX.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-DHpC851f.js +0 -268
- package/web-app/web-dist/assets/index-O-t1gyMG.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 -122
- package/zuzuu/digest.mjs +0 -154
- package/zuzuu/faculty/generation.mjs +0 -398
- package/zuzuu/guardrails/adapter.mjs +0 -103
- package/zuzuu/instructions/adapter.mjs +0 -93
- package/zuzuu/knowledge/adapter.mjs +0 -99
- package/zuzuu/miners/actions.mjs +0 -112
- package/zuzuu/miners/guardrails.mjs +0 -176
- 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
package/bin/zuzuu.mjs
CHANGED
|
@@ -21,11 +21,12 @@ import { doctor } from '../zuzuu/commands/doctor.mjs';
|
|
|
21
21
|
import { enable, disable } from '../zuzuu/commands/enable.mjs';
|
|
22
22
|
import { runHook } from '../zuzuu/commands/hook.mjs';
|
|
23
23
|
import { remember, recall, knowledge } from '../zuzuu/commands/knowledge.mjs';
|
|
24
|
-
import { review
|
|
24
|
+
import { review } from '../zuzuu/commands/review.mjs';
|
|
25
|
+
import { proposals } from '../zuzuu/commands/proposals.mjs';
|
|
25
26
|
import { distill } from '../zuzuu/commands/distill.mjs';
|
|
26
27
|
import { digest } from '../zuzuu/commands/digest.mjs';
|
|
27
28
|
import { act } from '../zuzuu/commands/act.mjs';
|
|
28
|
-
import { migrate } from '../zuzuu/commands/
|
|
29
|
+
import { migrate } from '../zuzuu/commands/migrations/index.mjs';
|
|
29
30
|
import { generation } from '../zuzuu/commands/generation.mjs';
|
|
30
31
|
import { evalCmd } from '../zuzuu/commands/eval.mjs';
|
|
31
32
|
import { code } from '../zuzuu/commands/code.mjs';
|
|
@@ -33,6 +34,7 @@ import { web } from '../zuzuu/commands/web.mjs';
|
|
|
33
34
|
import { explain } from '../zuzuu/commands/explain.mjs';
|
|
34
35
|
import { inbox } from '../zuzuu/commands/inbox.mjs';
|
|
35
36
|
import { session } from '../zuzuu/commands/session.mjs';
|
|
37
|
+
import { sessions } from '../zuzuu/commands/sessions.mjs';
|
|
36
38
|
import { faculty } from '../zuzuu/commands/faculty.mjs';
|
|
37
39
|
|
|
38
40
|
function parseArgs(argv) {
|
|
@@ -79,6 +81,9 @@ usage: zuzuu <command> [options]
|
|
|
79
81
|
list a faculty's envelope items (one doc · one line per item)
|
|
80
82
|
faculty schema <f> [--json]
|
|
81
83
|
print a faculty's payload schema (JSON-Schema subset)
|
|
84
|
+
faculty manifest <f> [--json]
|
|
85
|
+
print a faculty's module manifest (faculty.json)
|
|
86
|
+
faculty overview [--json] every faculty in one shot: ui + counts + top items + pending
|
|
82
87
|
digest [--json] [--budget N]
|
|
83
88
|
print the session-start grounding brief
|
|
84
89
|
act [list|show <slug>|new <slug>|schema <slug>]
|
|
@@ -99,6 +104,9 @@ usage: zuzuu <command> [options]
|
|
|
99
104
|
disable remove the background hooks
|
|
100
105
|
session [status|merge|continue|discard]
|
|
101
106
|
the invisible session branch (one per agent session)
|
|
107
|
+
sessions [--json] recorded sessions with lifecycle state labels
|
|
108
|
+
session inspect <id> [--json]
|
|
109
|
+
one session: trace summary + per-faculty mined signals
|
|
102
110
|
eval [--faculty f] rank pending proposals by eval score, highest first
|
|
103
111
|
migrate [--home|--items] one-time migrators: proposal schema · --home moves agent/ → .zuzuu/
|
|
104
112
|
· --items rewrites legacy faculty shapes → the envelope standard
|
|
@@ -123,7 +131,7 @@ switch (cmd) {
|
|
|
123
131
|
case 'knowledge': await knowledge(args); break;
|
|
124
132
|
case 'digest': digest(args); break;
|
|
125
133
|
case 'act': act(args); break;
|
|
126
|
-
case 'distill': distill(args); break;
|
|
134
|
+
case 'distill': await distill(args); break;
|
|
127
135
|
case 'inbox': inbox(args); break;
|
|
128
136
|
case 'review': await review(args); break;
|
|
129
137
|
case 'proposals': proposals(args); break;
|
|
@@ -134,6 +142,7 @@ switch (cmd) {
|
|
|
134
142
|
case 'disable': disable(args); break;
|
|
135
143
|
case 'hook': runHook(args._[0], { host: args.host, session: args.session }); break;
|
|
136
144
|
case 'session': session(args); break;
|
|
145
|
+
case 'sessions': sessions(args); break;
|
|
137
146
|
case 'faculty': faculty(args); break;
|
|
138
147
|
case 'eval': evalCmd(args); break;
|
|
139
148
|
case 'migrate': migrate(args); break;
|
package/package.json
CHANGED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// The daemon's auth + request-origin gates, extracted from server.ts:
|
|
2
|
+
// Host-header allowlist (DNS rebinding), Origin allowlist (cross-site WS
|
|
3
|
+
// hijacking / CSRF), and token-in-URL → HttpOnly cookie auth. One AuthGate
|
|
4
|
+
// instance guards both the Hono HTTP app and the WS upgrade path.
|
|
5
|
+
import crypto from "node:crypto";
|
|
6
|
+
import { getCookie, setCookie } from "hono/cookie";
|
|
7
|
+
const AUTH_COOKIE = "webcode_auth";
|
|
8
|
+
const COOKIE_MAX_AGE = 30 * 24 * 3600;
|
|
9
|
+
export class AuthGate {
|
|
10
|
+
authSessions = new Set();
|
|
11
|
+
allowedHosts;
|
|
12
|
+
allowedOrigins;
|
|
13
|
+
token;
|
|
14
|
+
constructor(cfg) {
|
|
15
|
+
this.token = cfg.token;
|
|
16
|
+
const hostNames = ["127.0.0.1", "localhost", "[::1]"];
|
|
17
|
+
this.allowedHosts = new Set(hostNames.flatMap((h) => [h, `${h}:${cfg.port}`]));
|
|
18
|
+
this.allowedOrigins = new Set([
|
|
19
|
+
...hostNames.map((h) => `http://${h}:${cfg.port}`),
|
|
20
|
+
...(cfg.extraOrigins ?? []),
|
|
21
|
+
]);
|
|
22
|
+
// hosted: also accept the public hostname (Fly's edge sets Host to it);
|
|
23
|
+
// Host/Origin defense stays on, just widened to the one public origin.
|
|
24
|
+
if (cfg.publicHost) {
|
|
25
|
+
this.allowedHosts.add(cfg.publicHost.toLowerCase());
|
|
26
|
+
this.allowedOrigins.add(`https://${cfg.publicHost}`);
|
|
27
|
+
this.allowedOrigins.add(`http://${cfg.publicHost}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/** Host allowlist defeats DNS rebinding: rebinding changes DNS, not the Host header. */
|
|
31
|
+
hostAllowed(host) {
|
|
32
|
+
return !!host && this.allowedHosts.has(host.toLowerCase());
|
|
33
|
+
}
|
|
34
|
+
/** Origin allowlist defeats cross-site WS hijacking / CSRF from arbitrary websites. */
|
|
35
|
+
originAllowed(origin) {
|
|
36
|
+
return origin === undefined || this.allowedOrigins.has(origin);
|
|
37
|
+
}
|
|
38
|
+
/** WS upgrade path: is the request's cookie an authenticated session? */
|
|
39
|
+
cookieAuthed(cookieHeader) {
|
|
40
|
+
if (!cookieHeader)
|
|
41
|
+
return false;
|
|
42
|
+
const match = /(?:^|;\s*)webcode_auth=([^;]+)/.exec(cookieHeader);
|
|
43
|
+
return !!match && this.authSessions.has(match[1]);
|
|
44
|
+
}
|
|
45
|
+
/** App-wide gate: Host/Origin allowlists + the ?token= → cookie exchange. */
|
|
46
|
+
gate() {
|
|
47
|
+
return async (c, next) => {
|
|
48
|
+
if (!this.hostAllowed(c.req.header("host"))) {
|
|
49
|
+
return c.text("forbidden host", 403);
|
|
50
|
+
}
|
|
51
|
+
if (!this.originAllowed(c.req.header("origin"))) {
|
|
52
|
+
return c.text("forbidden origin", 403);
|
|
53
|
+
}
|
|
54
|
+
// Token exchange: any page request carrying ?token= gets a cookie.
|
|
55
|
+
const token = c.req.query("token");
|
|
56
|
+
if (token && !c.req.path.startsWith("/api/")) {
|
|
57
|
+
if (!timingSafeEqualStr(token, this.token))
|
|
58
|
+
return c.text("invalid token", 403);
|
|
59
|
+
const secret = crypto.randomBytes(24).toString("base64url");
|
|
60
|
+
this.authSessions.add(secret);
|
|
61
|
+
setCookie(c, AUTH_COOKIE, secret, {
|
|
62
|
+
httpOnly: true,
|
|
63
|
+
sameSite: "Strict",
|
|
64
|
+
path: "/",
|
|
65
|
+
maxAge: COOKIE_MAX_AGE,
|
|
66
|
+
});
|
|
67
|
+
const url = new URL(c.req.url);
|
|
68
|
+
url.searchParams.delete("token");
|
|
69
|
+
// /auth?token=… exists so the Vite dev server can proxy the
|
|
70
|
+
// exchange; land on the app root afterwards either way.
|
|
71
|
+
const dest = url.pathname === "/auth" ? "/" : url.pathname + url.search;
|
|
72
|
+
return c.redirect(dest);
|
|
73
|
+
}
|
|
74
|
+
await next();
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/** /api/* gate: only cookie-authenticated sessions pass. */
|
|
78
|
+
requireAuth() {
|
|
79
|
+
return async (c, next) => {
|
|
80
|
+
if (!this.authSessions.has(getCookie(c, AUTH_COOKIE) ?? "")) {
|
|
81
|
+
return c.json({ error: "unauthorized" }, 401);
|
|
82
|
+
}
|
|
83
|
+
await next();
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function timingSafeEqualStr(a, b) {
|
|
88
|
+
const ba = Buffer.from(a);
|
|
89
|
+
const bb = Buffer.from(b);
|
|
90
|
+
return ba.length === bb.length && crypto.timingSafeEqual(ba, bb);
|
|
91
|
+
}
|
package/web-app/dist/server.js
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
import crypto from "node:crypto";
|
|
2
1
|
import fs from "node:fs";
|
|
3
2
|
import fsp from "node:fs/promises";
|
|
4
3
|
import path from "node:path";
|
|
5
4
|
import { Readable } from "node:stream";
|
|
6
5
|
import { Hono } from "hono";
|
|
7
|
-
import { getCookie, setCookie } from "hono/cookie";
|
|
8
6
|
import { serve } from "@hono/node-server";
|
|
9
7
|
import { WebSocketServer } from "ws";
|
|
10
8
|
import { execFile } from "node:child_process";
|
|
11
9
|
import { promisify } from "node:util";
|
|
12
10
|
import { SessionManager } from "./sessions.js";
|
|
11
|
+
import { AuthGate } from "./auth.js";
|
|
13
12
|
import { createFsApi } from "./fs-api.js";
|
|
14
|
-
import { createZuzuuApi
|
|
13
|
+
import { createZuzuuApi } from "./zuzuu-routes.js";
|
|
14
|
+
import { runZuzuuMut } from "./zuzuu-cli.js";
|
|
15
15
|
import { search } from "./search.js";
|
|
16
16
|
import { listFiles } from "./file-list.js";
|
|
17
17
|
import { listWorkflows, saveWorkflow } from "./workflows.js";
|
|
@@ -23,8 +23,6 @@ const execFileAsync = promisify(execFile);
|
|
|
23
23
|
import { handleTermSocket } from "./ws-term.js";
|
|
24
24
|
import { handleFsSocket } from "./ws-fs.js";
|
|
25
25
|
import { PathError, resolveSafe, safeJoin } from "./safe-path.js";
|
|
26
|
-
const AUTH_COOKIE = "webcode_auth";
|
|
27
|
-
const COOKIE_MAX_AGE = 30 * 24 * 3600;
|
|
28
26
|
/** Host CLIs an agent/command session may run. Argv-spawned, never a shell. */
|
|
29
27
|
const DEFAULT_COMMAND_ALLOWLIST = ["claude", "gemini", "codex", "pi", "opencode", "zuzuu"];
|
|
30
28
|
const STATIC_MIME = {
|
|
@@ -45,9 +43,7 @@ export class WebcodeServer {
|
|
|
45
43
|
/** mutable workspace root — switchable at runtime via switchTo() */
|
|
46
44
|
root;
|
|
47
45
|
startedAt = Date.now();
|
|
48
|
-
|
|
49
|
-
allowedHosts;
|
|
50
|
-
allowedOrigins;
|
|
46
|
+
auth;
|
|
51
47
|
commandAllowlist;
|
|
52
48
|
server = null;
|
|
53
49
|
constructor(cfg) {
|
|
@@ -55,19 +51,12 @@ export class WebcodeServer {
|
|
|
55
51
|
this.root = cfg.root;
|
|
56
52
|
this.commandAllowlist = new Set(cfg.commandAllowlist ?? DEFAULT_COMMAND_ALLOWLIST);
|
|
57
53
|
this.sessions = new SessionManager(cfg.root);
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
...
|
|
62
|
-
...(cfg.
|
|
63
|
-
|
|
64
|
-
// hosted: also accept the public hostname (Fly's edge sets Host to it);
|
|
65
|
-
// Host/Origin defense stays on, just widened to the one public origin.
|
|
66
|
-
if (cfg.publicHost) {
|
|
67
|
-
this.allowedHosts.add(cfg.publicHost.toLowerCase());
|
|
68
|
-
this.allowedOrigins.add(`https://${cfg.publicHost}`);
|
|
69
|
-
this.allowedOrigins.add(`http://${cfg.publicHost}`);
|
|
70
|
-
}
|
|
54
|
+
this.auth = new AuthGate({
|
|
55
|
+
port: cfg.port,
|
|
56
|
+
token: cfg.token,
|
|
57
|
+
...(cfg.extraOrigins !== undefined ? { extraOrigins: cfg.extraOrigins } : {}),
|
|
58
|
+
...(cfg.publicHost !== undefined ? { publicHost: cfg.publicHost } : {}),
|
|
59
|
+
});
|
|
71
60
|
this.app = this.buildApp();
|
|
72
61
|
}
|
|
73
62
|
/**
|
|
@@ -101,60 +90,13 @@ export class WebcodeServer {
|
|
|
101
90
|
return { cliAbsent: true };
|
|
102
91
|
return { ok: false, ...(r.stderr !== undefined ? { stderr: r.stderr } : {}), ...(r.data !== undefined ? { refusal: r.data } : {}) };
|
|
103
92
|
}
|
|
104
|
-
// ── security gates ─────────────────────────────────────────────────
|
|
105
|
-
/** Host allowlist defeats DNS rebinding: rebinding changes DNS, not the Host header. */
|
|
106
|
-
hostAllowed(host) {
|
|
107
|
-
return !!host && this.allowedHosts.has(host.toLowerCase());
|
|
108
|
-
}
|
|
109
|
-
/** Origin allowlist defeats cross-site WS hijacking / CSRF from arbitrary websites. */
|
|
110
|
-
originAllowed(origin) {
|
|
111
|
-
return origin === undefined || this.allowedOrigins.has(origin);
|
|
112
|
-
}
|
|
113
|
-
cookieAuthed(cookieHeader) {
|
|
114
|
-
if (!cookieHeader)
|
|
115
|
-
return false;
|
|
116
|
-
const match = /(?:^|;\s*)webcode_auth=([^;]+)/.exec(cookieHeader);
|
|
117
|
-
return !!match && this.authSessions.has(match[1]);
|
|
118
|
-
}
|
|
119
93
|
// ── HTTP app ───────────────────────────────────────────────────────
|
|
120
94
|
buildApp() {
|
|
121
95
|
const { cfg } = this;
|
|
122
96
|
const app = new Hono();
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}
|
|
127
|
-
if (!this.originAllowed(c.req.header("origin"))) {
|
|
128
|
-
return c.text("forbidden origin", 403);
|
|
129
|
-
}
|
|
130
|
-
// Token exchange: any page request carrying ?token= gets a cookie.
|
|
131
|
-
const token = c.req.query("token");
|
|
132
|
-
if (token && !c.req.path.startsWith("/api/")) {
|
|
133
|
-
if (!timingSafeEqualStr(token, cfg.token))
|
|
134
|
-
return c.text("invalid token", 403);
|
|
135
|
-
const secret = crypto.randomBytes(24).toString("base64url");
|
|
136
|
-
this.authSessions.add(secret);
|
|
137
|
-
setCookie(c, AUTH_COOKIE, secret, {
|
|
138
|
-
httpOnly: true,
|
|
139
|
-
sameSite: "Strict",
|
|
140
|
-
path: "/",
|
|
141
|
-
maxAge: COOKIE_MAX_AGE,
|
|
142
|
-
});
|
|
143
|
-
const url = new URL(c.req.url);
|
|
144
|
-
url.searchParams.delete("token");
|
|
145
|
-
// /auth?token=… exists so the Vite dev server can proxy the
|
|
146
|
-
// exchange; land on the app root afterwards either way.
|
|
147
|
-
const dest = url.pathname === "/auth" ? "/" : url.pathname + url.search;
|
|
148
|
-
return c.redirect(dest);
|
|
149
|
-
}
|
|
150
|
-
await next();
|
|
151
|
-
});
|
|
152
|
-
app.use("/api/*", async (c, next) => {
|
|
153
|
-
if (!this.authSessions.has(getCookie(c, AUTH_COOKIE) ?? "")) {
|
|
154
|
-
return c.json({ error: "unauthorized" }, 401);
|
|
155
|
-
}
|
|
156
|
-
await next();
|
|
157
|
-
});
|
|
97
|
+
// security gates live in auth.ts: Host/Origin allowlists + token→cookie
|
|
98
|
+
app.use("*", this.auth.gate());
|
|
99
|
+
app.use("/api/*", this.auth.requireAuth());
|
|
158
100
|
app.get("/api/workspace", (c) => {
|
|
159
101
|
const body = {
|
|
160
102
|
root: this.root,
|
|
@@ -434,11 +376,11 @@ export class WebcodeServer {
|
|
|
434
376
|
socket.write(`HTTP/1.1 ${status} ${msg}\r\nConnection: close\r\n\r\n`);
|
|
435
377
|
socket.destroy();
|
|
436
378
|
};
|
|
437
|
-
if (!this.hostAllowed(req.headers.host))
|
|
379
|
+
if (!this.auth.hostAllowed(req.headers.host))
|
|
438
380
|
return reject(403, "Forbidden");
|
|
439
|
-
if (!this.originAllowed(req.headers.origin))
|
|
381
|
+
if (!this.auth.originAllowed(req.headers.origin))
|
|
440
382
|
return reject(403, "Forbidden");
|
|
441
|
-
if (!this.cookieAuthed(req.headers.cookie))
|
|
383
|
+
if (!this.auth.cookieAuthed(req.headers.cookie))
|
|
442
384
|
return reject(401, "Unauthorized");
|
|
443
385
|
const url = new URL(req.url ?? "/", "http://localhost");
|
|
444
386
|
const termMatch = /^\/ws\/term\/([0-9a-f]+)$/.exec(url.pathname);
|
|
@@ -462,8 +404,3 @@ export class WebcodeServer {
|
|
|
462
404
|
this.server?.close();
|
|
463
405
|
}
|
|
464
406
|
}
|
|
465
|
-
function timingSafeEqualStr(a, b) {
|
|
466
|
-
const ba = Buffer.from(a);
|
|
467
|
-
const bb = Buffer.from(b);
|
|
468
|
-
return ba.length === bb.length && crypto.timingSafeEqual(ba, bb);
|
|
469
|
-
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// The zuzuu CLI spawn layer — the ONLY place the daemon shells out to the
|
|
2
|
+
// `zuzuu` binary. Two flavours:
|
|
3
|
+
// runZuzuu — reads: any failure (absent, non-zero, unparseable) → null;
|
|
4
|
+
// callers degrade to file-read fallbacks.
|
|
5
|
+
// runZuzuuMut — mutations + CLI-only reads: failures are distinguished
|
|
6
|
+
// (binary absent vs command failed + stderr tail) so routes
|
|
7
|
+
// can answer 503 vs 502.
|
|
8
|
+
// Always argv arrays (never a shell), time-boxed, cwd-scoped to the workspace.
|
|
9
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
10
|
+
/** Spawn `zuzuu <args> --json` in `root`. Returns parsed JSON, or null on any
|
|
11
|
+
* failure (binary absent, non-zero exit, unparseable). Read-only + time-boxed. */
|
|
12
|
+
export function runZuzuu(root, args, opts = {}) {
|
|
13
|
+
const binary = opts.binary ?? "zuzuu";
|
|
14
|
+
const timeoutMs = opts.timeoutMs ?? 5000;
|
|
15
|
+
return new Promise((resolve) => {
|
|
16
|
+
let out = "";
|
|
17
|
+
let done = false;
|
|
18
|
+
const finish = (v) => { if (!done) {
|
|
19
|
+
done = true;
|
|
20
|
+
resolve(v);
|
|
21
|
+
} };
|
|
22
|
+
let child;
|
|
23
|
+
try {
|
|
24
|
+
child = spawn(binary, [...args, "--json"], { cwd: root, stdio: ["ignore", "pipe", "ignore"] });
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
finish(null);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const timer = setTimeout(() => { try {
|
|
31
|
+
child.kill();
|
|
32
|
+
}
|
|
33
|
+
catch { /* noop */ } finish(null); }, timeoutMs);
|
|
34
|
+
child.stdout?.on("data", (b) => { out += b.toString(); });
|
|
35
|
+
child.on("error", () => { clearTimeout(timer); finish(null); });
|
|
36
|
+
child.on("close", (code) => {
|
|
37
|
+
clearTimeout(timer);
|
|
38
|
+
if (code !== 0)
|
|
39
|
+
return finish(null);
|
|
40
|
+
try {
|
|
41
|
+
finish(JSON.parse(out));
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
finish(null);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
const STDERR_TAIL = 2048;
|
|
50
|
+
/** Spawn `zuzuu <args> --json` where the caller needs to distinguish failures
|
|
51
|
+
* (mutations, and reads with no file fallback). Unlike runZuzuu: binary
|
|
52
|
+
* absent vs command failed (with a stderr tail) are separate results, so
|
|
53
|
+
* routes can answer 503 vs 502. Stdout must parse as JSON on success. */
|
|
54
|
+
export function runZuzuuMut(root, args, opts = {}) {
|
|
55
|
+
const binary = opts.binary ?? "zuzuu";
|
|
56
|
+
const timeoutMs = opts.timeoutMs ?? 10_000;
|
|
57
|
+
return new Promise((resolve) => {
|
|
58
|
+
let out = "";
|
|
59
|
+
let err = "";
|
|
60
|
+
let done = false;
|
|
61
|
+
const finish = (v) => { if (!done) {
|
|
62
|
+
done = true;
|
|
63
|
+
resolve(v);
|
|
64
|
+
} };
|
|
65
|
+
let child;
|
|
66
|
+
try {
|
|
67
|
+
child = spawn(binary, [...args, "--json"], { cwd: root, stdio: ["ignore", "pipe", "pipe"] });
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
finish({ ok: false, code: "absent" });
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const timer = setTimeout(() => {
|
|
74
|
+
try {
|
|
75
|
+
child.kill();
|
|
76
|
+
}
|
|
77
|
+
catch { /* noop */ }
|
|
78
|
+
finish({ ok: false, code: "failed", stderr: "zuzuu timed out" });
|
|
79
|
+
}, timeoutMs);
|
|
80
|
+
child.stdout?.on("data", (b) => { out += b.toString(); });
|
|
81
|
+
child.stderr?.on("data", (b) => {
|
|
82
|
+
err += b.toString();
|
|
83
|
+
if (err.length > STDERR_TAIL)
|
|
84
|
+
err = err.slice(-STDERR_TAIL);
|
|
85
|
+
});
|
|
86
|
+
child.on("error", (e) => {
|
|
87
|
+
clearTimeout(timer);
|
|
88
|
+
if (e.code === "ENOENT")
|
|
89
|
+
finish({ ok: false, code: "absent" });
|
|
90
|
+
else
|
|
91
|
+
finish({ ok: false, code: "failed", stderr: e.message });
|
|
92
|
+
});
|
|
93
|
+
child.on("close", (code) => {
|
|
94
|
+
clearTimeout(timer);
|
|
95
|
+
if (code !== 0) {
|
|
96
|
+
// zuzuu prints structured JSON even on refusals (exit 1, e.g.
|
|
97
|
+
// empty-squash-with-checkpoints) — keep it so the UI can act on reason.
|
|
98
|
+
try {
|
|
99
|
+
const parsed = JSON.parse(out);
|
|
100
|
+
return finish({ ok: false, code: "failed", stderr: err.slice(-STDERR_TAIL), data: parsed });
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return finish({ ok: false, code: "failed", stderr: err.slice(-STDERR_TAIL) });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
finish({ ok: true, data: JSON.parse(out) });
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
finish({ ok: false, code: "failed", stderr: "unparseable JSON from zuzuu" });
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
/** Best-effort: is the zuzuu binary runnable? */
|
|
116
|
+
export function binAvailable(binary) {
|
|
117
|
+
try {
|
|
118
|
+
const r = spawnSync(binary, ["version"], { stdio: "ignore", timeout: 3000 });
|
|
119
|
+
return !r.error && r.status === 0;
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -1,133 +1,20 @@
|
|
|
1
1
|
// /api/zuzuu/* — observe + act routes over a project's zuzuu `.zuzuu/` home.
|
|
2
2
|
// Reads: raw data (proposals, generations, sessions, digest) comes from disk;
|
|
3
3
|
// computed views (status, inbox, eval, generation diff) shell out to
|
|
4
|
-
// `zuzuu <cmd> --json`
|
|
4
|
+
// `zuzuu <cmd> --json` (the spawn layer lives in zuzuu-cli.ts) and fall back
|
|
5
|
+
// to file-reads when the binary is absent.
|
|
5
6
|
// Writes: mutations (approve/reject, mint, rollback) are CLI-ONLY — the daemon
|
|
6
7
|
// never reimplements faculty writes; no CLI → 503. Mirrors fs-api.ts.
|
|
7
8
|
import fsp from "node:fs/promises";
|
|
8
9
|
import { existsSync } from "node:fs";
|
|
9
|
-
import { spawn, spawnSync } from "node:child_process";
|
|
10
10
|
import path from "node:path";
|
|
11
11
|
import { Hono } from "hono";
|
|
12
12
|
import { PathError, resolveSafe } from "./safe-path.js";
|
|
13
|
+
import { binAvailable, runZuzuu, runZuzuuMut } from "./zuzuu-cli.js";
|
|
13
14
|
const FACULTIES = ["knowledge", "memory", "actions", "instructions", "guardrails"];
|
|
14
15
|
/** Ids/slugs/generation-ids that may ride into a zuzuu argv. Validated BEFORE any spawn. */
|
|
15
16
|
const SAFE_ID = /^[a-z0-9][a-z0-9._-]{0,127}$/i;
|
|
16
17
|
const MAX_REASON_LEN = 500;
|
|
17
|
-
/** Spawn `zuzuu <args> --json` in `root`. Returns parsed JSON, or null on any
|
|
18
|
-
* failure (binary absent, non-zero exit, unparseable). Read-only + time-boxed. */
|
|
19
|
-
export function runZuzuu(root, args, opts = {}) {
|
|
20
|
-
const binary = opts.binary ?? "zuzuu";
|
|
21
|
-
const timeoutMs = opts.timeoutMs ?? 5000;
|
|
22
|
-
return new Promise((resolve) => {
|
|
23
|
-
let out = "";
|
|
24
|
-
let done = false;
|
|
25
|
-
const finish = (v) => { if (!done) {
|
|
26
|
-
done = true;
|
|
27
|
-
resolve(v);
|
|
28
|
-
} };
|
|
29
|
-
let child;
|
|
30
|
-
try {
|
|
31
|
-
child = spawn(binary, [...args, "--json"], { cwd: root, stdio: ["ignore", "pipe", "ignore"] });
|
|
32
|
-
}
|
|
33
|
-
catch {
|
|
34
|
-
finish(null);
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
const timer = setTimeout(() => { try {
|
|
38
|
-
child.kill();
|
|
39
|
-
}
|
|
40
|
-
catch { /* noop */ } finish(null); }, timeoutMs);
|
|
41
|
-
child.stdout?.on("data", (b) => { out += b.toString(); });
|
|
42
|
-
child.on("error", () => { clearTimeout(timer); finish(null); });
|
|
43
|
-
child.on("close", (code) => {
|
|
44
|
-
clearTimeout(timer);
|
|
45
|
-
if (code !== 0)
|
|
46
|
-
return finish(null);
|
|
47
|
-
try {
|
|
48
|
-
finish(JSON.parse(out));
|
|
49
|
-
}
|
|
50
|
-
catch {
|
|
51
|
-
finish(null);
|
|
52
|
-
}
|
|
53
|
-
});
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
const STDERR_TAIL = 2048;
|
|
57
|
-
/** Spawn `zuzuu <args> --json` for a MUTATION. Unlike runZuzuu, failures are
|
|
58
|
-
* distinguished: binary absent vs command failed (with a stderr tail), so
|
|
59
|
-
* routes can answer 503 vs 502. Stdout must parse as JSON on success. */
|
|
60
|
-
export function runZuzuuMut(root, args, opts = {}) {
|
|
61
|
-
const binary = opts.binary ?? "zuzuu";
|
|
62
|
-
const timeoutMs = opts.timeoutMs ?? 10_000;
|
|
63
|
-
return new Promise((resolve) => {
|
|
64
|
-
let out = "";
|
|
65
|
-
let err = "";
|
|
66
|
-
let done = false;
|
|
67
|
-
const finish = (v) => { if (!done) {
|
|
68
|
-
done = true;
|
|
69
|
-
resolve(v);
|
|
70
|
-
} };
|
|
71
|
-
let child;
|
|
72
|
-
try {
|
|
73
|
-
child = spawn(binary, [...args, "--json"], { cwd: root, stdio: ["ignore", "pipe", "pipe"] });
|
|
74
|
-
}
|
|
75
|
-
catch {
|
|
76
|
-
finish({ ok: false, code: "absent" });
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
const timer = setTimeout(() => {
|
|
80
|
-
try {
|
|
81
|
-
child.kill();
|
|
82
|
-
}
|
|
83
|
-
catch { /* noop */ }
|
|
84
|
-
finish({ ok: false, code: "failed", stderr: "zuzuu timed out" });
|
|
85
|
-
}, timeoutMs);
|
|
86
|
-
child.stdout?.on("data", (b) => { out += b.toString(); });
|
|
87
|
-
child.stderr?.on("data", (b) => {
|
|
88
|
-
err += b.toString();
|
|
89
|
-
if (err.length > STDERR_TAIL)
|
|
90
|
-
err = err.slice(-STDERR_TAIL);
|
|
91
|
-
});
|
|
92
|
-
child.on("error", (e) => {
|
|
93
|
-
clearTimeout(timer);
|
|
94
|
-
if (e.code === "ENOENT")
|
|
95
|
-
finish({ ok: false, code: "absent" });
|
|
96
|
-
else
|
|
97
|
-
finish({ ok: false, code: "failed", stderr: e.message });
|
|
98
|
-
});
|
|
99
|
-
child.on("close", (code) => {
|
|
100
|
-
clearTimeout(timer);
|
|
101
|
-
if (code !== 0) {
|
|
102
|
-
// zuzuu prints structured JSON even on refusals (exit 1, e.g.
|
|
103
|
-
// empty-squash-with-checkpoints) — keep it so the UI can act on reason.
|
|
104
|
-
try {
|
|
105
|
-
const parsed = JSON.parse(out);
|
|
106
|
-
return finish({ ok: false, code: "failed", stderr: err.slice(-STDERR_TAIL), data: parsed });
|
|
107
|
-
}
|
|
108
|
-
catch {
|
|
109
|
-
return finish({ ok: false, code: "failed", stderr: err.slice(-STDERR_TAIL) });
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
try {
|
|
113
|
-
finish({ ok: true, data: JSON.parse(out) });
|
|
114
|
-
}
|
|
115
|
-
catch {
|
|
116
|
-
finish({ ok: false, code: "failed", stderr: "unparseable JSON from zuzuu" });
|
|
117
|
-
}
|
|
118
|
-
});
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
/** Best-effort: is the zuzuu binary runnable? */
|
|
122
|
-
function binAvailable(binary) {
|
|
123
|
-
try {
|
|
124
|
-
const r = spawnSync(binary, ["version"], { stdio: "ignore", timeout: 3000 });
|
|
125
|
-
return !r.error && r.status === 0;
|
|
126
|
-
}
|
|
127
|
-
catch {
|
|
128
|
-
return false;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
18
|
// ── Faculty Standard envelope listing ────────────────────────────────────
|
|
132
19
|
// The CLI is the parser of record (`zuzuu faculty items <f> --json` returns
|
|
133
20
|
// the full envelopes incl. payload/body). When it's absent we degrade to a
|
|
@@ -278,6 +165,28 @@ export function createZuzuuApi(getRoot, opts = {}) {
|
|
|
278
165
|
}));
|
|
279
166
|
return c.json({ faculties });
|
|
280
167
|
});
|
|
168
|
+
// The batched faculty surface: ONE `zuzuu faculty overview --json` spawn
|
|
169
|
+
// covers all faculties (manifest ui descriptors + counts + top titles +
|
|
170
|
+
// pending) — replaces the 5-spawn-per-cycle /faculties pattern for the
|
|
171
|
+
// panel root. CLI absent → peek fallback (counts survive, ui descriptors
|
|
172
|
+
// degrade to the web kit's built-in metadata).
|
|
173
|
+
app.get("/overview", async (c) => {
|
|
174
|
+
const viaCli = await runZuzuu(root, ["faculty", "overview"], { binary: opts.binary });
|
|
175
|
+
if (viaCli && Array.isArray(viaCli.faculties))
|
|
176
|
+
return c.json(viaCli);
|
|
177
|
+
const agent = await agentDir();
|
|
178
|
+
const faculties = await Promise.all(FACULTIES.map(async (id) => {
|
|
179
|
+
const [items, proposals] = await Promise.all([peekFacultyItems(agent, id), proposalsOf(agent, id)]);
|
|
180
|
+
return {
|
|
181
|
+
id,
|
|
182
|
+
title: id.charAt(0).toUpperCase() + id.slice(1),
|
|
183
|
+
counts: { items: items.length, pending: proposals.length, errors: 0 },
|
|
184
|
+
top: items.slice(0, 3).map((it) => String(it.title ?? it.id)),
|
|
185
|
+
declarative: false,
|
|
186
|
+
};
|
|
187
|
+
}));
|
|
188
|
+
return c.json({ faculties, degraded: true });
|
|
189
|
+
});
|
|
281
190
|
app.get("/faculty/:key", async (c) => {
|
|
282
191
|
const key = c.req.param("key");
|
|
283
192
|
if (!FACULTIES.includes(key))
|
|
@@ -320,7 +229,13 @@ export function createZuzuuApi(getRoot, opts = {}) {
|
|
|
320
229
|
generations: gens.map((g) => ({ id: String(g.id), mintedAt: g.mintedAt ?? null, mintedFrom: g.mintedFrom ?? [] })),
|
|
321
230
|
});
|
|
322
231
|
});
|
|
232
|
+
// Sessions list: CLI-first (`zuzuu sessions --json` — state-labelled:
|
|
233
|
+
// active|completed|abandoned|crashed|captured); falls back to the raw
|
|
234
|
+
// sessions.json index (no state labels) when the CLI is absent.
|
|
323
235
|
app.get("/sessions", async (c) => {
|
|
236
|
+
const viaCli = await runZuzuu(root, ["sessions"], { binary: opts.binary });
|
|
237
|
+
if (viaCli && Array.isArray(viaCli.sessions))
|
|
238
|
+
return c.json(viaCli);
|
|
324
239
|
const agent = await agentDir();
|
|
325
240
|
try {
|
|
326
241
|
const idx = JSON.parse(await fsp.readFile(path.join(agent, "sessions.json"), "utf8"));
|
|
@@ -330,6 +245,21 @@ export function createZuzuuApi(getRoot, opts = {}) {
|
|
|
330
245
|
return c.json({ sessions: [] });
|
|
331
246
|
}
|
|
332
247
|
});
|
|
248
|
+
// One session's observability document (`zuzuu session inspect <id> --json`:
|
|
249
|
+
// trace summary + per-faculty mined signals + warnings). CLI-only — the
|
|
250
|
+
// daemon never re-mines transcripts; absent → 503, failed → 502.
|
|
251
|
+
app.get("/session-inspect/:id", async (c) => {
|
|
252
|
+
const id = c.req.param("id");
|
|
253
|
+
if (!SAFE_ID.test(id))
|
|
254
|
+
return c.json({ error: "bad id" }, 400);
|
|
255
|
+
const r = await runZuzuuMut(root, ["session", "inspect", id], { binary: opts.binary });
|
|
256
|
+
if (!r.ok) {
|
|
257
|
+
return r.code === "absent"
|
|
258
|
+
? c.json({ error: "zuzuu CLI required" }, 503)
|
|
259
|
+
: c.json({ error: "zuzuu command failed", stderr: r.stderr ?? "", data: r.data ?? null }, 502);
|
|
260
|
+
}
|
|
261
|
+
return c.json(r.data);
|
|
262
|
+
});
|
|
333
263
|
app.get("/digest", async (c) => {
|
|
334
264
|
const agent = await agentDir();
|
|
335
265
|
try {
|