@rubytech/create-maxy-code 0.1.561 → 0.1.563
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/package.json +1 -1
- package/payload/platform/plugins/admin/hooks/__tests__/datetime-inject.test.sh +10 -0
- package/payload/platform/plugins/admin/hooks/__tests__/mailbox-inject.test.sh +15 -2
- package/payload/platform/plugins/admin/hooks/__tests__/preference-consult-directive.test.sh +6 -0
- package/payload/platform/plugins/admin/hooks/__tests__/prompt-optimiser-directive.test.sh +10 -0
- package/payload/platform/plugins/admin/hooks/datetime-inject.sh +10 -0
- package/payload/platform/plugins/admin/hooks/mailbox-inject.sh +14 -0
- package/payload/platform/plugins/admin/hooks/preference-consult-directive.sh +9 -0
- package/payload/platform/plugins/admin/hooks/prompt-optimiser-directive.sh +10 -0
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +21 -11
- package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +11 -0
- package/payload/platform/plugins/cloudflare/bin/portal-enrol.mjs +19 -5
- package/payload/platform/plugins/cloudflare/mcp/__tests__/authorize.test.ts +6 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-enrol.test.ts +77 -6
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-session-grant.test.ts +39 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-visit-status-write.test.ts +176 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-visits-scope.test.ts +112 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/SKILL.md +23 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/schema.sql +94 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/session.ts +31 -4
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/visit-status.ts +136 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/visits.ts +93 -0
- package/payload/platform/plugins/docs/references/deployment.md +14 -8
- package/payload/platform/plugins/docs/references/internals.md +2 -0
- package/payload/platform/plugins/docs/references/platform.md +4 -2
- package/payload/platform/scripts/__tests__/public-prompt-gate-registered.test.sh +114 -0
- package/payload/platform/scripts/smoke-boot-services.sh +14 -0
- package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/http-server.js +26 -10
- package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/index.js +16 -0
- package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/public-prompt-surface.d.ts +56 -0
- package/payload/platform/services/claude-session-manager/dist/public-prompt-surface.d.ts.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/public-prompt-surface.js +155 -0
- package/payload/platform/services/claude-session-manager/dist/public-prompt-surface.js.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/rc-public-lockdown.d.ts +6 -0
- package/payload/platform/services/claude-session-manager/dist/rc-public-lockdown.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/rc-public-lockdown.js +1 -1
- package/payload/platform/services/claude-session-manager/dist/rc-public-lockdown.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/system-prompt.d.ts +4 -0
- package/payload/platform/services/claude-session-manager/dist/system-prompt.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/system-prompt.js +45 -27
- package/payload/platform/services/claude-session-manager/dist/system-prompt.js.map +1 -1
- package/payload/platform/templates/agents/public/IDENTITY.md +3 -3
- package/payload/server/maxy-edge.js +302 -33
- package/payload/server/public/activity.html +5 -5
- package/payload/server/public/agents.html +4 -4
- package/payload/server/public/assets/{AdminLoginScreens-DLogIsJT.js → AdminLoginScreens-C6e5jf0S.js} +1 -1
- package/payload/server/public/assets/AdminLoginScreens-C6e5jf0S.js.br +0 -0
- package/payload/server/public/assets/AdminLoginScreens-C6e5jf0S.js.gz +0 -0
- package/payload/server/public/assets/AdminShell-DStA0lfm.js +3 -0
- package/payload/server/public/assets/AdminShell-DStA0lfm.js.br +0 -0
- package/payload/server/public/assets/AdminShell-DStA0lfm.js.gz +0 -0
- package/payload/server/public/assets/{activity-AVJSFESV.js → activity--yyBcNfB.js} +1 -1
- package/payload/server/public/assets/activity--yyBcNfB.js.br +0 -0
- package/payload/server/public/assets/activity--yyBcNfB.js.gz +0 -0
- package/payload/server/public/assets/{admin-eFgbld42.js → admin-Dr6Jwe7W.js} +1 -1
- package/payload/server/public/assets/admin-Dr6Jwe7W.js.br +0 -0
- package/payload/server/public/assets/admin-Dr6Jwe7W.js.gz +0 -0
- package/payload/server/public/assets/{agents-CoWLZMf7.js → agents-odyVOb5E.js} +1 -1
- package/payload/server/public/assets/agents-odyVOb5E.js.br +0 -0
- package/payload/server/public/assets/agents-odyVOb5E.js.gz +0 -0
- package/payload/server/public/assets/{browser-C6K66_fw.js → browser-D1K_MoHt.js} +1 -1
- package/payload/server/public/assets/browser-D1K_MoHt.js.br +0 -0
- package/payload/server/public/assets/browser-D1K_MoHt.js.gz +0 -0
- package/payload/server/public/assets/{calendar-Dnw_cP4b.js → calendar-jqh9U7Ic.js} +1 -1
- package/payload/server/public/assets/calendar-jqh9U7Ic.js.br +0 -0
- package/payload/server/public/assets/calendar-jqh9U7Ic.js.gz +0 -0
- package/payload/server/public/assets/chat-QQHBdVor.js +1 -0
- package/payload/server/public/assets/chat-QQHBdVor.js.br +0 -0
- package/payload/server/public/assets/chat-QQHBdVor.js.gz +0 -0
- package/payload/server/public/assets/chevron-left-DwTXgKwr.js +1 -0
- package/payload/server/public/assets/clock-DOmh9iKX.js +1 -0
- package/payload/server/public/assets/clock-DOmh9iKX.js.br +0 -0
- package/payload/server/public/assets/{copy-kuNM0Etw.js → copy-DkuHwkaB.js} +1 -1
- package/payload/server/public/assets/copy-DkuHwkaB.js.br +0 -0
- package/payload/server/public/assets/copy-DkuHwkaB.js.gz +0 -0
- package/payload/server/public/assets/data-DYmTNqrb.js +1 -0
- package/payload/server/public/assets/data-DYmTNqrb.js.br +0 -0
- package/payload/server/public/assets/data-DYmTNqrb.js.gz +0 -0
- package/payload/server/public/assets/{file-text-BWkobrcx.js → file-text-C6o_NRjj.js} +1 -1
- package/payload/server/public/assets/file-text-C6o_NRjj.js.br +0 -0
- package/payload/server/public/assets/file-text-C6o_NRjj.js.gz +0 -0
- package/payload/server/public/assets/{graph-BGjTL5Vz.js → graph-BN30v5hJ.js} +1 -1
- package/payload/server/public/assets/graph-BN30v5hJ.js.br +0 -0
- package/payload/server/public/assets/graph-BN30v5hJ.js.gz +0 -0
- package/payload/server/public/assets/{graph-labels-Dvc5Kw_r.js → graph-labels-DHFyNXQt.js} +1 -1
- package/payload/server/public/assets/graph-labels-DHFyNXQt.js.br +0 -0
- package/payload/server/public/assets/graph-labels-DHFyNXQt.js.gz +0 -0
- package/payload/server/public/assets/{operator-CzBGv_he.js → operator-DwFYA4Fp.js} +1 -1
- package/payload/server/public/assets/operator-DwFYA4Fp.js.br +0 -0
- package/payload/server/public/assets/operator-DwFYA4Fp.js.gz +0 -0
- package/payload/server/public/assets/{page-DaySqamH.js → page-BALVRssF.js} +1 -1
- package/payload/server/public/assets/page-BALVRssF.js.br +0 -0
- package/payload/server/public/assets/page-BALVRssF.js.gz +0 -0
- package/payload/server/public/assets/{page-BIY4Ku8Q.js → page-CEWIP3VF.js} +1 -1
- package/payload/server/public/assets/page-CEWIP3VF.js.br +0 -0
- package/payload/server/public/assets/page-CEWIP3VF.js.gz +0 -0
- package/payload/server/public/assets/play-Bg-BY_oj.js +1 -0
- package/payload/server/public/assets/play-Bg-BY_oj.js.br +0 -0
- package/payload/server/public/assets/play-Bg-BY_oj.js.gz +0 -0
- package/payload/server/public/assets/{public-pwDSvhBv.js → public-Ceb0n7Km.js} +1 -1
- package/payload/server/public/assets/public-Ceb0n7Km.js.br +0 -0
- package/payload/server/public/assets/public-Ceb0n7Km.js.gz +0 -0
- package/payload/server/public/assets/{rotate-ccw-B6s7tkEt.js → rotate-ccw-CDYdx91V.js} +1 -1
- package/payload/server/public/assets/rotate-ccw-CDYdx91V.js.gz +0 -0
- package/payload/server/public/assets/{routines-CFlyRdYg.js → routines-DwNTih0y.js} +1 -1
- package/payload/server/public/assets/routines-DwNTih0y.js.br +0 -0
- package/payload/server/public/assets/routines-DwNTih0y.js.gz +0 -0
- package/payload/server/public/assets/{skills-C45kfL_Q.js → skills-TIDCdOLQ.js} +1 -1
- package/payload/server/public/assets/skills-TIDCdOLQ.js.br +0 -0
- package/payload/server/public/assets/skills-TIDCdOLQ.js.gz +0 -0
- package/payload/server/public/assets/{tasks-BwoCTCu5.js → tasks-eTXvaWnd.js} +1 -1
- package/payload/server/public/assets/tasks-eTXvaWnd.js.br +0 -0
- package/payload/server/public/assets/tasks-eTXvaWnd.js.gz +0 -0
- package/payload/server/public/assets/{triangle-alert-B3bthSzx.js → triangle-alert-JwLSrj17.js} +1 -1
- package/payload/server/public/assets/triangle-alert-JwLSrj17.js.br +0 -0
- package/payload/server/public/assets/triangle-alert-JwLSrj17.js.gz +0 -0
- package/payload/server/public/assets/{useCopyFeedback-BzJAkFEb.js → useCopyFeedback-DjTkNw2p.js} +1 -1
- package/payload/server/public/assets/useCopyFeedback-DjTkNw2p.js.br +0 -0
- package/payload/server/public/assets/useCopyFeedback-DjTkNw2p.js.gz +0 -0
- package/payload/server/public/assets/useMediaQuery-BsBzbWaf.css +1 -0
- package/payload/server/public/assets/useMediaQuery-BsBzbWaf.css.br +0 -0
- package/payload/server/public/assets/useMediaQuery-BsBzbWaf.css.gz +0 -0
- package/payload/server/public/assets/useMediaQuery-D8VDA0RN.js +14 -0
- package/payload/server/public/assets/useMediaQuery-D8VDA0RN.js.br +0 -0
- package/payload/server/public/assets/useMediaQuery-D8VDA0RN.js.gz +0 -0
- package/payload/server/public/assets/{useVoiceRecorder-_qI_BAOo.js → useVoiceRecorder-B8vxObZG.js} +1 -1
- package/payload/server/public/assets/useVoiceRecorder-B8vxObZG.js.br +0 -0
- package/payload/server/public/assets/useVoiceRecorder-B8vxObZG.js.gz +0 -0
- package/payload/server/public/assets/{wrench-Bqux9jaw.js → wrench-DutUiRu7.js} +1 -1
- package/payload/server/public/assets/wrench-DutUiRu7.js.br +0 -0
- package/payload/server/public/assets/wrench-DutUiRu7.js.gz +0 -0
- package/payload/server/public/browser.html +4 -4
- package/payload/server/public/calendar.html +7 -7
- package/payload/server/public/chat.html +13 -13
- package/payload/server/public/data.html +11 -11
- package/payload/server/public/graph.html +9 -9
- package/payload/server/public/index.html +13 -13
- package/payload/server/public/operator.html +14 -14
- package/payload/server/public/public.html +13 -13
- package/payload/server/public/routines.html +7 -7
- package/payload/server/public/skills.html +5 -5
- package/payload/server/public/tasks.html +6 -6
- package/payload/server/server.js +1118 -636
- package/payload/server/public/assets/AdminLoginScreens-DLogIsJT.js.br +0 -0
- package/payload/server/public/assets/AdminLoginScreens-DLogIsJT.js.gz +0 -0
- package/payload/server/public/assets/AdminShell-B67tPTyE.js +0 -2
- package/payload/server/public/assets/AdminShell-B67tPTyE.js.br +0 -0
- package/payload/server/public/assets/AdminShell-B67tPTyE.js.gz +0 -0
- package/payload/server/public/assets/activity-AVJSFESV.js.br +0 -0
- package/payload/server/public/assets/activity-AVJSFESV.js.gz +0 -0
- package/payload/server/public/assets/admin-eFgbld42.js.br +0 -0
- package/payload/server/public/assets/admin-eFgbld42.js.gz +0 -0
- package/payload/server/public/assets/agents-CoWLZMf7.js.br +0 -0
- package/payload/server/public/assets/agents-CoWLZMf7.js.gz +0 -0
- package/payload/server/public/assets/browser-C6K66_fw.js.br +0 -0
- package/payload/server/public/assets/browser-C6K66_fw.js.gz +0 -0
- package/payload/server/public/assets/calendar-Dnw_cP4b.js.br +0 -0
- package/payload/server/public/assets/calendar-Dnw_cP4b.js.gz +0 -0
- package/payload/server/public/assets/chat-Cm0fkpg1.js +0 -1
- package/payload/server/public/assets/chat-Cm0fkpg1.js.br +0 -0
- package/payload/server/public/assets/chat-Cm0fkpg1.js.gz +0 -0
- package/payload/server/public/assets/chevron-left-CjRH-BtF.js +0 -1
- package/payload/server/public/assets/chevron-left-CjRH-BtF.js.br +0 -0
- package/payload/server/public/assets/clock-wbYEt1kM.js +0 -1
- package/payload/server/public/assets/clock-wbYEt1kM.js.br +0 -0
- package/payload/server/public/assets/clock-wbYEt1kM.js.gz +0 -0
- package/payload/server/public/assets/copy-kuNM0Etw.js.br +0 -0
- package/payload/server/public/assets/copy-kuNM0Etw.js.gz +0 -0
- package/payload/server/public/assets/data-c47T2NCd.js +0 -1
- package/payload/server/public/assets/data-c47T2NCd.js.br +0 -1
- package/payload/server/public/assets/data-c47T2NCd.js.gz +0 -0
- package/payload/server/public/assets/file-text-BWkobrcx.js.br +0 -0
- package/payload/server/public/assets/file-text-BWkobrcx.js.gz +0 -0
- package/payload/server/public/assets/graph-BGjTL5Vz.js.br +0 -0
- package/payload/server/public/assets/graph-BGjTL5Vz.js.gz +0 -0
- package/payload/server/public/assets/graph-labels-Dvc5Kw_r.js.br +0 -0
- package/payload/server/public/assets/graph-labels-Dvc5Kw_r.js.gz +0 -0
- package/payload/server/public/assets/operator-CzBGv_he.js.br +0 -0
- package/payload/server/public/assets/operator-CzBGv_he.js.gz +0 -0
- package/payload/server/public/assets/page-BIY4Ku8Q.js.br +0 -0
- package/payload/server/public/assets/page-BIY4Ku8Q.js.gz +0 -0
- package/payload/server/public/assets/page-DaySqamH.js.br +0 -0
- package/payload/server/public/assets/page-DaySqamH.js.gz +0 -0
- package/payload/server/public/assets/play-C8RA52_i.js +0 -1
- package/payload/server/public/assets/play-C8RA52_i.js.br +0 -0
- package/payload/server/public/assets/play-C8RA52_i.js.gz +0 -0
- package/payload/server/public/assets/public-pwDSvhBv.js.br +0 -0
- package/payload/server/public/assets/public-pwDSvhBv.js.gz +0 -0
- package/payload/server/public/assets/rotate-ccw-B6s7tkEt.js.br +0 -0
- package/payload/server/public/assets/rotate-ccw-B6s7tkEt.js.gz +0 -0
- package/payload/server/public/assets/routines-CFlyRdYg.js.br +0 -0
- package/payload/server/public/assets/routines-CFlyRdYg.js.gz +0 -0
- package/payload/server/public/assets/skills-C45kfL_Q.js.br +0 -0
- package/payload/server/public/assets/skills-C45kfL_Q.js.gz +0 -0
- package/payload/server/public/assets/tasks-BwoCTCu5.js.br +0 -0
- package/payload/server/public/assets/tasks-BwoCTCu5.js.gz +0 -0
- package/payload/server/public/assets/triangle-alert-B3bthSzx.js.br +0 -0
- package/payload/server/public/assets/triangle-alert-B3bthSzx.js.gz +0 -0
- package/payload/server/public/assets/useCopyFeedback-BzJAkFEb.js.br +0 -0
- package/payload/server/public/assets/useCopyFeedback-BzJAkFEb.js.gz +0 -0
- package/payload/server/public/assets/useMediaQuery-DJ6XeNN7.js +0 -14
- package/payload/server/public/assets/useMediaQuery-DJ6XeNN7.js.br +0 -0
- package/payload/server/public/assets/useMediaQuery-DJ6XeNN7.js.gz +0 -0
- package/payload/server/public/assets/useMediaQuery-DW_xRrMl.css +0 -1
- package/payload/server/public/assets/useMediaQuery-DW_xRrMl.css.br +0 -0
- package/payload/server/public/assets/useMediaQuery-DW_xRrMl.css.gz +0 -0
- package/payload/server/public/assets/useVoiceRecorder-_qI_BAOo.js.br +0 -0
- package/payload/server/public/assets/useVoiceRecorder-_qI_BAOo.js.gz +0 -0
- package/payload/server/public/assets/wrench-Bqux9jaw.js.br +0 -0
- package/payload/server/public/assets/wrench-Bqux9jaw.js.gz +0 -0
package/package.json
CHANGED
|
@@ -114,6 +114,16 @@ else
|
|
|
114
114
|
fi
|
|
115
115
|
rm -rf "$FAKEBIN2"
|
|
116
116
|
|
|
117
|
+
# Task 2424 — a public visitor's turn carries only its agent's authored
|
|
118
|
+
# IDENTITY/SOUL/KNOWLEDGE. MAXY_PUBLIC_ALLOWED_TOOLS is stamped for role=public
|
|
119
|
+
# spawns only, by both spawn paths.
|
|
120
|
+
pub_out=$(printf '%s' '{"session_id":"x","prompt":"hi"}' | MAXY_PUBLIC_ALLOWED_TOOLS='mcp__channel__reply' bash "$HOOK" 2>/dev/null); pub_rc=$?
|
|
121
|
+
if [[ "$pub_rc" -eq 0 && -z "$pub_out" ]]; then
|
|
122
|
+
echo "PASS: public turn exits 0 with no stdout"; PASS=$((PASS+1))
|
|
123
|
+
else
|
|
124
|
+
echo "FAIL: public turn injected (exit=$pub_rc stdout=$pub_out)" >&2; FAIL=$((FAIL+1))
|
|
125
|
+
fi
|
|
126
|
+
|
|
117
127
|
echo "----"
|
|
118
128
|
echo "PASS=$PASS FAIL=$FAIL"
|
|
119
129
|
[[ "$FAIL" -eq 0 ]]
|
|
@@ -493,13 +493,26 @@ else
|
|
|
493
493
|
fi
|
|
494
494
|
rm -rf "$A"
|
|
495
495
|
|
|
496
|
+
# Task 2424 — a public visitor's turn must never carry the operator's mailbox
|
|
497
|
+
# list. This hook's only role gate was "is ACCOUNT_DIR set", which held on the
|
|
498
|
+
# pty-spawner path but not on the rc-spawn path, where buildRcChildEnv stamps it
|
|
499
|
+
# unconditionally. MAXY_PUBLIC_ALLOWED_TOOLS is stamped for role=public only.
|
|
500
|
+
PUB_A=$(mktemp -d)
|
|
501
|
+
PUB_OUT=$(printf '%s' '{"session_id":"x","prompt":"hi"}' | ACCOUNT_DIR="$PUB_A" MAXY_PUBLIC_ALLOWED_TOOLS='mcp__channel__reply' bash "$HOOK" 2>/dev/null); PUB_RC=$?
|
|
502
|
+
if [ "$PUB_RC" -eq 0 ] && [ -z "$PUB_OUT" ]; then
|
|
503
|
+
ok "public turn exits 0 with no stdout even with ACCOUNT_DIR set"
|
|
504
|
+
else
|
|
505
|
+
bad "public turn injected (exit=$PUB_RC stdout=$PUB_OUT)"
|
|
506
|
+
fi
|
|
507
|
+
rm -rf "$PUB_A"
|
|
508
|
+
|
|
496
509
|
echo "----"
|
|
497
510
|
echo "PASS=$PASS FAIL=$FAIL"
|
|
498
511
|
# The count is asserted, not just the failures: a case that stops running (a
|
|
499
512
|
# broken helper, a guard, an early exit) otherwise leaves a green suite that
|
|
500
513
|
# tested less than it claims, and .husky/pre-commit reads only the exit status.
|
|
501
|
-
EXPECTED=
|
|
502
|
-
if [ "$(id -u)" -eq 0 ]; then EXPECTED=
|
|
514
|
+
EXPECTED=29
|
|
515
|
+
if [ "$(id -u)" -eq 0 ]; then EXPECTED=27; fi
|
|
503
516
|
if [[ $((PASS+FAIL)) -ne "$EXPECTED" ]]; then
|
|
504
517
|
echo "FAIL: expected $EXPECTED cases, ran $((PASS+FAIL))" >&2
|
|
505
518
|
exit 1
|
|
@@ -10,6 +10,12 @@ check 'printf "%s" "$OUT" | grep -q "hookSpecificOutput"' "emits envelope"
|
|
|
10
10
|
check 'printf "%s" "$OUT" | grep -q "profile-read"' "names profile-read"
|
|
11
11
|
check 'printf "%s" "$OUT" | grep -q "output/"' "names output scratch rule"
|
|
12
12
|
check '! printf "%s" "$OUT" | grep -q "—"' "no em-dash"
|
|
13
|
+
# Task 2424 — a public visitor's turn carries only its agent's authored
|
|
14
|
+
# IDENTITY/SOUL/KNOWLEDGE. MAXY_PUBLIC_ALLOWED_TOOLS is stamped for role=public
|
|
15
|
+
# spawns only, by both spawn paths.
|
|
16
|
+
OUT_PUB=$(printf '{"session_id":"x","prompt":"hi"}' | MAXY_PUBLIC_ALLOWED_TOOLS='mcp__channel__reply' bash "$HOOK" 2>/dev/null); RC_PUB=$?
|
|
17
|
+
check '[ "$RC_PUB" = "0" ]' "public turn exits 0"
|
|
18
|
+
check '[ -z "$OUT_PUB" ]' "public turn emits nothing on stdout"
|
|
13
19
|
echo "----- $PASS passed, $FAIL failed -----"
|
|
14
20
|
for f in "${FAILED[@]:-}"; do [ -n "$f" ] && echo " $f"; done
|
|
15
21
|
[ "$FAIL" -eq 0 ]
|
|
@@ -195,6 +195,16 @@ else
|
|
|
195
195
|
echo "FAIL: false-suppression on a normal prompt (exit=$norm_exit stdout=$norm_out)" >&2; FAIL=$((FAIL+1))
|
|
196
196
|
fi
|
|
197
197
|
|
|
198
|
+
# Task 2424 — a public visitor's turn carries only its agent's authored
|
|
199
|
+
# IDENTITY/SOUL/KNOWLEDGE. MAXY_PUBLIC_ALLOWED_TOOLS is stamped for role=public
|
|
200
|
+
# spawns only, by both spawn paths.
|
|
201
|
+
pub_out=$(printf '%s' '{"session_id":"x","prompt":"hi"}' | MAXY_PUBLIC_ALLOWED_TOOLS='mcp__channel__reply' bash "$HOOK" 2>/dev/null); pub_rc=$?
|
|
202
|
+
if [[ "$pub_rc" -eq 0 && -z "$pub_out" ]]; then
|
|
203
|
+
echo "PASS: public turn exits 0 with no stdout"; PASS=$((PASS+1))
|
|
204
|
+
else
|
|
205
|
+
echo "FAIL: public turn injected (exit=$pub_rc stdout=$pub_out)" >&2; FAIL=$((FAIL+1))
|
|
206
|
+
fi
|
|
207
|
+
|
|
198
208
|
echo "----"
|
|
199
209
|
echo "PASS=$PASS FAIL=$FAIL"
|
|
200
210
|
[[ "$FAIL" -eq 0 ]]
|
|
@@ -34,6 +34,16 @@ set -uo pipefail
|
|
|
34
34
|
# session_id is used here (for the breadcrumb); the block is prompt-independent.
|
|
35
35
|
HOOK_INPUT=$(cat 2>/dev/null || true)
|
|
36
36
|
|
|
37
|
+
# Task 2424 — a public visitor's turn carries only its agent's authored
|
|
38
|
+
# IDENTITY/SOUL/KNOWLEDGE. MAXY_PUBLIC_ALLOWED_TOOLS is stamped for role=public
|
|
39
|
+
# spawns only, by both spawn paths, and is the signal public-tool-surface.sh
|
|
40
|
+
# already keys on. Placed after the stdin drain so the parent never sees EPIPE.
|
|
41
|
+
if [ -n "${MAXY_PUBLIC_ALLOWED_TOOLS:-}" ]; then
|
|
42
|
+
echo "[public-prompt-gate] op=gate hook=datetime-inject.sh public=1" >&2
|
|
43
|
+
exit 0
|
|
44
|
+
fi
|
|
45
|
+
echo "[public-prompt-gate] op=gate hook=datetime-inject.sh public=0" >&2
|
|
46
|
+
|
|
37
47
|
# Fail-open if either encoder is unavailable: python3 wraps the JSON envelope,
|
|
38
48
|
# node formats the instant in the target zone (Intl has the full IANA database).
|
|
39
49
|
command -v python3 >/dev/null 2>&1 || exit 0
|
|
@@ -32,6 +32,20 @@ set -uo pipefail
|
|
|
32
32
|
# hit with EPIPE. Same reason the two sibling hooks drain first.
|
|
33
33
|
HOOK_INPUT=$(cat 2>/dev/null || true)
|
|
34
34
|
|
|
35
|
+
# Task 2424 — a public visitor's turn carries only its agent's authored
|
|
36
|
+
# IDENTITY/SOUL/KNOWLEDGE. The ACCOUNT_DIR test below is NOT a role gate on the
|
|
37
|
+
# rc-spawn path: buildRcChildEnv stamps that variable unconditionally
|
|
38
|
+
# (rc-daemon.ts), so Task 2271's reasoning held only for pty-spawner, and after
|
|
39
|
+
# Task 2423 routed public Telegram through /rc-spawn the operator's mailbox list
|
|
40
|
+
# reached visitors. MAXY_PUBLIC_ALLOWED_TOOLS is stamped for role=public spawns
|
|
41
|
+
# only, by both paths. Placed after the stdin drain so the parent never sees
|
|
42
|
+
# EPIPE.
|
|
43
|
+
if [ -n "${MAXY_PUBLIC_ALLOWED_TOOLS:-}" ]; then
|
|
44
|
+
echo "[public-prompt-gate] op=gate hook=mailbox-inject.sh public=1" >&2
|
|
45
|
+
exit 0
|
|
46
|
+
fi
|
|
47
|
+
echo "[public-prompt-gate] op=gate hook=mailbox-inject.sh public=0" >&2
|
|
48
|
+
|
|
35
49
|
command -v python3 >/dev/null 2>&1 || exit 0
|
|
36
50
|
command -v node >/dev/null 2>&1 || exit 0
|
|
37
51
|
|
|
@@ -5,6 +5,15 @@
|
|
|
5
5
|
# promote finished deliverables out of output/. Prompt-independent; fail-open.
|
|
6
6
|
set -uo pipefail
|
|
7
7
|
HOOK_INPUT=$(cat 2>/dev/null || true)
|
|
8
|
+
# Task 2424 — a public visitor's turn carries only its agent's authored
|
|
9
|
+
# IDENTITY/SOUL/KNOWLEDGE. MAXY_PUBLIC_ALLOWED_TOOLS is stamped for role=public
|
|
10
|
+
# spawns only, by both spawn paths, and is the signal public-tool-surface.sh
|
|
11
|
+
# already keys on. Placed after the stdin drain so the parent never sees EPIPE.
|
|
12
|
+
if [ -n "${MAXY_PUBLIC_ALLOWED_TOOLS:-}" ]; then
|
|
13
|
+
echo "[public-prompt-gate] op=gate hook=preference-consult-directive.sh public=1" >&2
|
|
14
|
+
exit 0
|
|
15
|
+
fi
|
|
16
|
+
echo "[public-prompt-gate] op=gate hook=preference-consult-directive.sh public=0" >&2
|
|
8
17
|
command -v python3 >/dev/null 2>&1 || exit 0
|
|
9
18
|
python3 - <<'PY' 2>/dev/null || exit 0
|
|
10
19
|
import json
|
|
@@ -38,6 +38,16 @@ set -uo pipefail
|
|
|
38
38
|
# UserPromptSubmit payload carries session_id, used for the durable breadcrumb.
|
|
39
39
|
HOOK_INPUT=$(cat 2>/dev/null || true)
|
|
40
40
|
|
|
41
|
+
# Task 2424 — a public visitor's turn carries only its agent's authored
|
|
42
|
+
# IDENTITY/SOUL/KNOWLEDGE. MAXY_PUBLIC_ALLOWED_TOOLS is stamped for role=public
|
|
43
|
+
# spawns only, by both spawn paths, and is the signal public-tool-surface.sh
|
|
44
|
+
# already keys on. Placed after the stdin drain so the parent never sees EPIPE.
|
|
45
|
+
if [ -n "${MAXY_PUBLIC_ALLOWED_TOOLS:-}" ]; then
|
|
46
|
+
echo "[public-prompt-gate] op=gate hook=prompt-optimiser-directive.sh public=1" >&2
|
|
47
|
+
exit 0
|
|
48
|
+
fi
|
|
49
|
+
echo "[public-prompt-gate] op=gate hook=prompt-optimiser-directive.sh public=0" >&2
|
|
50
|
+
|
|
41
51
|
# Fail-open if the JSON encoder is unavailable.
|
|
42
52
|
command -v python3 >/dev/null 2>&1 || exit 0
|
|
43
53
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: platform-architecture
|
|
3
3
|
description: Use when grounding any documented-surface claim about what Maxy ships — plugins, skills, specialists, install/deploy flows, internals. This is the install catalogue, not evidence of what is enabled on the current account. For install state on this account, call `capabilities-here`; for documented surface, cite the `Source:` URL inline.
|
|
4
|
-
content-hash: sha256:
|
|
4
|
+
content-hash: sha256:5b93e9b58f687c920da46bec1137f326c5c00d5f2b1d4a460a5f5248eaee0c15
|
|
5
5
|
brand: maxy-code
|
|
6
6
|
product-name: Maxy
|
|
7
7
|
---
|
|
@@ -294,9 +294,11 @@ The Data search panel ranks results by combining vector similarity with keyword
|
|
|
294
294
|
|
|
295
295
|
## Software Update and Cloudflare Setup
|
|
296
296
|
|
|
297
|
-
|
|
297
|
+
Cloudflare setup runs on the native Claude Code PTY surface in admin chat: there is no Cloudflare setup form, the agent invokes the relevant Bash command directly and its stdout streams into chat verbatim.
|
|
298
298
|
|
|
299
|
-
|
|
299
|
+
Software update has two doors. The PTY one is the same shape: the agent runs the installer via Bash and its stdout streams into chat. The dashboard one is a modal over the edge-hosted `/api/admin/actions` routes, which run the installer as a `systemd-run` transient unit and stream its log back by byte offset. Both re-run the same installer; neither mutates state directly.
|
|
300
|
+
|
|
301
|
+
- **Software update.** Re-run the installer (`npx -y @rubytech/create-<brand>@latest`), from a shell, from admin chat via the `upgrade` skill, or from the dashboard control in the sidebar brand head. The brand head shows the installed version at all times and grows a download icon only while `updateAvailable` is true.
|
|
300
302
|
- **Cloudflare setup.** Operator asks in chat; the agent invokes `cloudflared` directly via the Bash tool, following the numbered steps in `plugins/cloudflare/references/manual-setup.md`. cloudflared's stdout and stderr stream into the PTY; the OAuth URL printed by `cloudflared tunnel login` is linkified by the terminal so the operator clicks it and authorises Cloudflare in their own browser.
|
|
301
303
|
|
|
302
304
|
**Mid-turn stream-drop banners.** If a chat turn ends abruptly the bubble shows one of two messages depending on what actually happened. You see "Server is restarting — reconnect will happen automatically." only when the app server itself emits the restart signal — typically during a Software Update or a Cloudflare setup that re-launches the brand service. You see "Lost connection — retrying." when your browser's connection to the Pi dropped mid-stream while the server was still up — typically a flaky Wi-Fi moment or the tunnel hiccupping. Either way the chat resumes once the connection is back; the previously-rendered messages stay on screen so you don't lose context.
|
|
@@ -5557,6 +5559,8 @@ There is exactly one place each datum is read from, and every subsystem reads it
|
|
|
5557
5559
|
- **Default agent + account id** come from the canonical account config `data/accounts/<id>/account.json`, resolved via `resolveAccount` / `resolveDefaultAgentSlug` / `getDefaultAccountId` (`app/lib/claude-agent/account.ts`). The UI server's bare-root handler and branding loader read it through these helpers — never from `~/.<brand>/account.json`, which nothing writes. A `null` default means a genuinely unset `defaultAgent`; the root logs `[public-root] op=serve-default slug=<s>` on success and `op=no-default` only when the canonical default is truly empty.
|
|
5558
5560
|
- **Identity files** (IDENTITY.md, SOUL.md, KNOWLEDGE.md) come from `agents/<slug>/` — keyed on the agent's slug, never the literal role. A public agent renamed to a slug `≠ "public"` (e.g. `maxy-info`) spawns from `agents/maxy-info/`. Admin reads `agents/admin/`. The spawn composer and the standing reachability audit resolve this directory through one shared function, `resolveAgentIdentityDir(accountDir, role, slug)` (`claude-session-manager/src/agent-identity-locator.ts`), so the path the audit vouches for is byte-identical to the path the spawn reads — the audit's `reachable=true` implies the spawn will find the files, and a slug whose files are misplaced is reported `reachable=false reason=files-missing file=<path>` without a visitor request.
|
|
5559
5561
|
|
|
5562
|
+
**What a public block contains.** A `role='public'` compose emits `<identity>`, `<soul>`, `<platform-boundary>`, `<data-stewardship>` and `<knowledge>`, and nothing else. The `<host>` section every other role gets is omitted: hostname, LAN IPv4, admin URL and tunnel URL are facts about the operator's machine, and a visitor's agent holds only what its author wrote. The two standing directives stay because they constrain behaviour rather than stating extractable facts. `op=public-persona-composed … hostBytes=0` is the per-spawn proof; non-zero on a public spawn is the regression signal, readable without reproducing a visitor turn.
|
|
5563
|
+
|
|
5560
5564
|
Which account is `<id>`? The slug is the address, and the address names the account. `resolvePublicAddressFromDisk` (`platform/ui/app/lib/claude-agent/public-address.ts`) walks every valid account for an active agent at the requested slug: exactly one claimant serves; the house serves when it is among several claimants and the others are refused by name; two or more non-house claimants serve nobody. The bare root `/` carries no slug and stays the house's door with the house's `defaultAgent`. The four public surfaces — the `/{slug}` page, `POST /api/chat`, `POST /api/session` and the visitor transcript reader — share that one resolver, and the resolved account rides `agentAccountId` to `/public-spawn`, which re-scopes the spawn onto it. A visitor's uploads follow the same account, and the reader locates them from the session's own sidecar `accountId` rather than re-deriving them from the slug, because a slug's owner can change after the session ran. Slugs are not unique across accounts, so a collision is reported on every refusal (`[webchat] op=address-collision`) and counted on every run of the `webchat-address-audit` loop.
|
|
5561
5565
|
|
|
5562
5566
|
### Observability
|
|
@@ -6108,11 +6112,11 @@ The logs will show which service failed to start and why. Common causes:
|
|
|
6108
6112
|
Each installed brand runs two per-brand `--user` systemd units (earlier platform fixes + — unit filenames are prefixed with the brand's `hostname` so two brands on the same device never share a unit file):
|
|
6109
6113
|
|
|
6110
6114
|
- `{hostname}.service` — the admin + public HTTP server on `127.0.0.1:19201` (public port + 1). Restarted by the upgrade flow; short downtime is expected during steps 8→11 of an upgrade. An earlier fix: the unit carries two port env vars — `PORT=<public>` (canonical public port, read by the upgrade detector) and `MAXY_UI_INTERNAL_PORT=<public+1>` (the port maxy-ui actually binds).
|
|
6111
|
-
- `{hostname}-edge.service` — the always-on public listener on the configured port (default 19200). Reverse-proxies HTTP to the main brand service and handles `/websockify` (VNC) WebSocket upgrades locally. An earlier fix: also hosts `/api/admin/actions/*` and `/api/admin/version*` — the Software Update modal's own routes — so
|
|
6115
|
+
- `{hostname}-edge.service` — the always-on public listener on the configured port (default 19200). Reverse-proxies HTTP to the main brand service and handles `/websockify` (VNC) WebSocket upgrades locally. An earlier fix: also hosts `/api/admin/actions/*` and `/api/admin/version*` — the Software Update modal's own routes — so they survive the brand service's restart window. It is restarted by the upgrade flow too (`packages/create-maxy-code/src/index.ts:5143`, after the brand service is stopped and before it is restarted), so the update stream drops near the end of every run and resumes by byte offset. It is not a connection that survives; the log file is what survives, and the browser's `EventSource` replays `Last-Event-ID` to pick the tail back up. The operator views the device's Chromium in the admin UI via the standalone `/browser` page (peer to `/graph` and `/data`) and on the Claude sign-in screen; both render CDP screencast frames over `/cdp-screencast`, not the framebuffer. Virtual-mode Chromium is headless, so nothing renders onto the X display and the `/websockify` upgrade has no remaining consumer; deleting that stack is separate work.
|
|
6112
6116
|
|
|
6113
|
-
|
|
6117
|
+
The upgrade runs as a detached action: a `systemd-run --user --unit=maxy-action-<actionId> --collect` transient unit per invocation, with stdout+stderr persisted to `~/.<brand>/logs/actions/<actionId>.log` and streamed to the UI via SSE. The unit has its own cgroup, so it outlives both service restarts the installer performs. Its final log line is `[action-exit] code=<n>`, written by the wrapper, which is how the exit code survives a unit that `--collect` has already reaped. A sidecar `~/.<brand>/logs/actions/<actionId>.json` holds the package, start time and pre-upgrade version, so a modal reopened after a reload can still report what changed. No boot-time service file exists for these. Cloudflare setup is not an action; it runs PTY-native from admin chat.
|
|
6114
6118
|
|
|
6115
|
-
If an action looks stuck, read
|
|
6119
|
+
If an action looks stuck, read `~/.<brand>/logs/actions/<actionId>.log` directly for the full output, or `journalctl --user --identifier=maxy-action-<actionId>` for systemd's record. The lifecycle lines are tagged `[admin-action]` and keyed by `actionId`; they land in `~/.<brand>/logs/edge.log`, not `server.log`, because these routes run on the edge process.
|
|
6116
6120
|
|
|
6117
6121
|
## Linux laptops: snap-confined Chromium replacement
|
|
6118
6122
|
|
|
@@ -6221,17 +6225,23 @@ Empty output from step 3 = brand.json resolved cleanly and the badge reflects th
|
|
|
6221
6225
|
|
|
6222
6226
|
## Upgrading
|
|
6223
6227
|
|
|
6224
|
-
|
|
6228
|
+
There are two ways to upgrade, and both run the same installer.
|
|
6229
|
+
|
|
6230
|
+
From the dashboard, press the download icon in the sidebar header (see below). From chat, ask Maxy: "Upgrade Maxy." The platform checks the current device identity (hostname and port via `system-status`), then re-runs the installer with explicit `--hostname` and `--port` flags to preserve them across the upgrade.
|
|
6225
6231
|
|
|
6226
6232
|
The docs plugin (this plugin) is upgraded in the same step — you always have the documentation that matches your installed version.
|
|
6227
6233
|
|
|
6228
|
-
###
|
|
6234
|
+
### Upgrading from the dashboard
|
|
6235
|
+
|
|
6236
|
+
The sidebar header shows the version you are running, under the product name, at all times. When a newer one is published a download icon appears beside it. There is no icon when you are already up to date, so the icon appearing is itself the news. If you work with the sidebar collapsed to the narrow rail, the version and the icon are hidden with the rest of the header text and a small coloured dot on the brand icon carries the same signal.
|
|
6237
|
+
|
|
6238
|
+
Pressing the icon opens the Software Update window, which shows the version you have against the version available and an Update button. Pressing Update runs the installer and streams its output into the window as it happens.
|
|
6229
6239
|
|
|
6230
|
-
|
|
6240
|
+
Closing the window does not cancel the upgrade. The installer runs in its own background unit, so it keeps going whether or not anything is watching, and reopening the window reattaches to the run in progress.
|
|
6231
6241
|
|
|
6232
|
-
The upgrade
|
|
6242
|
+
**The connection drops near the end of every upgrade, and that is expected.** The installer restarts the server as its last step, which cuts the browser's connection to it. The window says "Reconnecting" while that happens, then picks the output back up from exactly where it left off. Nothing is lost and nothing needs restarting. When it finishes, the window reports the exit code and whether the version actually moved.
|
|
6233
6243
|
|
|
6234
|
-
The
|
|
6244
|
+
The window reports a genuine failure in two shapes worth knowing. If the installer exits non-zero it says so with the code. If it exits cleanly but the version did not change, it says that too, rather than claiming success.
|
|
6235
6245
|
|
|
6236
6246
|
---
|
|
6237
6247
|
# Samba Share
|
|
@@ -9,6 +9,17 @@ Invoked by the admin agent directly.
|
|
|
9
9
|
|
|
10
10
|
This is the platform's release timeline, newest first. Each entry shows the date it shipped and the version it shipped in, so you can tell the operator how current their install is. To compare, read the installed version from `capabilities-here` and match it against the versions below. Keep answers high level and in plain English; this is a summary, not a full commit log.
|
|
11
11
|
|
|
12
|
+
## 2026-08-05 (0.1.563)
|
|
13
|
+
|
|
14
|
+
- You can now update from the dashboard. The installed version sits in the sidebar header rather than buried in the footer menu, and when a newer one is available the control there starts the update and shows its progress live. Previously the only route was asking the assistant in chat, and an available update was invisible until you opened the menu.
|
|
15
|
+
- A public bot no longer loses the first message it is sent. A message that arrived while the conversation was still starting up was handed over a moment too early and discarded silently; delivery now waits until the conversation is ready to receive it.
|
|
16
|
+
- The data portal can now hold records, not just files. A portal login can be scoped to see only the records assigned to that person rather than everything for the account, work assigned in the office appears on the portal, and what is filled in on site comes back. A standing check reports any exchange that stalls.
|
|
17
|
+
|
|
18
|
+
## 2026-08-05 (0.1.562)
|
|
19
|
+
|
|
20
|
+
- A public agent no longer sees anything about you or your machine. A stranger messaging a public bot was having your connected mailbox list, this machine's name and network addresses, its admin and tunnel web addresses, today's date and your working preferences added to every turn, alongside the persona you wrote. A public turn now carries only what the agent's author wrote plus its single reply tool.
|
|
21
|
+
- Two standing checks back that up: one records, for every account, exactly what gets added to a turn, and the other refuses to start if that picture drifts from what is expected. Each spawn also reports how much was added, so a public one showing anything other than nothing is visible immediately.
|
|
22
|
+
|
|
12
23
|
## 2026-08-04 (0.1.561)
|
|
13
24
|
|
|
14
25
|
- A public Telegram bot now answers. Messages were arriving and the conversation staying alive, but nothing the visitor sent ever became a turn, so a public bot received everything and replied to nothing. Public bots now open through the same locked-down path the platform already uses elsewhere: the ordinary setup with only the reply tool available.
|
|
@@ -108,6 +108,17 @@ if (foldersArg !== undefined) {
|
|
|
108
108
|
folders = [...new Set(names)].join(',')
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
// Which RECORDS this person reads, as distinct from --folders, which is which
|
|
112
|
+
// files they browse. Two values and no third.
|
|
113
|
+
//
|
|
114
|
+
// The absent flag is the NARROW one. Nobody is granted account-wide visibility
|
|
115
|
+
// by leaving a flag off, and a typo is refused rather than resolved to
|
|
116
|
+
// whichever value happens to be nearer — there is no third scope for it to be.
|
|
117
|
+
const recordScope = arg('--record-scope') ?? 'own'
|
|
118
|
+
if (recordScope !== 'own' && recordScope !== 'account') {
|
|
119
|
+
die('--record-scope must be "own" or "account" — there is no third scope')
|
|
120
|
+
}
|
|
121
|
+
|
|
111
122
|
const saltBytes = new Uint8Array(16)
|
|
112
123
|
crypto.getRandomValues(saltBytes)
|
|
113
124
|
const saltHex = randomSaltHex(saltBytes)
|
|
@@ -155,14 +166,17 @@ const createdAt = new Date().toISOString()
|
|
|
155
166
|
// The dedicated path keeps `accountId` in the DO UPDATE exactly as before, so a
|
|
156
167
|
// grandfathered portal's behaviour is unchanged in every respect.
|
|
157
168
|
//
|
|
158
|
-
// `folders`
|
|
159
|
-
// narrower or wider
|
|
169
|
+
// `folders` and `recordScope` are BOTH in the DO UPDATE on both models:
|
|
170
|
+
// re-enrolment replaces each, so a narrower or wider setting on a re-run takes
|
|
171
|
+
// effect rather than sticking. Narrowing matters most — a person moved off the
|
|
172
|
+
// account-wide scope must actually lose it, and the re-run is the only path
|
|
173
|
+
// there is.
|
|
160
174
|
const conflict =
|
|
161
175
|
storageModel === 'shared'
|
|
162
|
-
? 'ON CONFLICT(accountId, ownerId) DO UPDATE SET name=excluded.name, salt=excluded.salt, hash=excluded.hash, folders=excluded.folders'
|
|
163
|
-
: 'ON CONFLICT(ownerId) DO UPDATE SET name=excluded.name, salt=excluded.salt, hash=excluded.hash, accountId=excluded.accountId, folders=excluded.folders'
|
|
176
|
+
? 'ON CONFLICT(accountId, ownerId) DO UPDATE SET name=excluded.name, salt=excluded.salt, hash=excluded.hash, folders=excluded.folders, recordScope=excluded.recordScope'
|
|
177
|
+
: 'ON CONFLICT(ownerId) DO UPDATE SET name=excluded.name, salt=excluded.salt, hash=excluded.hash, accountId=excluded.accountId, folders=excluded.folders, recordScope=excluded.recordScope'
|
|
164
178
|
console.log(
|
|
165
|
-
`INSERT INTO people (ownerId, name, salt, hash, createdAt, accountId, folders) VALUES ('${ownerId}', '${sqlName}', '${saltHex}', '${hash}', '${createdAt}', '${accountId}', '${folders}') ${conflict};`,
|
|
179
|
+
`INSERT INTO people (ownerId, name, salt, hash, createdAt, accountId, folders, recordScope) VALUES ('${ownerId}', '${sqlName}', '${saltHex}', '${hash}', '${createdAt}', '${accountId}', '${folders}', '${recordScope}') ${conflict};`,
|
|
166
180
|
)
|
|
167
181
|
|
|
168
182
|
// stderr: everything for the human. Never stdout — see the note at the top.
|
|
@@ -102,6 +102,9 @@ describe('sessions', () => {
|
|
|
102
102
|
ownerId: 'alice',
|
|
103
103
|
accountId: ACC,
|
|
104
104
|
folders: [],
|
|
105
|
+
// The fake's people row carries no recordScope, which is a legacy row.
|
|
106
|
+
// It resolves narrow, not wide (Task 2425).
|
|
107
|
+
recordScope: 'own',
|
|
105
108
|
})
|
|
106
109
|
})
|
|
107
110
|
|
|
@@ -186,6 +189,9 @@ describe('session revocation — the property the design claims', () => {
|
|
|
186
189
|
ownerId: 'alice',
|
|
187
190
|
accountId: ACC,
|
|
188
191
|
folders: [],
|
|
192
|
+
// The fake's people row carries no recordScope, which is a legacy row.
|
|
193
|
+
// It resolves narrow, not wide (Task 2425).
|
|
194
|
+
recordScope: 'own',
|
|
189
195
|
})
|
|
190
196
|
|
|
191
197
|
peopleRows[0].hash = 'HASH-v2-rotated0' // operator re-runs enrolment
|
|
@@ -32,7 +32,7 @@ function parse(r: { stdout: string; stderr: string }) {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
const valuesOf = (sql: string) =>
|
|
35
|
-
/VALUES \('([^']*)', '((?:[^']|'')*)', '([0-9a-f]+)', '([0-9a-f]+)', '([^']*)', '([^']*)', '([^']*)'\)/.exec(
|
|
35
|
+
/VALUES \('([^']*)', '((?:[^']|'')*)', '([0-9a-f]+)', '([0-9a-f]+)', '([^']*)', '([^']*)', '([^']*)', '([^']*)'\)/.exec(
|
|
36
36
|
sql,
|
|
37
37
|
)!
|
|
38
38
|
|
|
@@ -129,9 +129,9 @@ describe('portal-enrol', () => {
|
|
|
129
129
|
const r = await enrol(['--owner', 'alice', '--name', 'Alice'])
|
|
130
130
|
const { sql } = parse(r)
|
|
131
131
|
expect(sql).toContain(
|
|
132
|
-
'INSERT INTO people (ownerId, name, salt, hash, createdAt, accountId, folders)',
|
|
132
|
+
'INSERT INTO people (ownerId, name, salt, hash, createdAt, accountId, folders, recordScope)',
|
|
133
133
|
)
|
|
134
|
-
const [, owner, name, salt, hash, createdAt, accountId, folders] = valuesOf(sql!)
|
|
134
|
+
const [, owner, name, salt, hash, createdAt, accountId, folders, recordScope] = valuesOf(sql!)
|
|
135
135
|
expect(owner).toBe('alice')
|
|
136
136
|
expect(name).toBe('Alice')
|
|
137
137
|
expect(salt).toMatch(/^[0-9a-f]{32}$/)
|
|
@@ -139,6 +139,57 @@ describe('portal-enrol', () => {
|
|
|
139
139
|
expect(Number.isNaN(Date.parse(createdAt))).toBe(false)
|
|
140
140
|
expect(accountId).toBe('acc-1')
|
|
141
141
|
expect(folders).toBe('')
|
|
142
|
+
expect(recordScope).toBe('own')
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
describe('record scope (Task 2425)', () => {
|
|
146
|
+
it('defaults to own when the flag is absent', async () => {
|
|
147
|
+
const { sql } = parse(await enrol(['--owner', 'alice', '--name', 'Alice']))
|
|
148
|
+
expect(valuesOf(sql!)[8]).toBe('own')
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('writes account when asked for it', async () => {
|
|
152
|
+
const { sql } = parse(
|
|
153
|
+
await enrol(['--owner', 'marie', '--name', 'Marie', '--record-scope', 'account']),
|
|
154
|
+
)
|
|
155
|
+
expect(valuesOf(sql!)[8]).toBe('account')
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
it('dies on a third value, before any SQL reaches stdout', async () => {
|
|
159
|
+
// There is no third scope. A typo must not silently land as one of the
|
|
160
|
+
// two, and it must not land at all: stdout is empty, so the documented
|
|
161
|
+
// pipe carries nothing into wrangler.
|
|
162
|
+
await expect(
|
|
163
|
+
enrol(['--owner', 'alice', '--name', 'Alice', '--record-scope', 'admin']),
|
|
164
|
+
).rejects.toMatchObject({ stdout: '' })
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it('replaces the scope on re-enrolment, on both storage models', async () => {
|
|
168
|
+
const shared = parse(
|
|
169
|
+
await enrol(['--owner', 'a', '--name', 'A', '--record-scope', 'account']),
|
|
170
|
+
)
|
|
171
|
+
expect(shared.sql).toContain('recordScope=excluded.recordScope')
|
|
172
|
+
const dedicated = parse(
|
|
173
|
+
await enrol(['--owner', 'a', '--name', 'A', '--storage-model', 'dedicated']),
|
|
174
|
+
)
|
|
175
|
+
expect(dedicated.sql).toContain('recordScope=excluded.recordScope')
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it('lands in a real database and the re-run narrows it back', async () => {
|
|
179
|
+
// Against the template's own schema.sql, not a fake: a fake would model
|
|
180
|
+
// the very constraint we are proving we satisfy, which is how 1689
|
|
181
|
+
// shipped a UNIQUE-violating upload.
|
|
182
|
+
const db = await d1('portal-record-scope-')
|
|
183
|
+
const wide = parse(
|
|
184
|
+
await enrol(['--owner', 'alice', '--name', 'Alice', '--record-scope', 'account']),
|
|
185
|
+
)
|
|
186
|
+
await db.exec(wide.sql!)
|
|
187
|
+
expect(await db.query("SELECT recordScope FROM people WHERE ownerId='alice'")).toBe('account')
|
|
188
|
+
|
|
189
|
+
const narrow = parse(await enrol(['--owner', 'alice', '--name', 'Alice']))
|
|
190
|
+
await db.exec(narrow.sql!)
|
|
191
|
+
expect(await db.query("SELECT recordScope FROM people WHERE ownerId='alice'")).toBe('own')
|
|
192
|
+
})
|
|
142
193
|
})
|
|
143
194
|
|
|
144
195
|
it('refuses enrolment with no --account', async () => {
|
|
@@ -175,8 +226,14 @@ describe('portal-enrol', () => {
|
|
|
175
226
|
})
|
|
176
227
|
|
|
177
228
|
describe('the conflict target follows the storage model (Task 2099)', () => {
|
|
178
|
-
/** A pre-2099 portal's own database: `people` keyed on ownerId alone.
|
|
179
|
-
|
|
229
|
+
/** A pre-2099 portal's own database: `people` keyed on ownerId alone.
|
|
230
|
+
*
|
|
231
|
+
* `withRecordScope` is the hand `ALTER` schema.sql tells an operator to run
|
|
232
|
+
* on a table that predates the column (Task 2425). Both shapes are real and
|
|
233
|
+
* the tests below need each: without it, enrolment must fail loudly, which
|
|
234
|
+
* is what the schema comment claims; with it, rotation must keep working,
|
|
235
|
+
* which is what the composite-target regression is about. */
|
|
236
|
+
async function legacyDb(dir: string, withRecordScope = true) {
|
|
180
237
|
const { mkdtemp } = await import('node:fs/promises')
|
|
181
238
|
const { tmpdir } = await import('node:os')
|
|
182
239
|
const root = await mkdtemp(resolve(tmpdir(), dir))
|
|
@@ -185,7 +242,10 @@ describe('portal-enrol', () => {
|
|
|
185
242
|
db,
|
|
186
243
|
'CREATE TABLE people (ownerId TEXT PRIMARY KEY, name TEXT NOT NULL, salt TEXT NOT NULL, ' +
|
|
187
244
|
"hash TEXT NOT NULL, createdAt TEXT NOT NULL, accountId TEXT NOT NULL DEFAULT '', " +
|
|
188
|
-
"folders TEXT NOT NULL DEFAULT '');"
|
|
245
|
+
"folders TEXT NOT NULL DEFAULT '');" +
|
|
246
|
+
(withRecordScope
|
|
247
|
+
? " ALTER TABLE people ADD COLUMN recordScope TEXT NOT NULL DEFAULT 'own';"
|
|
248
|
+
: ''),
|
|
189
249
|
])
|
|
190
250
|
return {
|
|
191
251
|
exec: (sql: string) => run('sqlite3', [db, sql]),
|
|
@@ -193,6 +253,17 @@ describe('portal-enrol', () => {
|
|
|
193
253
|
}
|
|
194
254
|
}
|
|
195
255
|
|
|
256
|
+
it('fails loudly on a table that never got the recordScope column', async () => {
|
|
257
|
+
// schema.sql claims enrolment's INSERT names the column, so a table
|
|
258
|
+
// predating it fails loudly rather than writing a row with no scope. A
|
|
259
|
+
// property a comment asserts has to be a property the code has.
|
|
260
|
+
const db = await legacyDb('enrol-legacy-noscope-', false)
|
|
261
|
+
const r = parse(
|
|
262
|
+
await enrol(['--owner', 'dale', '--name', 'Dale', '--account', 'acct-a', '--storage-model', 'dedicated']),
|
|
263
|
+
)
|
|
264
|
+
await expect(db.exec(r.sql!)).rejects.toThrow(/no column named recordScope/)
|
|
265
|
+
})
|
|
266
|
+
|
|
196
267
|
it('lets two tenants each enrol the same person on the shared store', async () => {
|
|
197
268
|
const db = await d1('enrol-two-tenants-')
|
|
198
269
|
await db.exec(parse(await enrol(['--owner', 'rob', '--name', 'Rob', '--account', 'acct-a'])).sql!)
|
|
@@ -72,6 +72,45 @@ describe('resolveSession folders', () => {
|
|
|
72
72
|
})
|
|
73
73
|
})
|
|
74
74
|
|
|
75
|
+
describe('resolveSession recordScope', () => {
|
|
76
|
+
const row = (recordScope?: unknown) => ({
|
|
77
|
+
ownerId: 'alice',
|
|
78
|
+
accountId: 'acc-a',
|
|
79
|
+
folders: '',
|
|
80
|
+
...(recordScope === undefined ? {} : { recordScope }),
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('returns account for a person who holds it', async () => {
|
|
84
|
+
const s = await resolveSession(db(row('account')), 'sess', 'acc-a', 1)
|
|
85
|
+
expect(s?.recordScope).toBe('account')
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('returns own for a person who holds it', async () => {
|
|
89
|
+
const s = await resolveSession(db(row('own')), 'sess', 'acc-a', 1)
|
|
90
|
+
expect(s?.recordScope).toBe('own')
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
// Every unreadable reading is the NARROW one. Only the exact token widens.
|
|
94
|
+
// Reverting the fallback to 'account' must turn all four of these red — a
|
|
95
|
+
// read failure that grants account-wide visibility is the one outcome this
|
|
96
|
+
// column exists to prevent.
|
|
97
|
+
it('falls back to own for an empty string', async () => {
|
|
98
|
+
expect((await resolveSession(db(row('')), 'sess', 'acc-a', 1))?.recordScope).toBe('own')
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('falls back to own for an unknown token', async () => {
|
|
102
|
+
expect((await resolveSession(db(row('admin')), 'sess', 'acc-a', 1))?.recordScope).toBe('own')
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('falls back to own for a null value', async () => {
|
|
106
|
+
expect((await resolveSession(db(row(null)), 'sess', 'acc-a', 1))?.recordScope).toBe('own')
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('falls back to own for a legacy row with no column at all', async () => {
|
|
110
|
+
expect((await resolveSession(db(row()), 'sess', 'acc-a', 1))?.recordScope).toBe('own')
|
|
111
|
+
})
|
|
112
|
+
})
|
|
113
|
+
|
|
75
114
|
describe('grantAllows', () => {
|
|
76
115
|
it('admits everything when the grant is empty', () => {
|
|
77
116
|
expect(grantAllows([], 'documents/secret.pdf')).toBe(true)
|