@rubytech/create-maxy-code 0.1.477 → 0.1.479
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/docs/superpowers/plans/2026-07-20-task-1704-data-portal-standing-audit.md +673 -0
- package/payload/platform/docs/superpowers/plans/2026-07-20-task-1789-reseat-channel-row-fork.md +1244 -0
- package/payload/platform/docs/superpowers/plans/2026-07-20-task-1818-loop-gate-app-routes.md +462 -0
- package/payload/platform/docs/superpowers/plans/2026-07-20-task-1819-top-level-label-allowlist.md +321 -0
- package/payload/platform/docs/superpowers/plans/2026-07-20-task-1831-data-portal-class-derived-allowlist.md +2177 -0
- package/payload/platform/docs/superpowers/specs/2026-07-20-task-1704-data-portal-standing-audit-design.md +177 -0
- package/payload/platform/docs/superpowers/specs/2026-07-20-task-1789-reseat-channel-row-fork-design.md +201 -0
- package/payload/platform/docs/superpowers/specs/2026-07-20-task-1818-loop-gate-app-routes-design.md +217 -0
- package/payload/platform/docs/superpowers/specs/2026-07-20-task-1819-top-level-label-allowlist-design.md +80 -0
- package/payload/platform/docs/superpowers/specs/2026-07-20-task-1820-cpu-triage-admin-tools-design.md +97 -0
- package/payload/platform/docs/superpowers/specs/2026-07-20-task-1831-data-portal-class-derived-allowlist-design.md +163 -0
- package/payload/platform/lib/graph-style/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/graph-style/dist/index.js +22 -0
- package/payload/platform/lib/graph-style/dist/index.js.map +1 -1
- package/payload/platform/lib/graph-style/src/__tests__/parity.test.ts +6 -0
- package/payload/platform/lib/graph-style/src/index.ts +20 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/audit.test.js +44 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/audit.test.js.map +1 -1
- package/payload/platform/lib/storage-broker/dist/audit.d.ts +56 -0
- package/payload/platform/lib/storage-broker/dist/audit.d.ts.map +1 -1
- package/payload/platform/lib/storage-broker/dist/audit.js +34 -0
- package/payload/platform/lib/storage-broker/dist/audit.js.map +1 -1
- package/payload/platform/lib/storage-broker/src/__tests__/audit.test.ts +70 -1
- package/payload/platform/lib/storage-broker/src/audit.ts +111 -0
- package/payload/platform/plugins/admin/.claude-plugin/plugin.json +1 -1
- package/payload/platform/plugins/admin/PLUGIN.md +7 -1
- package/payload/platform/plugins/admin/mcp/dist/index.js +72 -0
- package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/admin/mcp/dist/tools/cpu-triage.d.ts +94 -0
- package/payload/platform/plugins/admin/mcp/dist/tools/cpu-triage.d.ts.map +1 -0
- package/payload/platform/plugins/admin/mcp/dist/tools/cpu-triage.js +229 -0
- package/payload/platform/plugins/admin/mcp/dist/tools/cpu-triage.js.map +1 -0
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +14 -2
- package/payload/platform/plugins/cloudflare/PLUGIN.md +26 -1
- package/payload/platform/plugins/cloudflare/bin/d1-http.mjs +80 -0
- package/payload/platform/plugins/cloudflare/bin/portal-enrol.mjs +17 -2
- package/payload/platform/plugins/cloudflare/bin/portal-index-push.mjs +357 -0
- package/payload/platform/plugins/cloudflare/bin/schema-exposed-dirs.mjs +103 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/authorize.test.ts +2 -2
- package/payload/platform/plugins/cloudflare/mcp/__tests__/d1-http.test.ts +96 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-directory-listing.test.ts +120 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-enrol.test.ts +59 -4
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-index-push.test.ts +176 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-index-state-contract.test.ts +75 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-indexed-download.test.ts +131 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-sign.test.ts +68 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/schema-exposed-dirs.test.ts +88 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/template-config.test.ts +39 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/SKILL.md +68 -1
- package/payload/platform/plugins/cloudflare/skills/data-portal/schema.sql +30 -1
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/portal-sign.mjs +87 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/session.ts +8 -4
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/types.ts +10 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/download.ts +107 -1
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/files.ts +82 -2
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/index.html +9 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/portal.css +56 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/portal.js +98 -2
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/wrangler.toml +12 -0
- package/payload/platform/plugins/docs/references/admin-ui.md +1 -1
- package/payload/platform/plugins/docs/references/cloudflare.md +12 -0
- package/payload/platform/plugins/email/PLUGIN.md +2 -1
- package/payload/platform/plugins/email/mcp/dist/__tests__/confirm-sent-copy.test.d.ts +2 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/confirm-sent-copy.test.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/confirm-sent-copy.test.js +92 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/confirm-sent-copy.test.js.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/email-draft-send.test.js +112 -12
- package/payload/platform/plugins/email/mcp/dist/__tests__/email-draft-send.test.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/__tests__/email-send-sent-copy.test.d.ts +2 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/email-send-sent-copy.test.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/email-send-sent-copy.test.js +54 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/email-send-sent-copy.test.js.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/imap-drafts.test.js +170 -1
- package/payload/platform/plugins/email/mcp/dist/__tests__/imap-drafts.test.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/__tests__/sent-copy-note.test.d.ts +2 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/sent-copy-note.test.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/sent-copy-note.test.js +86 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/sent-copy-note.test.js.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/sent-copy-sweep.test.d.ts +2 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/sent-copy-sweep.test.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/sent-copy-sweep.test.js +164 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/sent-copy-sweep.test.js.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/sent-ledger.test.d.ts +2 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/sent-ledger.test.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/sent-ledger.test.js +82 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/sent-ledger.test.js.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/lib/imap.d.ts +95 -0
- package/payload/platform/plugins/email/mcp/dist/lib/imap.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/imap.js +223 -0
- package/payload/platform/plugins/email/mcp/dist/lib/imap.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/sent-copy-note.d.ts +18 -0
- package/payload/platform/plugins/email/mcp/dist/lib/sent-copy-note.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/lib/sent-copy-note.js +59 -0
- package/payload/platform/plugins/email/mcp/dist/lib/sent-copy-note.js.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/lib/sent-copy-sweep.d.ts +48 -0
- package/payload/platform/plugins/email/mcp/dist/lib/sent-copy-sweep.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/lib/sent-copy-sweep.js +79 -0
- package/payload/platform/plugins/email/mcp/dist/lib/sent-copy-sweep.js.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/lib/sent-ledger.d.ts +44 -0
- package/payload/platform/plugins/email/mcp/dist/lib/sent-ledger.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/lib/sent-ledger.js +63 -0
- package/payload/platform/plugins/email/mcp/dist/lib/sent-ledger.js.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/scripts/sent-copy-sweep.d.ts +2 -0
- package/payload/platform/plugins/email/mcp/dist/scripts/sent-copy-sweep.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/scripts/sent-copy-sweep.js +55 -0
- package/payload/platform/plugins/email/mcp/dist/scripts/sent-copy-sweep.js.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/tools/email-draft-send.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-draft-send.js +64 -13
- package/payload/platform/plugins/email/mcp/dist/tools/email-draft-send.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-reply.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-reply.js +11 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-reply.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-send.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-send.js +16 -3
- package/payload/platform/plugins/email/mcp/dist/tools/email-send.js.map +1 -1
- package/payload/platform/plugins/email/references/email-reference.md +36 -0
- package/payload/platform/plugins/scheduling/mcp/dist/__tests__/portal-index-audit.test.d.ts +2 -0
- package/payload/platform/plugins/scheduling/mcp/dist/__tests__/portal-index-audit.test.d.ts.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/__tests__/portal-index-audit.test.js +66 -0
- package/payload/platform/plugins/scheduling/mcp/dist/__tests__/portal-index-audit.test.js.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/__tests__/sent-sweep-throttle.test.d.ts +2 -0
- package/payload/platform/plugins/scheduling/mcp/dist/__tests__/sent-sweep-throttle.test.d.ts.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/__tests__/sent-sweep-throttle.test.js +18 -0
- package/payload/platform/plugins/scheduling/mcp/dist/__tests__/sent-sweep-throttle.test.js.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/portal-index-audit.d.ts +41 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/portal-index-audit.d.ts.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/portal-index-audit.js +75 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/portal-index-audit.js.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/sent-sweep-throttle.d.ts +13 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/sent-sweep-throttle.d.ts.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/sent-sweep-throttle.js +10 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/sent-sweep-throttle.js.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js +175 -0
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js.map +1 -1
- package/payload/platform/scripts/cpu-triage-run.sh +117 -0
- package/payload/platform/scripts/cpu-triage.sh +23 -4
- package/payload/platform/services/claude-session-manager/dist/activity-range.d.ts +40 -0
- package/payload/platform/services/claude-session-manager/dist/activity-range.d.ts.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/activity-range.js +68 -0
- package/payload/platform/services/claude-session-manager/dist/activity-range.js.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/activity-rows.d.ts +40 -8
- package/payload/platform/services/claude-session-manager/dist/activity-rows.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/activity-rows.js +74 -7
- package/payload/platform/services/claude-session-manager/dist/activity-rows.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js +2 -0
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js.map +1 -1
- 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 +9 -0
- package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
- package/payload/server/{chunk-56WJMBQQ.js → chunk-3XLLTG6R.js} +14 -0
- package/payload/server/{chunk-JECAP3Z2.js → chunk-XGNRSM57.js} +3 -0
- package/payload/server/maxy-edge.js +1 -1
- package/payload/server/public/activity.html +8 -6
- package/payload/server/public/assets/{AdminLoginScreens-mEKP4pDi.js → AdminLoginScreens-DkxQZnvB.js} +1 -1
- package/payload/server/public/assets/AdminShell-Cv_nIFGe.js +2 -0
- package/payload/server/public/assets/Checkbox-CsTWK8mm.js +1 -0
- package/payload/server/public/assets/activity-BbnTiVBT.js +1 -0
- package/payload/server/public/assets/{admin-BVhgfgs1.js → admin-BZY3i_Ff.js} +1 -1
- package/payload/server/public/assets/{browser-D-HMmHcM.js → browser-BbIiSFKr.js} +1 -1
- package/payload/server/public/assets/{calendar-tGWvFyFn.js → calendar-BvxeTHvQ.js} +1 -1
- package/payload/server/public/assets/chat-DUKxufRk.js +1 -0
- package/payload/server/public/assets/chevron-left-BtfJD6LL.js +1 -0
- package/payload/server/public/assets/data-hl1wa-Ax.js +1 -0
- package/payload/server/public/assets/{graph-zEw610xK.js → graph-CDSGRoeb.js} +1 -1
- package/payload/server/public/assets/graph-labels-C5qLwnWA.js +1 -0
- package/payload/server/public/assets/{maximize-2-BlTjXT_Y.js → maximize-2-CAzH8dED.js} +1 -1
- package/payload/server/public/assets/{operator-C7oIw2PG.js → operator-BK7r8DAB.js} +1 -1
- package/payload/server/public/assets/{page-CTP7OFZa.js → page-B5YngOrK.js} +1 -1
- package/payload/server/public/assets/{page-DL6Zsdvk.js → page-BsCS0Dmc.js} +1 -1
- package/payload/server/public/assets/{public-r1A9dqR_.js → public-C1Pz-IBs.js} +1 -1
- package/payload/server/public/assets/{rotate-ccw-CzkkKx4-.js → rotate-ccw-UDln4Tou.js} +1 -1
- package/payload/server/public/assets/tasks-B-cLsn3p.js +1 -0
- package/payload/server/public/assets/{time-entry-format-j669DgXj.js → time-entry-format-DPOBIE18.js} +1 -1
- package/payload/server/public/assets/{triangle-alert-BPgCgIef.js → triangle-alert-Dz-CZKo5.js} +1 -1
- package/payload/server/public/assets/{useCopyFeedback-BmLa2aes.js → useCopyFeedback-wtUHGohk.js} +1 -1
- package/payload/server/public/assets/{useSelectionMode-DnnM7A8-.js → useSelectionMode-sph7AQT5.js} +1 -1
- package/payload/server/public/assets/useSubAccountSwitcher-BRIluIKf.css +1 -0
- package/payload/server/public/assets/{useVoiceRecorder-u2dwZfen.js → useVoiceRecorder-DN4jYQ0A.js} +1 -1
- package/payload/server/public/brand-defaults.css +1 -0
- package/payload/server/public/browser.html +5 -5
- package/payload/server/public/calendar.html +6 -6
- package/payload/server/public/chat.html +12 -12
- package/payload/server/public/data.html +11 -11
- package/payload/server/public/graph.html +11 -11
- package/payload/server/public/index.html +14 -14
- package/payload/server/public/operator.html +14 -14
- package/payload/server/public/privacy.html +1 -0
- package/payload/server/public/public.html +12 -12
- package/payload/server/public/tasks.html +6 -5
- package/payload/server/public/vnc-popout.html +1 -0
- package/payload/server/server.js +894 -384
- package/payload/server/{src-JYPKMWJR.js → src-XQ63FPRE.js} +3 -1
- package/payload/server/public/assets/AdminShell-BwSaZ88d.js +0 -2
- package/payload/server/public/assets/Checkbox-CN4uQ80w.js +0 -1
- package/payload/server/public/assets/activity-CIQ8ozNZ.js +0 -1
- package/payload/server/public/assets/chat-DkttVxAz.js +0 -1
- package/payload/server/public/assets/chevron-left-CZ4ez9G5.js +0 -1
- package/payload/server/public/assets/data-P-mcmnNi.js +0 -1
- package/payload/server/public/assets/graph-labels-qnBleOE6.js +0 -1
- package/payload/server/public/assets/tasks-fIJwYFWG.js +0 -1
- package/payload/server/public/assets/useSubAccountSwitcher-C_E8h07P.css +0 -1
- /package/payload/server/public/assets/{useSubAccountSwitcher-CFa6ZAT1.js → useSubAccountSwitcher-v8LyBzZN.js} +0 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cpu-triage — start/read pair for the device CPU diagnostic.
|
|
3
|
+
*
|
|
4
|
+
* The measurement samples for at least one full 120 s reconcile cycle, because
|
|
5
|
+
* anything shorter catches a random phase of a sawtooth and produces
|
|
6
|
+
* contradictory answers (see `platform/scripts/cpu-triage.sh`). No admin tool
|
|
7
|
+
* exceeds a 12 s envelope, so the run is detached and the caller polls.
|
|
8
|
+
*
|
|
9
|
+
* Three files per run live under `<configDir>/logs/`:
|
|
10
|
+
*
|
|
11
|
+
* cpu-triage-<id>.meta.json written here at start
|
|
12
|
+
* cpu-triage-<id>.json written by the runner, STREAMING
|
|
13
|
+
* cpu-triage-<id>.status written by the runner, last
|
|
14
|
+
*
|
|
15
|
+
* `.status` is the only readiness signal. The result file exists from the first
|
|
16
|
+
* byte of output and is half-written for the whole window, so keying on it
|
|
17
|
+
* reports a sampling run as complete.
|
|
18
|
+
*/
|
|
19
|
+
/** Shortest window the measurement is trustworthy over — one reconcile cycle. */
|
|
20
|
+
export declare const MIN_WINDOW_SEC = 120;
|
|
21
|
+
/** Runs retained on disk. Older runs are pruned when a new one starts. */
|
|
22
|
+
export declare const RETAINED_RUNS = 20;
|
|
23
|
+
export interface CpuTriagePaths {
|
|
24
|
+
/** `platform/scripts/cpu-triage.sh` */
|
|
25
|
+
scriptPath: string;
|
|
26
|
+
/** `platform/scripts/cpu-triage-run.sh` */
|
|
27
|
+
runnerPath: string;
|
|
28
|
+
/** `<configDir>/logs` — where per-run files live */
|
|
29
|
+
logsDir: string;
|
|
30
|
+
/** brand log receiving the op=start line */
|
|
31
|
+
serverLog: string;
|
|
32
|
+
}
|
|
33
|
+
export interface StartOptions {
|
|
34
|
+
windowSec?: number;
|
|
35
|
+
intervalSec?: number;
|
|
36
|
+
thresholdPct?: number;
|
|
37
|
+
/** A peer brand on the previous build, for like-for-like comparison. */
|
|
38
|
+
control?: string;
|
|
39
|
+
}
|
|
40
|
+
export interface RunMeta {
|
|
41
|
+
runId: string;
|
|
42
|
+
startedAt: string;
|
|
43
|
+
expectedCompleteAt: string;
|
|
44
|
+
windowSec: number;
|
|
45
|
+
args: string[];
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* `fault` separates a genuine defect the operator must act on (a missing
|
|
49
|
+
* script) from an ordinary empty state (nothing has been started yet). Only a
|
|
50
|
+
* fault is surfaced as an MCP error.
|
|
51
|
+
*/
|
|
52
|
+
export interface Refusal {
|
|
53
|
+
ok: false;
|
|
54
|
+
reason: "script-missing" | "runner-missing" | "window-too-short" | "no-runs" | "run-not-found";
|
|
55
|
+
fault: boolean;
|
|
56
|
+
detail: string;
|
|
57
|
+
}
|
|
58
|
+
export type StartOutcome = ({
|
|
59
|
+
ok: true;
|
|
60
|
+
resultPath: string;
|
|
61
|
+
} & RunMeta) | Refusal;
|
|
62
|
+
export type ReadOutcome = {
|
|
63
|
+
ok: true;
|
|
64
|
+
status: "running";
|
|
65
|
+
runId: string;
|
|
66
|
+
startedAt: string;
|
|
67
|
+
expectedCompleteAt: string;
|
|
68
|
+
windowSec: number;
|
|
69
|
+
} | {
|
|
70
|
+
ok: true;
|
|
71
|
+
status: "complete";
|
|
72
|
+
runId: string;
|
|
73
|
+
exitCode: number;
|
|
74
|
+
report: unknown;
|
|
75
|
+
} | {
|
|
76
|
+
ok: true;
|
|
77
|
+
status: "failed";
|
|
78
|
+
runId: string;
|
|
79
|
+
exitCode: number;
|
|
80
|
+
stderr: string;
|
|
81
|
+
detail?: string;
|
|
82
|
+
} | Refusal;
|
|
83
|
+
/** Injection seam so tests drive start without spawning a real process. */
|
|
84
|
+
export type SpawnRunner = (command: string, args: string[]) => void;
|
|
85
|
+
/**
|
|
86
|
+
* Start a detached triage run. Returns as soon as the run handle is on disk;
|
|
87
|
+
* the measurement continues in a process this one does not hold open.
|
|
88
|
+
*/
|
|
89
|
+
export declare function startCpuTriage(paths: CpuTriagePaths, opts: StartOptions, spawnRunner?: SpawnRunner): StartOutcome;
|
|
90
|
+
/** Read a triage run. Defaults to the most recent. */
|
|
91
|
+
export declare function readCpuTriage(paths: CpuTriagePaths, opts: {
|
|
92
|
+
runId?: string;
|
|
93
|
+
}): ReadOutcome;
|
|
94
|
+
//# sourceMappingURL=cpu-triage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cpu-triage.d.ts","sourceRoot":"","sources":["../../src/tools/cpu-triage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAOH,iFAAiF;AACjF,eAAO,MAAM,cAAc,MAAM,CAAC;AAElC,0EAA0E;AAC1E,eAAO,MAAM,aAAa,KAAK,CAAC;AAKhC,MAAM,WAAW,cAAc;IAC7B,uCAAuC;IACvC,UAAU,EAAE,MAAM,CAAC;IACnB,2CAA2C;IAC3C,UAAU,EAAE,MAAM,CAAC;IACnB,oDAAoD;IACpD,OAAO,EAAE,MAAM,CAAC;IAChB,4CAA4C;IAC5C,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,YAAY;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,wEAAwE;IACxE,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB;AAED;;;;GAIG;AACH,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,KAAK,CAAC;IACV,MAAM,EAAE,gBAAgB,GAAG,gBAAgB,GAAG,kBAAkB,GAAG,SAAS,GAAG,eAAe,CAAC;IAC/F,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,MAAM,YAAY,GACpB,CAAC;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,GAC5C,OAAO,CAAC;AAEZ,MAAM,MAAM,WAAW,GACnB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,SAAS,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,kBAAkB,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GAChH;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,UAAU,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,OAAO,CAAA;CAAE,GAClF;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,QAAQ,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GAChG,OAAO,CAAC;AAEZ,2EAA2E;AAC3E,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;AAqDpE;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,cAAc,EACrB,IAAI,EAAE,YAAY,EAClB,WAAW,GAAE,WAA2B,GACvC,YAAY,CAgEd;AAED,sDAAsD;AACtD,wBAAgB,aAAa,CAAC,KAAK,EAAE,cAAc,EAAE,IAAI,EAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,WAAW,CA8F1F"}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cpu-triage — start/read pair for the device CPU diagnostic.
|
|
3
|
+
*
|
|
4
|
+
* The measurement samples for at least one full 120 s reconcile cycle, because
|
|
5
|
+
* anything shorter catches a random phase of a sawtooth and produces
|
|
6
|
+
* contradictory answers (see `platform/scripts/cpu-triage.sh`). No admin tool
|
|
7
|
+
* exceeds a 12 s envelope, so the run is detached and the caller polls.
|
|
8
|
+
*
|
|
9
|
+
* Three files per run live under `<configDir>/logs/`:
|
|
10
|
+
*
|
|
11
|
+
* cpu-triage-<id>.meta.json written here at start
|
|
12
|
+
* cpu-triage-<id>.json written by the runner, STREAMING
|
|
13
|
+
* cpu-triage-<id>.status written by the runner, last
|
|
14
|
+
*
|
|
15
|
+
* `.status` is the only readiness signal. The result file exists from the first
|
|
16
|
+
* byte of output and is half-written for the whole window, so keying on it
|
|
17
|
+
* reports a sampling run as complete.
|
|
18
|
+
*/
|
|
19
|
+
import { spawn } from "node:child_process";
|
|
20
|
+
import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
21
|
+
import { dirname, join } from "node:path";
|
|
22
|
+
import { randomBytes } from "node:crypto";
|
|
23
|
+
/** Shortest window the measurement is trustworthy over — one reconcile cycle. */
|
|
24
|
+
export const MIN_WINDOW_SEC = 120;
|
|
25
|
+
/** Runs retained on disk. Older runs are pruned when a new one starts. */
|
|
26
|
+
export const RETAINED_RUNS = 20;
|
|
27
|
+
/** Grace added to the window when reporting when a run should be readable. */
|
|
28
|
+
const COMPLETION_SLACK_MS = 30_000;
|
|
29
|
+
const spawnDetached = (command, args) => {
|
|
30
|
+
const child = spawn(command, args, { detached: true, stdio: "ignore" });
|
|
31
|
+
child.unref();
|
|
32
|
+
};
|
|
33
|
+
function metaPath(paths, runId) {
|
|
34
|
+
return join(paths.logsDir, `cpu-triage-${runId}.meta.json`);
|
|
35
|
+
}
|
|
36
|
+
function resultPath(paths, runId) {
|
|
37
|
+
return join(paths.logsDir, `cpu-triage-${runId}.json`);
|
|
38
|
+
}
|
|
39
|
+
function statusPath(paths, runId) {
|
|
40
|
+
return join(paths.logsDir, `cpu-triage-${runId}.status`);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Timestamp-prefixed so "most recent" is a filename sort rather than a stat
|
|
44
|
+
* walk, and so a run id is legible in a log line without a lookup.
|
|
45
|
+
*/
|
|
46
|
+
function mintRunId(now) {
|
|
47
|
+
const iso = now.toISOString().replace(/[-:]/g, "").replace(/\.\d+Z$/, "");
|
|
48
|
+
return `${iso}-${randomBytes(2).toString("hex")}`;
|
|
49
|
+
}
|
|
50
|
+
/** Run ids present on disk, oldest first. */
|
|
51
|
+
function listRunIds(paths) {
|
|
52
|
+
if (!existsSync(paths.logsDir))
|
|
53
|
+
return [];
|
|
54
|
+
return readdirSync(paths.logsDir)
|
|
55
|
+
.filter((f) => f.startsWith("cpu-triage-") && f.endsWith(".meta.json"))
|
|
56
|
+
.map((f) => f.slice("cpu-triage-".length, -".meta.json".length))
|
|
57
|
+
.sort();
|
|
58
|
+
}
|
|
59
|
+
function prune(paths, keep) {
|
|
60
|
+
const ids = listRunIds(paths);
|
|
61
|
+
for (const id of ids.slice(0, Math.max(0, ids.length - keep))) {
|
|
62
|
+
for (const p of [metaPath(paths, id), resultPath(paths, id), statusPath(paths, id)]) {
|
|
63
|
+
rmSync(p, { force: true });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function logLine(paths, line) {
|
|
68
|
+
try {
|
|
69
|
+
mkdirSync(dirname(paths.serverLog), { recursive: true });
|
|
70
|
+
appendFileSync(paths.serverLog, `${new Date().toISOString()} ${line}\n`);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
// Best-effort. A log write must never take the diagnostic down with it.
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Start a detached triage run. Returns as soon as the run handle is on disk;
|
|
78
|
+
* the measurement continues in a process this one does not hold open.
|
|
79
|
+
*/
|
|
80
|
+
export function startCpuTriage(paths, opts, spawnRunner = spawnDetached) {
|
|
81
|
+
const windowSec = opts.windowSec ?? 300;
|
|
82
|
+
// The script refuses a short window with exit 2, but detached that refusal
|
|
83
|
+
// arrives one full window late. It has to be synchronous.
|
|
84
|
+
if (windowSec < MIN_WINDOW_SEC) {
|
|
85
|
+
return {
|
|
86
|
+
ok: false,
|
|
87
|
+
reason: "window-too-short",
|
|
88
|
+
fault: true,
|
|
89
|
+
detail: `windowSec=${windowSec} is shorter than the ${MIN_WINDOW_SEC}s reconcile cycle. ` +
|
|
90
|
+
`Samples shorter than the cycle land on a random phase of a sawtooth and contradict each other.`,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
if (!existsSync(paths.scriptPath)) {
|
|
94
|
+
return {
|
|
95
|
+
ok: false,
|
|
96
|
+
reason: "script-missing",
|
|
97
|
+
fault: true,
|
|
98
|
+
detail: `cpu-triage.sh is not on this device at ${paths.scriptPath}. No measurement was taken.`,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
if (!existsSync(paths.runnerPath)) {
|
|
102
|
+
return {
|
|
103
|
+
ok: false,
|
|
104
|
+
reason: "runner-missing",
|
|
105
|
+
fault: true,
|
|
106
|
+
detail: `cpu-triage-run.sh is not on this device at ${paths.runnerPath}. No measurement was taken.`,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
const now = new Date();
|
|
110
|
+
const runId = mintRunId(now);
|
|
111
|
+
const measureArgs = ["--window", String(windowSec)];
|
|
112
|
+
if (opts.intervalSec !== undefined)
|
|
113
|
+
measureArgs.push("--interval", String(opts.intervalSec));
|
|
114
|
+
if (opts.thresholdPct !== undefined)
|
|
115
|
+
measureArgs.push("--threshold", String(opts.thresholdPct));
|
|
116
|
+
if (opts.control)
|
|
117
|
+
measureArgs.push("--control", opts.control);
|
|
118
|
+
const meta = {
|
|
119
|
+
runId,
|
|
120
|
+
startedAt: now.toISOString(),
|
|
121
|
+
expectedCompleteAt: new Date(now.getTime() + windowSec * 1000 + COMPLETION_SLACK_MS).toISOString(),
|
|
122
|
+
windowSec,
|
|
123
|
+
args: measureArgs,
|
|
124
|
+
};
|
|
125
|
+
mkdirSync(paths.logsDir, { recursive: true });
|
|
126
|
+
// Prune before the new run lands so it can never prune itself.
|
|
127
|
+
prune(paths, RETAINED_RUNS - 1);
|
|
128
|
+
writeFileSync(metaPath(paths, runId), JSON.stringify(meta, null, 2));
|
|
129
|
+
logLine(paths, `[cpu-triage] op=start runId=${runId} windowSec=${windowSec}`);
|
|
130
|
+
spawnRunner(paths.runnerPath, [
|
|
131
|
+
"--run-id", runId,
|
|
132
|
+
"--out-dir", paths.logsDir,
|
|
133
|
+
"--log", paths.serverLog,
|
|
134
|
+
"--script", paths.scriptPath,
|
|
135
|
+
"--", ...measureArgs,
|
|
136
|
+
]);
|
|
137
|
+
return { ok: true, ...meta, resultPath: resultPath(paths, runId) };
|
|
138
|
+
}
|
|
139
|
+
/** Read a triage run. Defaults to the most recent. */
|
|
140
|
+
export function readCpuTriage(paths, opts) {
|
|
141
|
+
const ids = listRunIds(paths);
|
|
142
|
+
if (ids.length === 0) {
|
|
143
|
+
return {
|
|
144
|
+
ok: false,
|
|
145
|
+
reason: "no-runs",
|
|
146
|
+
fault: false,
|
|
147
|
+
detail: "No cpu-triage run has been started on this device. Call cpu-triage-start first.",
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
const runId = opts.runId ?? ids[ids.length - 1];
|
|
151
|
+
if (!ids.includes(runId)) {
|
|
152
|
+
return {
|
|
153
|
+
ok: false,
|
|
154
|
+
reason: "run-not-found",
|
|
155
|
+
fault: false,
|
|
156
|
+
detail: `No run ${runId} on this device. Available: ${ids.join(", ")}`,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
let meta;
|
|
160
|
+
try {
|
|
161
|
+
meta = JSON.parse(readFileSync(metaPath(paths, runId), "utf-8"));
|
|
162
|
+
}
|
|
163
|
+
catch (err) {
|
|
164
|
+
return {
|
|
165
|
+
ok: true,
|
|
166
|
+
status: "failed",
|
|
167
|
+
runId,
|
|
168
|
+
exitCode: -1,
|
|
169
|
+
stderr: "",
|
|
170
|
+
detail: `run metadata unreadable: ${err instanceof Error ? err.message : String(err)}`,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
// The status marker is the only readiness signal. The result file is
|
|
174
|
+
// streamed and exists half-written for the whole sampling window.
|
|
175
|
+
const status = statusPath(paths, runId);
|
|
176
|
+
if (!existsSync(status)) {
|
|
177
|
+
return {
|
|
178
|
+
ok: true,
|
|
179
|
+
status: "running",
|
|
180
|
+
runId,
|
|
181
|
+
startedAt: meta.startedAt,
|
|
182
|
+
expectedCompleteAt: meta.expectedCompleteAt,
|
|
183
|
+
windowSec: meta.windowSec,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
const statusText = readFileSync(status, "utf-8");
|
|
187
|
+
const match = statusText.match(/^exit=(-?\d+)/);
|
|
188
|
+
const exitCode = match ? Number(match[1]) : -1;
|
|
189
|
+
// Split on the first newline rather than slicing from indexOf: a status file
|
|
190
|
+
// with no trailing newline has indexOf === -1, and slice(0) would hand back
|
|
191
|
+
// the whole text — reporting `exit=0` itself as the run's stderr.
|
|
192
|
+
const newline = statusText.indexOf("\n");
|
|
193
|
+
const stderr = newline === -1 ? "" : statusText.slice(newline + 1).trim();
|
|
194
|
+
// Exit 1 is the script's sustained-hot-core gate — a finding, not a failure.
|
|
195
|
+
// Only a precondition failure (2) or an unexpected code is failed.
|
|
196
|
+
if (exitCode !== 0 && exitCode !== 1) {
|
|
197
|
+
return { ok: true, status: "failed", runId, exitCode, stderr };
|
|
198
|
+
}
|
|
199
|
+
const result = resultPath(paths, runId);
|
|
200
|
+
// An absent result file and an unparseable one are different conditions and
|
|
201
|
+
// must not share a message — "will not parse" sends the reader looking at
|
|
202
|
+
// JSON when the file was never written.
|
|
203
|
+
if (!existsSync(result)) {
|
|
204
|
+
return {
|
|
205
|
+
ok: true,
|
|
206
|
+
status: "failed",
|
|
207
|
+
runId,
|
|
208
|
+
exitCode,
|
|
209
|
+
stderr,
|
|
210
|
+
detail: `the run exited ${exitCode} but wrote no report at ${result}`,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
let report;
|
|
214
|
+
try {
|
|
215
|
+
report = JSON.parse(readFileSync(result, "utf-8"));
|
|
216
|
+
}
|
|
217
|
+
catch (err) {
|
|
218
|
+
return {
|
|
219
|
+
ok: true,
|
|
220
|
+
status: "failed",
|
|
221
|
+
runId,
|
|
222
|
+
exitCode,
|
|
223
|
+
stderr,
|
|
224
|
+
detail: `the run exited ${exitCode} but its report will not parse: ${err instanceof Error ? err.message : String(err)}`,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
return { ok: true, status: "complete", runId, exitCode, report };
|
|
228
|
+
}
|
|
229
|
+
//# sourceMappingURL=cpu-triage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cpu-triage.js","sourceRoot":"","sources":["../../src/tools/cpu-triage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAC3C,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,SAAS,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAClH,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE1C,iFAAiF;AACjF,MAAM,CAAC,MAAM,cAAc,GAAG,GAAG,CAAC;AAElC,0EAA0E;AAC1E,MAAM,CAAC,MAAM,aAAa,GAAG,EAAE,CAAC;AAEhC,8EAA8E;AAC9E,MAAM,mBAAmB,GAAG,MAAM,CAAC;AAsDnC,MAAM,aAAa,GAAgB,CAAC,OAAO,EAAE,IAAI,EAAE,EAAE;IACnD,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,EAAE,IAAI,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,CAAC;IACxE,KAAK,CAAC,KAAK,EAAE,CAAC;AAChB,CAAC,CAAC;AAEF,SAAS,QAAQ,CAAC,KAAqB,EAAE,KAAa;IACpD,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,cAAc,KAAK,YAAY,CAAC,CAAC;AAC9D,CAAC;AACD,SAAS,UAAU,CAAC,KAAqB,EAAE,KAAa;IACtD,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,cAAc,KAAK,OAAO,CAAC,CAAC;AACzD,CAAC;AACD,SAAS,UAAU,CAAC,KAAqB,EAAE,KAAa;IACtD,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,cAAc,KAAK,SAAS,CAAC,CAAC;AAC3D,CAAC;AAED;;;GAGG;AACH,SAAS,SAAS,CAAC,GAAS;IAC1B,MAAM,GAAG,GAAG,GAAG,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;IAC1E,OAAO,GAAG,GAAG,IAAI,WAAW,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;AACpD,CAAC;AAED,6CAA6C;AAC7C,SAAS,UAAU,CAAC,KAAqB;IACvC,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,OAAO,CAAC;QAAE,OAAO,EAAE,CAAC;IAC1C,OAAO,WAAW,CAAC,KAAK,CAAC,OAAO,CAAC;SAC9B,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC;SACtE,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,aAAa,CAAC,MAAM,EAAE,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC;SAC/D,IAAI,EAAE,CAAC;AACZ,CAAC;AAED,SAAS,KAAK,CAAC,KAAqB,EAAE,IAAY;IAChD,MAAM,GAAG,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC;IAC9B,KAAK,MAAM,EAAE,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC,EAAE,CAAC;QAC9D,KAAK,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,EAAE,CAAC,EAAE,UAAU,CAAC,KAAK,EAAE,EAAE,CAAC,EAAE,UAAU,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC;YACpF,MAAM,CAAC,CAAC,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QAC7B,CAAC;IACH,CAAC;AACH,CAAC;AAED,SAAS,OAAO,CAAC,KAAqB,EAAE,IAAY;IAClD,IAAI,CAAC;QACH,SAAS,CAAC,OAAO,CAAC,KAAK,CAAC,SAAS,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACzD,cAAc,CAAC,KAAK,CAAC,SAAS,EAAE,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,IAAI,IAAI,IAAI,CAAC,CAAC;IAC3E,CAAC;IAAC,MAAM,CAAC;QACP,wEAAwE;IAC1E,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,cAAc,CAC5B,KAAqB,EACrB,IAAkB,EAClB,cAA2B,aAAa;IAExC,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,IAAI,GAAG,CAAC;IAExC,2EAA2E;IAC3E,0DAA0D;IAC1D,IAAI,SAAS,GAAG,cAAc,EAAE,CAAC;QAC/B,OAAO;YACL,EAAE,EAAE,KAAK;YACT,MAAM,EAAE,kBAAkB;YAC1B,KAAK,EAAE,IAAI;YACX,MAAM,EACJ,aAAa,SAAS,wBAAwB,cAAc,qBAAqB;gBACjF,gGAAgG;SACnG,CAAC;IACJ,CAAC;IACD,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,UAAU,CAAC,EAAE,CAAC;QAClC,OAAO;YACL,EAAE,EAAE,KAAK;YACT,MAAM,EAAE,gBAAgB;YACxB,KAAK,EAAE,IAAI;YACX,MAAM,EAAE,0CAA0C,KAAK,CAAC,UAAU,6BAA6B;SAChG,CAAC;IACJ,CAAC;IACD,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,UAAU,CAAC,EAAE,CAAC;QAClC,OAAO;YACL,EAAE,EAAE,KAAK;YACT,MAAM,EAAE,gBAAgB;YACxB,KAAK,EAAE,IAAI;YACX,MAAM,EAAE,8CAA8C,KAAK,CAAC,UAAU,6BAA6B;SACpG,CAAC;IACJ,CAAC;IAED,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC;IACvB,MAAM,KAAK,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC;IAE7B,MAAM,WAAW,GAAa,CAAC,UAAU,EAAE,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC;IAC9D,IAAI,IAAI,CAAC,WAAW,KAAK,SAAS;QAAE,WAAW,CAAC,IAAI,CAAC,YAAY,EAAE,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC;IAC7F,IAAI,IAAI,CAAC,YAAY,KAAK,SAAS;QAAE,WAAW,CAAC,IAAI,CAAC,aAAa,EAAE,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC;IAChG,IAAI,IAAI,CAAC,OAAO;QAAE,WAAW,CAAC,IAAI,CAAC,WAAW,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;IAE9D,MAAM,IAAI,GAAY;QACpB,KAAK;QACL,SAAS,EAAE,GAAG,CAAC,WAAW,EAAE;QAC5B,kBAAkB,EAAE,IAAI,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,SAAS,GAAG,IAAI,GAAG,mBAAmB,CAAC,CAAC,WAAW,EAAE;QAClG,SAAS;QACT,IAAI,EAAE,WAAW;KAClB,CAAC;IAEF,SAAS,CAAC,KAAK,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC9C,+DAA+D;IAC/D,KAAK,CAAC,KAAK,EAAE,aAAa,GAAG,CAAC,CAAC,CAAC;IAChC,aAAa,CAAC,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;IAErE,OAAO,CAAC,KAAK,EAAE,+BAA+B,KAAK,cAAc,SAAS,EAAE,CAAC,CAAC;IAE9E,WAAW,CAAC,KAAK,CAAC,UAAU,EAAE;QAC5B,UAAU,EAAE,KAAK;QACjB,WAAW,EAAE,KAAK,CAAC,OAAO;QAC1B,OAAO,EAAE,KAAK,CAAC,SAAS;QACxB,UAAU,EAAE,KAAK,CAAC,UAAU;QAC5B,IAAI,EAAE,GAAG,WAAW;KACrB,CAAC,CAAC;IAEH,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,GAAG,IAAI,EAAE,UAAU,EAAE,UAAU,CAAC,KAAK,EAAE,KAAK,CAAC,EAAE,CAAC;AACrE,CAAC;AAED,sDAAsD;AACtD,MAAM,UAAU,aAAa,CAAC,KAAqB,EAAE,IAAwB;IAC3E,MAAM,GAAG,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC;IAC9B,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACrB,OAAO;YACL,EAAE,EAAE,KAAK;YACT,MAAM,EAAE,SAAS;YACjB,KAAK,EAAE,KAAK;YACZ,MAAM,EAAE,iFAAiF;SAC1F,CAAC;IACJ,CAAC;IAED,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,GAAG,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAChD,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO;YACL,EAAE,EAAE,KAAK;YACT,MAAM,EAAE,eAAe;YACvB,KAAK,EAAE,KAAK;YACZ,MAAM,EAAE,UAAU,KAAK,+BAA+B,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE;SACvE,CAAC;IACJ,CAAC;IAED,IAAI,IAAa,CAAC;IAClB,IAAI,CAAC;QACH,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC,EAAE,OAAO,CAAC,CAAY,CAAC;IAC9E,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO;YACL,EAAE,EAAE,IAAI;YACR,MAAM,EAAE,QAAQ;YAChB,KAAK;YACL,QAAQ,EAAE,CAAC,CAAC;YACZ,MAAM,EAAE,EAAE;YACV,MAAM,EAAE,4BAA4B,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE;SACvF,CAAC;IACJ,CAAC;IAED,qEAAqE;IACrE,kEAAkE;IAClE,MAAM,MAAM,GAAG,UAAU,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;IACxC,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;QACxB,OAAO;YACL,EAAE,EAAE,IAAI;YACR,MAAM,EAAE,SAAS;YACjB,KAAK;YACL,SAAS,EAAE,IAAI,CAAC,SAAS;YACzB,kBAAkB,EAAE,IAAI,CAAC,kBAAkB;YAC3C,SAAS,EAAE,IAAI,CAAC,SAAS;SAC1B,CAAC;IACJ,CAAC;IAED,MAAM,UAAU,GAAG,YAAY,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjD,MAAM,KAAK,GAAG,UAAU,CAAC,KAAK,CAAC,eAAe,CAAC,CAAC;IAChD,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC/C,6EAA6E;IAC7E,4EAA4E;IAC5E,kEAAkE;IAClE,MAAM,OAAO,GAAG,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IACzC,MAAM,MAAM,GAAG,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,KAAK,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IAE1E,6EAA6E;IAC7E,mEAAmE;IACnE,IAAI,QAAQ,KAAK,CAAC,IAAI,QAAQ,KAAK,CAAC,EAAE,CAAC;QACrC,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;IACjE,CAAC;IAED,MAAM,MAAM,GAAG,UAAU,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;IACxC,4EAA4E;IAC5E,0EAA0E;IAC1E,wCAAwC;IACxC,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;QACxB,OAAO;YACL,EAAE,EAAE,IAAI;YACR,MAAM,EAAE,QAAQ;YAChB,KAAK;YACL,QAAQ;YACR,MAAM;YACN,MAAM,EAAE,kBAAkB,QAAQ,2BAA2B,MAAM,EAAE;SACtE,CAAC;IACJ,CAAC;IAED,IAAI,MAAe,CAAC;IACpB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACrD,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO;YACL,EAAE,EAAE,IAAI;YACR,MAAM,EAAE,QAAQ;YAChB,KAAK;YACL,QAAQ;YACR,MAAM;YACN,MAAM,EAAE,kBAAkB,QAAQ,mCAAmC,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE;SACxH,CAAC;IACJ,CAAC;IAED,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;AACnE,CAAC"}
|
|
@@ -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:c7a28352289a7121b08a8d8f2d5bcb1e79497a6acda2e355756cf7b02051c3a5
|
|
5
5
|
brand: maxy-code
|
|
6
6
|
product-name: Maxy
|
|
7
7
|
---
|
|
@@ -1285,6 +1285,18 @@ The most common cause is wrong nameservers on the domain. The domain must use Cl
|
|
|
1285
1285
|
- The `/data` page handles this itself: files over 95 MB are split into ~48 MB parts and sent one at a time under the cap, and the server reassembles them. No plan change or dashboard action is needed for uploads up to the 2 GB app ceiling.
|
|
1286
1286
|
- Raising the proxied body limit is a paid-plan change made on the Cloudflare dashboard, not something the agent does.
|
|
1287
1287
|
|
|
1288
|
+
## The client file portal
|
|
1289
|
+
|
|
1290
|
+
A portal gives named people their own passcode-protected page on a custom domain. Two things sit on it.
|
|
1291
|
+
|
|
1292
|
+
**What they send you.** Anything they upload lands in the account's own storage, one area per person, and nobody sees anyone else's files.
|
|
1293
|
+
|
|
1294
|
+
**What you have prepared for them.** The same page shows the account's deliverable folders — the work produced for that client. Which folders those are is not a setting to maintain: it follows the account's own filing structure, so a business whose work is organised into quotes sees quotes, and a business organised some other way sees that instead. Operator working data, the client's own uploads, and anything internal are never shown.
|
|
1295
|
+
|
|
1296
|
+
Folder names and sizes are published to the portal on a schedule, so the client can browse even while the device is off. The files themselves stay on the device. Opening one checks the device is reachable and then hands the browser a link that expires after a few minutes; if the device is down the client is told exactly that, rather than getting a broken download.
|
|
1297
|
+
|
|
1298
|
+
Each person is bound to one account when they are enrolled. Someone enrolled before this existed has no account bound and sees no folders until they are enrolled again.
|
|
1299
|
+
|
|
1288
1300
|
## What the agent does and does not do
|
|
1289
1301
|
|
|
1290
1302
|
**Does:** invokes `cloudflared` directly via Bash, following `plugins/cloudflare/references/manual-setup.md` step by step; quotes click-paths from the reference files verbatim; verifies external reachability with `curl -I` and surfaces the response.
|
|
@@ -2943,7 +2955,7 @@ either is a regression.
|
|
|
2943
2955
|
|
|
2944
2956
|
**Row title resolution.** Both the sidebar (`/sidebar-sessions`) and the manager's own row payload resolve a row's title in the same order: operator rename → Claude Code `ai-title` → first non-CLI user message → 8-char sessionId prefix. The operator-rename tier is the on-disk `<accountDir>/session-titles.json` (the manager's `UserTitleStore`), keyed by the CC sessionId — the sidebar reads that same file, so a write to the store lights up both surfaces with one write.
|
|
2945
2957
|
|
|
2946
|
-
**Session row delete, rename, and select mode.** The sidebar Sessions row supports three operator affordances. **Rename** (kebab → Rename) opens the inline edit input with its existing title pre-selected on focus, so the first keystroke replaces the whole title rather than appending to it — on both the sidebar (`conv-name-edit`) and the `/chat` Conversations flyout (`op-conv-rename-input`). **Delete** (kebab → Delete) arms a confirm that names the session (its label plus the 8-char id prefix) and renders as a popover anchored beside the row it will delete — reusing the same `AnchoredPopover` the kebab menu uses, not the shared bottom-right info dock — so the target is unambiguous; it still states the delete is permanent and, for a live row, that the session is stopped first. **Select mode** (a "Select" control in the Sessions header) turns each row into a checkbox and shows a "Delete N" bar; the row body toggles selection instead of opening, the per-row kebab is hidden, and confirming a count-based `BulkDeleteConfirmModal` (which names N and how many are live) loops the single-id `session-delete` over the selection (no route change — `/api/admin/session-delete` stays single-id). Bulk deletion is instrumented with a `[admin-ui] sidebar-bulk-delete op=start count=<N> live=<M>` / `op=done requested=<N> ok=<X> failed=<Y>` envelope around the per-session `sidebar-session-delete` lines, so a silently-dropped item shows as `ok+failed < requested`. Outside select mode the single-delete and row-open behaviours are unchanged.
|
|
2958
|
+
**Session row delete, rename, and select mode.** The sidebar Sessions row supports three operator affordances. **Rename** (kebab → Rename) opens the inline edit input with its existing title pre-selected on focus, so the first keystroke replaces the whole title rather than appending to it — on both the sidebar (`conv-name-edit`) and the `/chat` Conversations flyout (`op-conv-rename-input`). **Delete** (kebab → Delete) arms a confirm that names the session (its label plus the 8-char id prefix) and renders as a popover anchored beside the row it will delete — reusing the same `AnchoredPopover` the kebab menu uses, not the shared bottom-right info dock — so the target is unambiguous; it still states the delete is permanent and, for a live row, that the session is stopped first. **Usage & cost** (kebab → Usage & cost) opens the per-day active-time / token / GBP-cost table for that session, and anchors beside its row through the same `AnchoredPopover`, with no scrim, so the rest of the dashboard stays legible and interactive while it is open. `AnchoredPopover` right-aligns to the anchor's rect and clamps horizontally against both viewport edges. Vertically it places below when the popover fits there, above when it fits there instead, and otherwise on whichever side has more room — and in every case it bounds the popover's height to the room on the side it chose. That bound is what keeps a long session's panel anchored: a 30-day usage table is taller than the viewport, and clamping it to fit would push it up over the very row it describes, which is not "anchored" to anything. Instead it sits flush against its row and scrolls internally. Because the bound is derived from the anchor rect and the viewport rather than from the popover's own height, the `ResizeObserver` pass it triggers recomputes the same values and settles in one extra pass. The horizontal right-edge clamp does not engage for a left-docked sidebar row (a popover wider than its anchor overflows left, which the left clamp handles); it is kept because the component is general-purpose and is covered by unit test only. Positioning is instrumented, because a mispositioned client-side panel throws nothing and the operator just sees a menu item that did nothing: every positioning pass (each open, each `ResizeObserver` callback, each window resize) emits `[anchored-popover] op=anchor id=<class>:<sessionId> present=<bool> rect=<x,y,w,h>` then `op=placed … maxH=<px> flipped=<bool> clampedLeft=<bool> clampedRight=<bool>`, and `op=offscreen` at `console.error` when the anchor is detached from layout or a placed rect misses the viewport. A pass that computes exactly what the previous pass did is suppressed, so mount and its trailing `ResizeObserver` callback log once rather than twice; the `op=offscreen` assertion is never suppressed. A healthy open is `present=true` with a non-zero rect followed by `op=placed` and no `op=offscreen`. `present=false` means the row is not in `rowRefs` — the row unmounted or its ref callback did not run — and the panel docks bottom-right rather than vanishing, because an invisible panel is indistinguishable from a broken menu item. An all-zero rect means the anchor is detached. The assertion is gated on the popover having non-zero measured size, so an unlaid-out popover (pre-paint, or jsdom) stays silent; a genuinely zero-size popover in production is therefore not reported. **Select mode** (a "Select" control in the Sessions header) turns each row into a checkbox and shows a "Delete N" bar; the row body toggles selection instead of opening, the per-row kebab is hidden, and confirming a count-based `BulkDeleteConfirmModal` (which names N and how many are live) loops the single-id `session-delete` over the selection (no route change — `/api/admin/session-delete` stays single-id). Bulk deletion is instrumented with a `[admin-ui] sidebar-bulk-delete op=start count=<N> live=<M>` / `op=done requested=<N> ok=<X> failed=<Y>` envelope around the per-session `sidebar-session-delete` lines, so a silently-dropped item shows as `ok+failed < requested`. Outside select mode the single-delete and row-open behaviours are unchanged.
|
|
2947
2959
|
|
|
2948
2960
|
**`/chat` empty-state starter chips.** While the admin webchat has no conversation JSONL yet (`GET /api/webchat/session` → `projectDir:null`), `app/chat/page.tsx` renders a "What's up next, {givenName}?" greeting (given name = first whitespace token of the admin session's `userName`; degrades to "What's up next?" without one) above three one-tap starter chips (replacing the earlier read-only lists): **Catch me up** (`Inbox`), **My day** (`CalendarClock`), **To-do** (`ListTodo`). `GreetingPanel` no longer fetches `GET /api/webchat/greeting` — the headline reads from the `userName` prop alone. A tap dispatches the chip's verbatim prompt through the existing `send()` with zero typing, via the same arm/fire path as the `?q=` handoff seed: `dispatchChip` logs `[admin-ui] op=chip-tap id=<catch-up|my-day|to-do> chars=<n>`, sets the composer text, and arms a one-shot effect that fires `send()` once the text matches. Chips gate to `variant` admin/operator; the public surface shows the headline alone. The first journaled turn replaces the panel with the transcript. The `/api/webchat/greeting` route (`server/routes/webchat-greeting.ts`) and `specialist-roster.ts` are now unconsumed by this surface — slimming them is a separate follow-up. The full webchat architecture lives in `.docs/admin-webchat-native-channel.md`.
|
|
2949
2961
|
|
|
@@ -34,7 +34,7 @@ The plugin registers no agent-facing MCP tools. Every Cloudflare operation is th
|
|
|
34
34
|
| [cloudflare/SKILL.md](skills/cloudflare/SKILL.md) | The entry point for every Cloudflare operation. Routes each operation class to its reference, names the outcome contracts (tunnel external HTTP 200; hosting deployed URL; D1 round-trip), and states the tool-discipline + secret-redaction rules that bind the agent. |
|
|
35
35
|
| [site-deploy/SKILL.md](skills/site-deploy/SKILL.md) | Assemble one canonical content tree into a live Pages deployment on its custom domain that survives the install device being offline. Names the single-source-tree convention, the reused per-scope token, the form-to-D1 trigger, the R2 binding trigger, Open Graph correctness, and the cache-busted live-`200` done-gate; defers the command blocks to `hosting-sites.md`, `d1-data-capture.md` and `r2-object-storage.md`. Admin-run; a content specialist hands the deploy off to it. |
|
|
36
36
|
| [calendar-site/SKILL.md](skills/calendar-site/SKILL.md) | Assemble and deploy one account's public booking page — a static page plus edge Functions over a D1 `bookings` table — on its own custom domain, so a submission survives the install device being offline. Admin-run; defers the deploy and the live done-gate to `site-deploy`. |
|
|
37
|
-
| [data-portal/SKILL.md](skills/data-portal/SKILL.md) | Stand up one account's gated file-drop portal on its own custom domain: each enrolled person signs in with their own passcode and can upload, list, download and delete only their own files, into one R2 bucket under a per-person prefix with a D1 manifest row per upload. The bucket is created by the owning account's own session — the broker registers ownership to the creator and never reassigns it — while the admin assembles and deploys. Admin-run; defers the deploy and the live done-gate to `site-deploy`. |
|
|
37
|
+
| [data-portal/SKILL.md](skills/data-portal/SKILL.md) | Stand up one account's gated file-drop portal on its own custom domain: each enrolled person signs in with their own passcode and can upload, list, download and delete only their own files, into one R2 bucket under a per-person prefix with a D1 manifest row per upload. The same portal also shows that sub-account's deliverable folders, published from the device as metadata only and opened through a short-lived signed link. The bucket is created by the owning account's own session — the broker registers ownership to the creator and never reassigns it — while the admin assembles and deploys. Admin-run; defers the deploy and the live done-gate to `site-deploy`. |
|
|
38
38
|
|
|
39
39
|
### References
|
|
40
40
|
|
|
@@ -69,6 +69,31 @@ The setup-done claim only fires when `curl -I https://<hostname>` issued from ou
|
|
|
69
69
|
- **Cloudflare account identity** — `cert.pem` from OAuth. One account per brand per device.
|
|
70
70
|
- **Account binding drift** — `~/{configDir}/cloudflared/account-binding.json` is a historical drift marker. Reset by `rm -rf ~/.${BRAND}/cloudflared/` per `references/reset-guide.md` when switching accounts.
|
|
71
71
|
|
|
72
|
+
## Data-portal folder index
|
|
73
|
+
|
|
74
|
+
The portal shows a sub-account's deliverable folders alongside what the client uploaded. Which folders those are is never a list in the code: it is derived per account from that account's own `SCHEMA.md` — the buckets in its `<!-- ontology-buckets -->` region plus `output`, intersected with its `allowed-top-level` block. A brand on a different vertical exposes different folders with no code change. `bin/schema-exposed-dirs.mjs` is the only place that decision is made.
|
|
75
|
+
|
|
76
|
+
`bin/portal-index-push.mjs` walks those folders and full-replaces the account's rows in the portal's D1 `directory` table each cycle. Metadata only — names, sizes, times — so listing survives the device being offline, and a deleted or renamed file converges on the next cycle without a tombstone. The scheduling heartbeat runs it hourly for every account carrying a `data-portal.json`; it is also runnable standalone, which is how it stays operable while the heartbeat spawn path is unproven on a device.
|
|
77
|
+
|
|
78
|
+
Opening a file reaches the install: the portal probes it, then mints an `/api/portal/fetch` link signed with the account's `PORTAL_FETCH_SECRET` and valid for five minutes. The install verifies the signature **and independently re-derives the exposed folder set**, so a correctly signed link for a non-exposed path is still refused. That second check is the security boundary, not the signature.
|
|
79
|
+
|
|
80
|
+
What to watch, on the device:
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
[portal-index] op=resolve account=<id> schemaPresent=<bool> allowed=<n> domainBuckets=<dirs|none> exposed=<dirs|none>
|
|
84
|
+
[portal-index] op=replace account=<id> deleted=all inserted=<n>
|
|
85
|
+
[portal-index] op=verify account=<id> rowsAfter=<n> expected=<n>
|
|
86
|
+
[portal-index] op=collision account=<id> dir=<dir> claimedBy=plugin-owned
|
|
87
|
+
[portal-index] op=exposed-changed account=<id> added=<dirs|none> removed=<dirs|none>
|
|
88
|
+
[portal-index] op=audit accounts=<n> stale=<n> never-ran=<n>
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`exposed=none` where deliverables exist means the schema was missing or carried no ontology region — not that the walk found nothing. `rowsAfter` disagreeing with `expected` is a half-applied replace. An `op=collision` line explains a folder that has silently vanished from the client's view, which nothing else would report. `op=exposed-changed` fires only on a delta, so it is rare and always worth reading; a `removed=` naming a populated bucket is the client-visible regression.
|
|
92
|
+
|
|
93
|
+
`op=audit` is the standing check, and it is the only one that speaks when the push does **not**. It runs in-process inside the heartbeat rather than as a spawn, and reads only local state, so it cannot be blinded by the same failure it is watching for. It fires hourly even when everything is healthy — an audit that goes quiet on success is indistinguishable from an audit that stopped running. `result=never-ran` on an account with a `data-portal.json` means no cycle has ever succeeded for it; `result=stale` with `ageMin` means cycles have stopped. Its input is written only after `op=verify`, so a stale entry means the push stopped succeeding rather than merely stopped logging.
|
|
94
|
+
|
|
95
|
+
On the portal side `[data-portal] op=list` carries `account=` and `op=fetch-through` carries the outcome; on the install `[portal-fetch]` logs `op=request` then exactly one of `op=served`, `op=denied`, `op=miss`. A portal `fetch-through` with no matching install `op=request` means the request never arrived, rather than having been refused. `[portal-fetch] op=no-secret account=<id>` is an unconfigured portal, which the `op=denied reason=secret` line alone would not distinguish from a bad signature.
|
|
96
|
+
|
|
72
97
|
## Discipline
|
|
73
98
|
|
|
74
99
|
The agent's permitted surfaces are: direct `cloudflared` and `wrangler` invocations via Bash following the references; Cloudflare API calls authenticated by a reused per-scope narrow token, minted once if absent and persisted to the secrets file (the master token stays in that same file, never on a command line that echoes, never in chat); the reference files; and live verification (`curl -I` for tunnels, the deployed URL for hosting, a `SELECT` for D1). Out of bounds: Playwright or Chrome DevTools driving the dashboard, browser-automating master-token creation, WebSearch-for-CF-recipes, ad-hoc `cloudflared` flag invention not in the runbook, and writing or echoing any token. When a step fails, the agent reports the exact output (secrets redacted), cites the recovery step from `references/reset-guide.md`, and stops.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// D1 over the Cloudflare HTTP API, with bound parameters.
|
|
2
|
+
//
|
|
3
|
+
// Not `wrangler d1 execute`: that spawns a process per statement, which is the
|
|
4
|
+
// CPU-burn defect Task 1805 fixed, and it takes a raw SQL string, which would
|
|
5
|
+
// mean interpolating filenames into SQL. The index push writes one row per file,
|
|
6
|
+
// so both matter here.
|
|
7
|
+
//
|
|
8
|
+
// storage-broker's cf-exec.d1Query still spawns wrangler, and the scheduling
|
|
9
|
+
// plugin has its own HTTP client. Converging the three is a follow-up; this
|
|
10
|
+
// client depends on neither, because the cloudflare plugin is where Cloudflare
|
|
11
|
+
// access belongs and importing the scheduling plugin from here would invert that.
|
|
12
|
+
|
|
13
|
+
const API = 'https://api.cloudflare.com/client/v4'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Always carries the response body into the message. A bare status renders
|
|
17
|
+
* every failure identical and unactionable.
|
|
18
|
+
* @param {unknown} parsed @param {number} status @param {string} label
|
|
19
|
+
* @returns {never}
|
|
20
|
+
*/
|
|
21
|
+
function fail(parsed, status, label) {
|
|
22
|
+
const errs = /** @type {{errors?: {message?: string}[]}} */ (parsed)?.errors
|
|
23
|
+
const detail = Array.isArray(errs) && errs.length ? errs.map((e) => e?.message).join('; ') : ''
|
|
24
|
+
throw new Error(`${label} failed: HTTP ${status}${detail ? ` — ${detail}` : ''}`)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param {{ accountId: string, token: string, dbName: string,
|
|
29
|
+
* fetchFn?: (url: string, init: object) => Promise<{ ok: boolean, status: number, json: () => Promise<unknown> }> }} opts
|
|
30
|
+
*/
|
|
31
|
+
export function createD1Client(opts) {
|
|
32
|
+
const fetchFn = opts.fetchFn ?? /** @type {never} */ (globalThis.fetch)
|
|
33
|
+
const headers = { Authorization: `Bearer ${opts.token}`, 'Content-Type': 'application/json' }
|
|
34
|
+
/** @type {string | undefined} */ let databaseId
|
|
35
|
+
let statements = 0
|
|
36
|
+
|
|
37
|
+
async function resolveId() {
|
|
38
|
+
if (databaseId !== undefined) return databaseId
|
|
39
|
+
const url = `${API}/accounts/${opts.accountId}/d1/database?name=${encodeURIComponent(opts.dbName)}`
|
|
40
|
+
const res = await fetchFn(url, { method: 'GET', headers })
|
|
41
|
+
const parsed = await res.json()
|
|
42
|
+
if (!res.ok) fail(parsed, res.status, 'd1 database lookup')
|
|
43
|
+
const list = /** @type {{result?: {name?: string, uuid?: string}[]}} */ (parsed).result ?? []
|
|
44
|
+
// Exact name match, never "the first result": `?name=` is a filter whose
|
|
45
|
+
// matching rule is not documented, so taking position on trust would let a
|
|
46
|
+
// near-name run this account's statements against the wrong database.
|
|
47
|
+
const hit = list.find((d) => d.name === opts.dbName)
|
|
48
|
+
if (!hit || typeof hit.uuid !== 'string') {
|
|
49
|
+
throw new Error(`no D1 database named "${opts.dbName}" in account ${opts.accountId}`)
|
|
50
|
+
}
|
|
51
|
+
databaseId = hit.uuid
|
|
52
|
+
return databaseId
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** @param {string} sql @param {unknown[]} [params] */
|
|
56
|
+
async function query(sql, params = []) {
|
|
57
|
+
const id = await resolveId()
|
|
58
|
+
// Counted before the await: `statements` is what reached D1, and a
|
|
59
|
+
// statement that was issued and then errored still went. Counting successes
|
|
60
|
+
// only would report 0 both for "the lookup failed so nothing was sent" and
|
|
61
|
+
// for "one statement was sent and errored".
|
|
62
|
+
statements++
|
|
63
|
+
const res = await fetchFn(`${API}/accounts/${opts.accountId}/d1/database/${id}/query`, {
|
|
64
|
+
method: 'POST',
|
|
65
|
+
headers,
|
|
66
|
+
body: JSON.stringify({ sql, params }),
|
|
67
|
+
})
|
|
68
|
+
const parsed = await res.json()
|
|
69
|
+
if (!res.ok) fail(parsed, res.status, 'd1 query')
|
|
70
|
+
const first = (/** @type {{result?: {results?: unknown}[]}} */ (parsed).result ?? [])[0]
|
|
71
|
+
return /** @type {Record<string, unknown>[]} */ (first?.results ?? [])
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
query,
|
|
76
|
+
get statements() {
|
|
77
|
+
return statements
|
|
78
|
+
},
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
//
|
|
21
21
|
// Usage:
|
|
22
22
|
// node platform/plugins/cloudflare/bin/portal-enrol.mjs --owner <ownerId> --name <name> \
|
|
23
|
-
// | npx wrangler d1 execute <db> --remote --file=-
|
|
23
|
+
// --account <accountId> | npx wrangler d1 execute <db> --remote --file=-
|
|
24
24
|
|
|
25
25
|
import {
|
|
26
26
|
PASSCODE_LENGTH,
|
|
@@ -60,6 +60,17 @@ if (!OWNER_ID.test(/** @type {string} */ (ownerId))) {
|
|
|
60
60
|
// the literal harmlessly — but it breaks the pipeable contract, so refuse it.
|
|
61
61
|
if (/[\r\n]/.test(/** @type {string} */ (name))) die('--name cannot contain a line break')
|
|
62
62
|
|
|
63
|
+
// The sub-account whose deliverable folders this person may read. Required, and
|
|
64
|
+
// validated to the same charset the account partition uses: a person enrolled
|
|
65
|
+
// with no account resolves to an EMPTY folder list, which is indistinguishable
|
|
66
|
+
// from a sub-account that simply has no deliverables. That is a failure nobody
|
|
67
|
+
// diagnoses, so it fails here instead — loudly, before the row exists.
|
|
68
|
+
const accountId = arg('--account')
|
|
69
|
+
if (!accountId) die('--account <accountId> is required')
|
|
70
|
+
if (!/^[A-Za-z0-9-]+$/.test(/** @type {string} */ (accountId))) {
|
|
71
|
+
die('--account must match ^[A-Za-z0-9-]+$ — an account id, not a path')
|
|
72
|
+
}
|
|
73
|
+
|
|
63
74
|
const saltBytes = new Uint8Array(16)
|
|
64
75
|
crypto.getRandomValues(saltBytes)
|
|
65
76
|
const saltHex = randomSaltHex(saltBytes)
|
|
@@ -84,8 +95,12 @@ const createdAt = new Date().toISOString()
|
|
|
84
95
|
//
|
|
85
96
|
// `createdAt` is deliberately absent from the DO UPDATE: it records when the
|
|
86
97
|
// person was enrolled, and rotating a passcode is not creating a person.
|
|
98
|
+
//
|
|
99
|
+
// `accountId` IS in the DO UPDATE: re-enrolling someone against a different
|
|
100
|
+
// sub-account has to move them, or they keep reading folders they no longer
|
|
101
|
+
// belong to.
|
|
87
102
|
console.log(
|
|
88
|
-
`INSERT INTO people (ownerId, name, salt, hash, createdAt) VALUES ('${ownerId}', '${sqlName}', '${saltHex}', '${hash}', '${createdAt}') ON CONFLICT(ownerId) DO UPDATE SET name=excluded.name, salt=excluded.salt, hash=excluded.hash;`,
|
|
103
|
+
`INSERT INTO people (ownerId, name, salt, hash, createdAt, accountId) VALUES ('${ownerId}', '${sqlName}', '${saltHex}', '${hash}', '${createdAt}', '${accountId}') ON CONFLICT(ownerId) DO UPDATE SET name=excluded.name, salt=excluded.salt, hash=excluded.hash, accountId=excluded.accountId;`,
|
|
89
104
|
)
|
|
90
105
|
|
|
91
106
|
// stderr: everything for the human. Never stdout — see the note at the top.
|