@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
|
@@ -3,9 +3,28 @@ export interface Stray {
|
|
|
3
3
|
kind: StrayKind;
|
|
4
4
|
detail: string;
|
|
5
5
|
}
|
|
6
|
+
/** CONCURRENCY CONTRACT — read before implementing these callbacks.
|
|
7
|
+
*
|
|
8
|
+
* reconcileStorage invokes all five through ONE Promise.all. They overlap,
|
|
9
|
+
* including the two registeredNames calls, which run at the same time as each
|
|
10
|
+
* other. So no callback may share, with any other callback or with a second
|
|
11
|
+
* invocation of itself, a resource that admits one operation at a time.
|
|
12
|
+
*
|
|
13
|
+
* The resource that has actually caused this is a Neo4j session: it rejects a
|
|
14
|
+
* query issued while one of its own is in flight. A caller that opened one
|
|
15
|
+
* session in the enclosing scope and used it in registeredNames made the
|
|
16
|
+
* second read reject, which left the standing storage audit reporting nothing
|
|
17
|
+
* for 802 consecutive ticks on maxy-code and 295 on sitedesk-code (Task 1813).
|
|
18
|
+
* Open the session inside the callback, one per invocation, and close it
|
|
19
|
+
* there; driver sessions come from a pool, so this is cheap.
|
|
20
|
+
*
|
|
21
|
+
* The contract is stated here rather than on reconcileStorage because this
|
|
22
|
+
* interface is what a caller writing an implementation is looking at, and the
|
|
23
|
+
* natural thing to write is the thing that breaks. */
|
|
6
24
|
export interface AuditDeps {
|
|
7
25
|
cfD1(): Promise<string[]>;
|
|
8
26
|
cfR2(): Promise<string[]>;
|
|
27
|
+
/** Called twice, concurrently, once per kind. See the contract above. */
|
|
9
28
|
registeredNames(kind: "d1" | "r2"): Promise<string[]>;
|
|
10
29
|
accountSecretsFiles(): Promise<Array<{
|
|
11
30
|
accountId: string;
|
|
@@ -16,6 +35,13 @@ export interface AuditDeps {
|
|
|
16
35
|
export declare function reconcileStorage(deps: AuditDeps): Promise<{
|
|
17
36
|
strays: Stray[];
|
|
18
37
|
}>;
|
|
38
|
+
/** Same concurrency contract as AuditDeps above: reconcilePages invokes both
|
|
39
|
+
* callbacks through one Promise.all, so neither may share a single-operation
|
|
40
|
+
* resource with the other.
|
|
41
|
+
*
|
|
42
|
+
* That registeredProjects happens to issue exactly one query today is not the
|
|
43
|
+
* property that makes a shared session safe here, and relying on it would put
|
|
44
|
+
* the 1813 defect one added read away. */
|
|
19
45
|
export interface PagesAuditDeps {
|
|
20
46
|
cfProjects(): Promise<string[]>;
|
|
21
47
|
registeredProjects(): Promise<string[]>;
|
|
@@ -27,4 +53,34 @@ export interface PagesReconcile {
|
|
|
27
53
|
phantoms: string[];
|
|
28
54
|
}
|
|
29
55
|
export declare function reconcilePages(deps: PagesAuditDeps): Promise<PagesReconcile>;
|
|
56
|
+
export interface DataPortalObject {
|
|
57
|
+
key: string;
|
|
58
|
+
}
|
|
59
|
+
export interface DataPortalRow {
|
|
60
|
+
objectKey: string;
|
|
61
|
+
ingested: number;
|
|
62
|
+
uploadedAt: string;
|
|
63
|
+
}
|
|
64
|
+
export interface DataPortalReconcile {
|
|
65
|
+
/** DISTINCT keys, which is not the same quantity as the length of the list
|
|
66
|
+
* the read returned. The caller logs the lengths — "how many did I see" —
|
|
67
|
+
* and these are "how many are distinct". They agree on a healthy store
|
|
68
|
+
* (manifest.objectKey is UNIQUE and R2 keys are unique by construction), and
|
|
69
|
+
* giving them one name would conflate two facts that diverge exactly when
|
|
70
|
+
* the store is unhealthy, which is the case this audit is for. */
|
|
71
|
+
distinctObjects: number;
|
|
72
|
+
distinctRows: number;
|
|
73
|
+
orphanObjects: string[];
|
|
74
|
+
phantomRows: string[];
|
|
75
|
+
/** Null when nothing is uningested, or when no uningested row carries a
|
|
76
|
+
* parseable timestamp. Rendered `na` by the caller. */
|
|
77
|
+
oldestUningestedHrs: number | null;
|
|
78
|
+
}
|
|
79
|
+
/** Pure over two lists and an injected clock, so the age arithmetic is
|
|
80
|
+
* deterministic under test rather than dependent on when the suite runs.
|
|
81
|
+
*
|
|
82
|
+
* Unlike its two siblings this takes lists rather than callbacks: both sides
|
|
83
|
+
* are read per account by a caller that must catch each independently, so
|
|
84
|
+
* there is no fan-out here to carry a concurrency contract. */
|
|
85
|
+
export declare function reconcileDataPortal(objects: DataPortalObject[], rows: DataPortalRow[], nowMs: number): DataPortalReconcile;
|
|
30
86
|
//# sourceMappingURL=audit.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audit.d.ts","sourceRoot":"","sources":["../src/audit.ts"],"names":[],"mappings":"AAMA,MAAM,MAAM,SAAS,GAAG,iBAAiB,GAAG,iBAAiB,GAAG,oBAAoB,CAAC;AAErF,MAAM,WAAW,KAAK;IACpB,IAAI,EAAE,SAAS,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;CAChB;AAMD,MAAM,WAAW,SAAS;IACxB,IAAI,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAC1B,IAAI,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAC1B,eAAe,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACtD,mBAAmB,IAAI,OAAO,CAAC,KAAK,CAAC;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC,CAAC;CAC9F;AAED,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC;IAAE,MAAM,EAAE,KAAK,EAAE,CAAA;CAAE,CAAC,CA8BpF;AAmBD,MAAM,WAAW,cAAc;IAC7B,UAAU,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAChC,kBAAkB,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;CACzC;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,wBAAsB,cAAc,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC,CAclF"}
|
|
1
|
+
{"version":3,"file":"audit.d.ts","sourceRoot":"","sources":["../src/audit.ts"],"names":[],"mappings":"AAMA,MAAM,MAAM,SAAS,GAAG,iBAAiB,GAAG,iBAAiB,GAAG,oBAAoB,CAAC;AAErF,MAAM,WAAW,KAAK;IACpB,IAAI,EAAE,SAAS,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;CAChB;AAMD;;;;;;;;;;;;;;;;;uDAiBuD;AACvD,MAAM,WAAW,SAAS;IACxB,IAAI,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAC1B,IAAI,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAC1B,yEAAyE;IACzE,eAAe,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACtD,mBAAmB,IAAI,OAAO,CAAC,KAAK,CAAC;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC,CAAC;CAC9F;AAED,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC;IAAE,MAAM,EAAE,KAAK,EAAE,CAAA;CAAE,CAAC,CA8BpF;AAmBD;;;;;;2CAM2C;AAC3C,MAAM,WAAW,cAAc;IAC7B,UAAU,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAChC,kBAAkB,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;CACzC;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,wBAAsB,cAAc,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC,CAclF;AAoBD,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,mBAAmB;IAClC;;;;;uEAKmE;IACnE,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB;4DACwD;IACxD,mBAAmB,EAAE,MAAM,GAAG,IAAI,CAAC;CACpC;AAED;;;;;gEAKgE;AAChE,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,gBAAgB,EAAE,EAC3B,IAAI,EAAE,aAAa,EAAE,EACrB,KAAK,EAAE,MAAM,GACZ,mBAAmB,CA6BrB"}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
8
|
exports.reconcileStorage = reconcileStorage;
|
|
9
9
|
exports.reconcilePages = reconcilePages;
|
|
10
|
+
exports.reconcileDataPortal = reconcileDataPortal;
|
|
10
11
|
// Keys that must never remain in a sub-account's cloudflare.env after
|
|
11
12
|
// remediation. Each is an account-wide capability.
|
|
12
13
|
const ACCOUNT_WIDE_KEYS = ["CLOUDFLARE_API_TOKEN", "CF_PAGES_D1_TOKEN", "CF_PAGES_TOKEN"];
|
|
@@ -54,4 +55,37 @@ async function reconcilePages(deps) {
|
|
|
54
55
|
phantoms: [...regSet].filter((name) => !cfSet.has(name)),
|
|
55
56
|
};
|
|
56
57
|
}
|
|
58
|
+
/** Pure over two lists and an injected clock, so the age arithmetic is
|
|
59
|
+
* deterministic under test rather than dependent on when the suite runs.
|
|
60
|
+
*
|
|
61
|
+
* Unlike its two siblings this takes lists rather than callbacks: both sides
|
|
62
|
+
* are read per account by a caller that must catch each independently, so
|
|
63
|
+
* there is no fan-out here to carry a concurrency contract. */
|
|
64
|
+
function reconcileDataPortal(objects, rows, nowMs) {
|
|
65
|
+
const objectKeys = new Set(objects.map((o) => o.key));
|
|
66
|
+
const rowKeys = new Set(rows.map((r) => r.objectKey));
|
|
67
|
+
// Date.parse of a truthy non-ISO value is NaN, which slips past a `??` guard
|
|
68
|
+
// and renders as `oldestUningestedHrs=NaN` — the defect the availability
|
|
69
|
+
// loop's snapshotAgeMs already had to fix. Unparseable rows are excluded from
|
|
70
|
+
// the minimum; if that leaves none, the answer is null, never NaN.
|
|
71
|
+
const uningestedMs = rows
|
|
72
|
+
.filter((r) => r.ingested === 0)
|
|
73
|
+
.map((r) => Date.parse(r.uploadedAt))
|
|
74
|
+
.filter((ms) => !Number.isNaN(ms));
|
|
75
|
+
// Reduced, never `Math.min(...uningestedMs)`. A spread passes one argument per
|
|
76
|
+
// element and blows the call stack — measured: fine at 125k, RangeError at
|
|
77
|
+
// 130k. One element per uningested row, and an ingestion sweep that has
|
|
78
|
+
// stalled accumulates those without bound. A stalled sweep is one of the three
|
|
79
|
+
// conditions this audit exists to reveal, so the arithmetic must not be what
|
|
80
|
+
// goes dark once the backlog it is watching grows large. The throw would also
|
|
81
|
+
// escape the caller's per-side catches and cost every later account its line.
|
|
82
|
+
const oldestMs = uningestedMs.reduce((min, ms) => (ms < min ? ms : min), Infinity);
|
|
83
|
+
return {
|
|
84
|
+
distinctObjects: objectKeys.size,
|
|
85
|
+
distinctRows: rowKeys.size,
|
|
86
|
+
orphanObjects: [...objectKeys].filter((k) => !rowKeys.has(k)),
|
|
87
|
+
phantomRows: [...rowKeys].filter((k) => !objectKeys.has(k)),
|
|
88
|
+
oldestUningestedHrs: uningestedMs.length === 0 ? null : (nowMs - oldestMs) / 3_600_000,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
57
91
|
//# sourceMappingURL=audit.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audit.js","sourceRoot":"","sources":["../src/audit.ts"],"names":[],"mappings":";AAAA,+EAA+E;AAC/E,+EAA+E;AAC/E,8EAA8E;AAC9E,gFAAgF;AAChF,0DAA0D;;
|
|
1
|
+
{"version":3,"file":"audit.js","sourceRoot":"","sources":["../src/audit.ts"],"names":[],"mappings":";AAAA,+EAA+E;AAC/E,+EAA+E;AAC/E,8EAA8E;AAC9E,gFAAgF;AAChF,0DAA0D;;AAuC1D,4CA8BC;AAsCD,wCAcC;AAoDD,kDAiCC;AArMD,sEAAsE;AACtE,mDAAmD;AACnD,MAAM,iBAAiB,GAAG,CAAC,sBAAsB,EAAE,mBAAmB,EAAE,gBAAgB,CAAC,CAAC;AA4BnF,KAAK,UAAU,gBAAgB,CAAC,IAAe;IACpD,MAAM,MAAM,GAAY,EAAE,CAAC;IAE3B,MAAM,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,YAAY,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;QACjE,IAAI,CAAC,IAAI,EAAE;QACX,IAAI,CAAC,IAAI,EAAE;QACX,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC;QAC1B,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC;QAC1B,IAAI,CAAC,mBAAmB,EAAE;KAC3B,CAAC,CAAC;IAEH,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC;IAChC,KAAK,MAAM,IAAI,IAAI,IAAI,EAAE,CAAC;QACxB,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC;YAAE,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,iBAAiB,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;IAClF,CAAC;IACD,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC;IAChC,KAAK,MAAM,IAAI,IAAI,IAAI,EAAE,CAAC;QACxB,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC;YAAE,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,iBAAiB,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;IAClF,CAAC;IAED,KAAK,MAAM,IAAI,IAAI,YAAY,EAAE,CAAC;QAChC,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;YAC7C,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,+BAA+B,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;YAC7D,IAAI,GAAG,IAAI,iBAAiB,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;gBAC3C,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,oBAAoB,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC,SAAS,IAAI,GAAG,EAAE,EAAE,CAAC,CAAC;YAClF,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,EAAE,MAAM,EAAE,CAAC;AACpB,CAAC;AAsCM,KAAK,UAAU,cAAc,CAAC,IAAoB;IACvD,4EAA4E;IAC5E,0EAA0E;IAC1E,8EAA8E;IAC9E,+CAA+C;IAC/C,MAAM,CAAC,EAAE,EAAE,UAAU,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,UAAU,EAAE,EAAE,IAAI,CAAC,kBAAkB,EAAE,CAAC,CAAC,CAAC;IAC3F,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,EAAE,CAAC,CAAC;IAC1B,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,UAAU,CAAC,CAAC;IACnC,OAAO;QACL,QAAQ,EAAE,KAAK,CAAC,IAAI;QACpB,UAAU,EAAE,MAAM,CAAC,IAAI;QACvB,OAAO,EAAE,CAAC,GAAG,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACvD,QAAQ,EAAE,CAAC,GAAG,MAAM,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;KACzD,CAAC;AACJ,CAAC;AA8CD;;;;;gEAKgE;AAChE,SAAgB,mBAAmB,CACjC,OAA2B,EAC3B,IAAqB,EACrB,KAAa;IAEb,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IACtD,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC;IAEtD,6EAA6E;IAC7E,yEAAyE;IACzE,8EAA8E;IAC9E,mEAAmE;IACnE,MAAM,YAAY,GAAG,IAAI;SACtB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,CAAC,CAAC;SAC/B,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC;SACpC,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC;IAErC,+EAA+E;IAC/E,2EAA2E;IAC3E,wEAAwE;IACxE,+EAA+E;IAC/E,6EAA6E;IAC7E,8EAA8E;IAC9E,8EAA8E;IAC9E,MAAM,QAAQ,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,QAAQ,CAAC,CAAC;IAEnF,OAAO;QACL,eAAe,EAAE,UAAU,CAAC,IAAI;QAChC,YAAY,EAAE,OAAO,CAAC,IAAI;QAC1B,aAAa,EAAE,CAAC,GAAG,UAAU,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QAC7D,WAAW,EAAE,CAAC,GAAG,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QAC3D,mBAAmB,EAAE,YAAY,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,GAAG,QAAQ,CAAC,GAAG,SAAS;KACvF,CAAC;AACJ,CAAC"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import test from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
|
-
import { reconcileStorage, reconcilePages, type AuditDeps } from "../audit.js";
|
|
3
|
+
import { reconcileStorage, reconcilePages, reconcileDataPortal, type AuditDeps } from "../audit.js";
|
|
4
4
|
|
|
5
5
|
const clean: AuditDeps = {
|
|
6
6
|
cfD1: async () => ["gls-leads"],
|
|
@@ -78,3 +78,72 @@ test("reconcilePages is clean when every live project has exactly one owner", as
|
|
|
78
78
|
assert.deepEqual(r.orphans, []);
|
|
79
79
|
assert.deepEqual(r.phantoms, []);
|
|
80
80
|
});
|
|
81
|
+
|
|
82
|
+
// --- Data portal reconcile (Task 1704) ------------------------------------
|
|
83
|
+
|
|
84
|
+
const row = (objectKey: string, ingested: number, uploadedAt: string) => ({
|
|
85
|
+
objectKey,
|
|
86
|
+
ingested,
|
|
87
|
+
uploadedAt,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("reconcileDataPortal counts an object with no row as an orphan", () => {
|
|
91
|
+
const r = reconcileDataPortal([{ key: "alice/a.pdf" }], [], 0);
|
|
92
|
+
assert.deepEqual(r.orphanObjects, ["alice/a.pdf"]);
|
|
93
|
+
assert.deepEqual(r.phantomRows, []);
|
|
94
|
+
assert.equal(r.distinctObjects, 1);
|
|
95
|
+
assert.equal(r.distinctRows, 0);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("reconcileDataPortal counts a row with no object as a phantom", () => {
|
|
99
|
+
const r = reconcileDataPortal([], [row("alice/a.pdf", 1, "2026-07-20T00:00:00Z")], 0);
|
|
100
|
+
assert.deepEqual(r.phantomRows, ["alice/a.pdf"]);
|
|
101
|
+
assert.deepEqual(r.orphanObjects, []);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("reconcileDataPortal counts a matched pair as neither", () => {
|
|
105
|
+
const r = reconcileDataPortal(
|
|
106
|
+
[{ key: "alice/a.pdf" }],
|
|
107
|
+
[row("alice/a.pdf", 1, "2026-07-20T00:00:00Z")],
|
|
108
|
+
0,
|
|
109
|
+
);
|
|
110
|
+
assert.deepEqual(r.orphanObjects, []);
|
|
111
|
+
assert.deepEqual(r.phantomRows, []);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("oldestUningestedHrs comes from the oldest ingested=0 row", () => {
|
|
115
|
+
const now = Date.parse("2026-07-20T12:00:00Z");
|
|
116
|
+
const r = reconcileDataPortal(
|
|
117
|
+
[],
|
|
118
|
+
[row("a", 0, "2026-07-20T09:00:00Z"), row("b", 0, "2026-07-20T11:00:00Z")],
|
|
119
|
+
now,
|
|
120
|
+
);
|
|
121
|
+
assert.equal(r.oldestUningestedHrs, 3);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("an ingested row is excluded from the backlog age", () => {
|
|
125
|
+
const now = Date.parse("2026-07-20T12:00:00Z");
|
|
126
|
+
const r = reconcileDataPortal(
|
|
127
|
+
[],
|
|
128
|
+
[row("a", 1, "2026-07-01T00:00:00Z"), row("b", 0, "2026-07-20T11:00:00Z")],
|
|
129
|
+
now,
|
|
130
|
+
);
|
|
131
|
+
assert.equal(r.oldestUningestedHrs, 1);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("oldestUningestedHrs is null when nothing is uningested", () => {
|
|
135
|
+
const r = reconcileDataPortal([], [row("a", 1, "2026-07-20T00:00:00Z")], 0);
|
|
136
|
+
assert.equal(r.oldestUningestedHrs, null);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("an unparseable uploadedAt never yields NaN", () => {
|
|
140
|
+
const now = Date.parse("2026-07-20T12:00:00Z");
|
|
141
|
+
const only = reconcileDataPortal([], [row("a", 0, "not-a-date")], now);
|
|
142
|
+
assert.equal(only.oldestUningestedHrs, null);
|
|
143
|
+
const mixed = reconcileDataPortal(
|
|
144
|
+
[],
|
|
145
|
+
[row("a", 0, "not-a-date"), row("b", 0, "2026-07-20T11:00:00Z")],
|
|
146
|
+
now,
|
|
147
|
+
);
|
|
148
|
+
assert.equal(mixed.oldestUningestedHrs, 1);
|
|
149
|
+
});
|
|
@@ -15,9 +15,28 @@ export interface Stray {
|
|
|
15
15
|
// remediation. Each is an account-wide capability.
|
|
16
16
|
const ACCOUNT_WIDE_KEYS = ["CLOUDFLARE_API_TOKEN", "CF_PAGES_D1_TOKEN", "CF_PAGES_TOKEN"];
|
|
17
17
|
|
|
18
|
+
/** CONCURRENCY CONTRACT — read before implementing these callbacks.
|
|
19
|
+
*
|
|
20
|
+
* reconcileStorage invokes all five through ONE Promise.all. They overlap,
|
|
21
|
+
* including the two registeredNames calls, which run at the same time as each
|
|
22
|
+
* other. So no callback may share, with any other callback or with a second
|
|
23
|
+
* invocation of itself, a resource that admits one operation at a time.
|
|
24
|
+
*
|
|
25
|
+
* The resource that has actually caused this is a Neo4j session: it rejects a
|
|
26
|
+
* query issued while one of its own is in flight. A caller that opened one
|
|
27
|
+
* session in the enclosing scope and used it in registeredNames made the
|
|
28
|
+
* second read reject, which left the standing storage audit reporting nothing
|
|
29
|
+
* for 802 consecutive ticks on maxy-code and 295 on sitedesk-code (Task 1813).
|
|
30
|
+
* Open the session inside the callback, one per invocation, and close it
|
|
31
|
+
* there; driver sessions come from a pool, so this is cheap.
|
|
32
|
+
*
|
|
33
|
+
* The contract is stated here rather than on reconcileStorage because this
|
|
34
|
+
* interface is what a caller writing an implementation is looking at, and the
|
|
35
|
+
* natural thing to write is the thing that breaks. */
|
|
18
36
|
export interface AuditDeps {
|
|
19
37
|
cfD1(): Promise<string[]>;
|
|
20
38
|
cfR2(): Promise<string[]>;
|
|
39
|
+
/** Called twice, concurrently, once per kind. See the contract above. */
|
|
21
40
|
registeredNames(kind: "d1" | "r2"): Promise<string[]>;
|
|
22
41
|
accountSecretsFiles(): Promise<Array<{ accountId: string; path: string; contents: string }>>;
|
|
23
42
|
}
|
|
@@ -71,6 +90,13 @@ export async function reconcileStorage(deps: AuditDeps): Promise<{ strays: Stray
|
|
|
71
90
|
// Names are reported, not just counted: a count tells an operator something is
|
|
72
91
|
// wrong, the name tells them what to adopt.
|
|
73
92
|
|
|
93
|
+
/** Same concurrency contract as AuditDeps above: reconcilePages invokes both
|
|
94
|
+
* callbacks through one Promise.all, so neither may share a single-operation
|
|
95
|
+
* resource with the other.
|
|
96
|
+
*
|
|
97
|
+
* That registeredProjects happens to issue exactly one query today is not the
|
|
98
|
+
* property that makes a shared session safe here, and relying on it would put
|
|
99
|
+
* the 1813 defect one added read away. */
|
|
74
100
|
export interface PagesAuditDeps {
|
|
75
101
|
cfProjects(): Promise<string[]>;
|
|
76
102
|
registeredProjects(): Promise<string[]>;
|
|
@@ -98,3 +124,88 @@ export async function reconcilePages(deps: PagesAuditDeps): Promise<PagesReconci
|
|
|
98
124
|
phantoms: [...regSet].filter((name) => !cfSet.has(name)),
|
|
99
125
|
};
|
|
100
126
|
}
|
|
127
|
+
|
|
128
|
+
// --- Data portal reconcile (Task 1704) -------------------------------------
|
|
129
|
+
//
|
|
130
|
+
// Third instance of the same no-event argument, one level down from the two
|
|
131
|
+
// above: those reconcile ownership records, this reconciles the objects
|
|
132
|
+
// themselves against the manifest rows that make them ingestible.
|
|
133
|
+
//
|
|
134
|
+
// orphan object — an object in R2 with no manifest row. The upload returned
|
|
135
|
+
// 200, the file is stored, and ingestion will never see it.
|
|
136
|
+
// phantom row — a manifest row whose object is absent. Ingestion will pull
|
|
137
|
+
// a missing file, and nothing says so until it runs.
|
|
138
|
+
//
|
|
139
|
+
// Keys are returned rather than only counted so the arithmetic is testable and
|
|
140
|
+
// a future caller has them, but the standing audit logs COUNTS ONLY: a key here
|
|
141
|
+
// is `<ownerId>/<filename>`, a real person's filename, and the brand journal is
|
|
142
|
+
// not where those belong. That is the same ownership discipline the in-process
|
|
143
|
+
// audit path owes for holding the account-wide credential and bypassing
|
|
144
|
+
// objectGate entirely.
|
|
145
|
+
|
|
146
|
+
export interface DataPortalObject {
|
|
147
|
+
key: string;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export interface DataPortalRow {
|
|
151
|
+
objectKey: string;
|
|
152
|
+
ingested: number;
|
|
153
|
+
uploadedAt: string;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export interface DataPortalReconcile {
|
|
157
|
+
/** DISTINCT keys, which is not the same quantity as the length of the list
|
|
158
|
+
* the read returned. The caller logs the lengths — "how many did I see" —
|
|
159
|
+
* and these are "how many are distinct". They agree on a healthy store
|
|
160
|
+
* (manifest.objectKey is UNIQUE and R2 keys are unique by construction), and
|
|
161
|
+
* giving them one name would conflate two facts that diverge exactly when
|
|
162
|
+
* the store is unhealthy, which is the case this audit is for. */
|
|
163
|
+
distinctObjects: number;
|
|
164
|
+
distinctRows: number;
|
|
165
|
+
orphanObjects: string[];
|
|
166
|
+
phantomRows: string[];
|
|
167
|
+
/** Null when nothing is uningested, or when no uningested row carries a
|
|
168
|
+
* parseable timestamp. Rendered `na` by the caller. */
|
|
169
|
+
oldestUningestedHrs: number | null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Pure over two lists and an injected clock, so the age arithmetic is
|
|
173
|
+
* deterministic under test rather than dependent on when the suite runs.
|
|
174
|
+
*
|
|
175
|
+
* Unlike its two siblings this takes lists rather than callbacks: both sides
|
|
176
|
+
* are read per account by a caller that must catch each independently, so
|
|
177
|
+
* there is no fan-out here to carry a concurrency contract. */
|
|
178
|
+
export function reconcileDataPortal(
|
|
179
|
+
objects: DataPortalObject[],
|
|
180
|
+
rows: DataPortalRow[],
|
|
181
|
+
nowMs: number,
|
|
182
|
+
): DataPortalReconcile {
|
|
183
|
+
const objectKeys = new Set(objects.map((o) => o.key));
|
|
184
|
+
const rowKeys = new Set(rows.map((r) => r.objectKey));
|
|
185
|
+
|
|
186
|
+
// Date.parse of a truthy non-ISO value is NaN, which slips past a `??` guard
|
|
187
|
+
// and renders as `oldestUningestedHrs=NaN` — the defect the availability
|
|
188
|
+
// loop's snapshotAgeMs already had to fix. Unparseable rows are excluded from
|
|
189
|
+
// the minimum; if that leaves none, the answer is null, never NaN.
|
|
190
|
+
const uningestedMs = rows
|
|
191
|
+
.filter((r) => r.ingested === 0)
|
|
192
|
+
.map((r) => Date.parse(r.uploadedAt))
|
|
193
|
+
.filter((ms) => !Number.isNaN(ms));
|
|
194
|
+
|
|
195
|
+
// Reduced, never `Math.min(...uningestedMs)`. A spread passes one argument per
|
|
196
|
+
// element and blows the call stack — measured: fine at 125k, RangeError at
|
|
197
|
+
// 130k. One element per uningested row, and an ingestion sweep that has
|
|
198
|
+
// stalled accumulates those without bound. A stalled sweep is one of the three
|
|
199
|
+
// conditions this audit exists to reveal, so the arithmetic must not be what
|
|
200
|
+
// goes dark once the backlog it is watching grows large. The throw would also
|
|
201
|
+
// escape the caller's per-side catches and cost every later account its line.
|
|
202
|
+
const oldestMs = uningestedMs.reduce((min, ms) => (ms < min ? ms : min), Infinity);
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
distinctObjects: objectKeys.size,
|
|
206
|
+
distinctRows: rowKeys.size,
|
|
207
|
+
orphanObjects: [...objectKeys].filter((k) => !rowKeys.has(k)),
|
|
208
|
+
phantomRows: [...rowKeys].filter((k) => !objectKeys.has(k)),
|
|
209
|
+
oldestUningestedHrs: uningestedMs.length === 0 ? null : (nowMs - oldestMs) / 3_600_000,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "admin",
|
|
3
|
-
"description": "Platform administration plugin. Provides system-status, public-hostname (deterministic Cloudflare public-URL resolver, single call returning the operator's canonical hostname so agents never guess property names on :CloudflareHostname nodes), admin-hostname (the admin-host twin of public-hostname — resolves the host that serves /calendar and the rest of the admin shell behind sign-in from the same ingress + alias + operator files, so agents point the operator at the admin /calendar URL instead of the public host that 404s it), publish-site (move an extracted static-site tree under <accountDir>/sites/<slug>/, emit one canonical path slug, and refresh llms.txt at the site root — typed refusal taxonomy replaces the prior skill-prose contract), brand-settings, account-manage, capabilities-here (deterministic install-introspection — returns enabledPlugins, tier, models, installed platform + premium specialists, registered Claude Code agent types, and the brand block as a single JSON object; the canonical tool for grounding any claim about what is enabled on the current account), account-update, the multi-account managed-service lifecycle tools (account_list, account_create, account_delete, account_purge, account_channel_routing, account_channel_routing_set — one Rubytech operator hosting N client accounts on one house install; create/delete/purge are operator-only and purge is two-step with a typed confirmation; account_create requires the client business owner's real name (ownerName), refusing a blank or the placeholder \"Owner\" and seeding it onto the sub-account's owner AdminUser and owner Person, and requires the owner's IANA timezone (timezone) — refusing a blank or a non-IANA value — seeding it onto the owner UserProfile marked owner-set so scheduling reads the owner's real zone from the start rather than the box's server default, while a managing house admin (its userId) is optional — when supplied it is validated as a live house admin and recorded as the sub-account's managingAdminUserId, and when omitted the sub-account is unmanaged and its passive-intake tasks route to the install's primary admin; account_reassign_admin repoints a client sub-account's managing admin to another live house admin (the repair path for a dangling managingAdminUserId, or to record one on an unmanaged account); account_roster is the canonical per-account identity view — one row per sub-account giving business name, role, the managing house admin resolved to a name with status (assigned/absent/dangling), the business owner from the graph with status (named/nameless), and the WhatsApp account-manager binding if any, each field carrying what it means and does not (managing admin is the responsible house team member not the owner; account manager is WhatsApp routing only with no admin rights), so the agent never stitches admin-list + account.json + whatsapp-config; account_set_owner_name names (or corrects) a client sub-account's business owner by writing the owned Person — the canonical name source — refusing a blank or the placeholder \"Owner\", the guarded repair for a nameless owner (the standing identity-audit op=nameless-owned-person detector); account_channel_routing_set repoints the install's single WhatsApp/Telegram inbound stream to a different sub-account), admin-add (refuses on a client sub-account, where admin access is already install-wide so a per-operator seat grants nothing — the sub-account's one identity, its business owner, is set at account creation; the house account, where operators hold PIN logins and admins[] rows, is unchanged), admin-remove (refuses to remove a house admin who still manages one or more client sub-accounts, naming them so the operator reassigns each with account_reassign_admin first — never silently orphans them), admin-list, admin-update-pin, agent-list, agent-config-read, logs-read, plugin-read, skill-load (one-call resolve+read for SKILL.md by skill name, the canonical primitive for loading a named skill; plugin-read remains the reader for references/* and PLUGIN.md), skill-search (free-text intent → skill resolver: ranks every plugin skill's SKILL.md name+description against a query and returns the top owners with their canonical skills/<slug>/ path, so intent→skill mapping survives the available-skills menu leaving context after a compaction), store-skill (deterministic write counterpart to plugin-read; persists operator-authored skills as plugin files under the active account), session-reset, session-resume, wifi, and action-approval tools (action-pending, action-approve, action-reject, action-edit) for managing the Maxy platform.",
|
|
3
|
+
"description": "Platform administration plugin. Provides system-status, public-hostname (deterministic Cloudflare public-URL resolver, single call returning the operator's canonical hostname so agents never guess property names on :CloudflareHostname nodes), admin-hostname (the admin-host twin of public-hostname — resolves the host that serves /calendar and the rest of the admin shell behind sign-in from the same ingress + alias + operator files, so agents point the operator at the admin /calendar URL instead of the public host that 404s it), publish-site (move an extracted static-site tree under <accountDir>/sites/<slug>/, emit one canonical path slug, and refresh llms.txt at the site root — typed refusal taxonomy replaces the prior skill-prose contract), brand-settings, account-manage, capabilities-here (deterministic install-introspection — returns enabledPlugins, tier, models, installed platform + premium specialists, registered Claude Code agent types, and the brand block as a single JSON object; the canonical tool for grounding any claim about what is enabled on the current account), account-update, the multi-account managed-service lifecycle tools (account_list, account_create, account_delete, account_purge, account_channel_routing, account_channel_routing_set — one Rubytech operator hosting N client accounts on one house install; create/delete/purge are operator-only and purge is two-step with a typed confirmation; account_create requires the client business owner's real name (ownerName), refusing a blank or the placeholder \"Owner\" and seeding it onto the sub-account's owner AdminUser and owner Person, and requires the owner's IANA timezone (timezone) — refusing a blank or a non-IANA value — seeding it onto the owner UserProfile marked owner-set so scheduling reads the owner's real zone from the start rather than the box's server default, while a managing house admin (its userId) is optional — when supplied it is validated as a live house admin and recorded as the sub-account's managingAdminUserId, and when omitted the sub-account is unmanaged and its passive-intake tasks route to the install's primary admin; account_reassign_admin repoints a client sub-account's managing admin to another live house admin (the repair path for a dangling managingAdminUserId, or to record one on an unmanaged account); account_roster is the canonical per-account identity view — one row per sub-account giving business name, role, the managing house admin resolved to a name with status (assigned/absent/dangling), the business owner from the graph with status (named/nameless), and the WhatsApp account-manager binding if any, each field carrying what it means and does not (managing admin is the responsible house team member not the owner; account manager is WhatsApp routing only with no admin rights), so the agent never stitches admin-list + account.json + whatsapp-config; account_set_owner_name names (or corrects) a client sub-account's business owner by writing the owned Person — the canonical name source — refusing a blank or the placeholder \"Owner\", the guarded repair for a nameless owner (the standing identity-audit op=nameless-owned-person detector); account_channel_routing_set repoints the install's single WhatsApp/Telegram inbound stream to a different sub-account), admin-add (refuses on a client sub-account, where admin access is already install-wide so a per-operator seat grants nothing — the sub-account's one identity, its business owner, is set at account creation; the house account, where operators hold PIN logins and admins[] rows, is unchanged), admin-remove (refuses to remove a house admin who still manages one or more client sub-accounts, naming them so the operator reassigns each with account_reassign_admin first — never silently orphans them), admin-list, admin-update-pin, agent-list, agent-config-read, logs-read, cpu-triage-start and cpu-triage-read (the two-call device CPU diagnostic — start spawns a detached sampling run across at least one full 120s reconcile cycle and returns a run handle immediately, read returns running/complete/failed with the per-core distribution; no admin tool blocks through a sampling window, and no spot reading is obtainable), plugin-read, skill-load (one-call resolve+read for SKILL.md by skill name, the canonical primitive for loading a named skill; plugin-read remains the reader for references/* and PLUGIN.md), skill-search (free-text intent → skill resolver: ranks every plugin skill's SKILL.md name+description against a query and returns the top owners with their canonical skills/<slug>/ path, so intent→skill mapping survives the available-skills menu leaving context after a compaction), store-skill (deterministic write counterpart to plugin-read; persists operator-authored skills as plugin files under the active account), session-reset, session-resume, wifi, and action-approval tools (action-pending, action-approve, action-reject, action-edit) for managing the Maxy platform.",
|
|
4
4
|
"version": "0.1.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Rubytech LLC"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: admin
|
|
3
3
|
surface: platform
|
|
4
|
-
description: "Platform administration plugin. Provides system-status, public-hostname (deterministic Cloudflare public-URL resolver, single call returning the operator's canonical hostname so agents never guess property names on :CloudflareHostname nodes), admin-hostname (the admin-host twin of public-hostname — resolves the host that serves /calendar and the rest of the admin shell behind sign-in from the same ingress + alias + operator files, so agents point the operator at the admin /calendar URL instead of the public host that 404s it), publish-site (move an extracted static-site tree under <accountDir>/sites/<slug>/, emit one canonical path slug, and refresh llms.txt at the site root — typed refusal taxonomy replaces the prior skill-prose contract), brand-settings, account-manage, capabilities-here (deterministic install-introspection — returns enabledPlugins, tier, models, installed platform + premium specialists, registered Claude Code agent types, and the brand block as a single JSON object; the canonical tool for grounding any claim about what is enabled on the current account), account-update, the multi-account managed-service lifecycle tools (account_list, account_create, account_delete, account_purge, account_channel_routing, account_channel_routing_set — one Rubytech operator hosting N client accounts on one house install; create/delete/purge are operator-only and purge is two-step with a typed confirmation; account_create requires the client business owner's real name (ownerName), refusing a blank or the placeholder "Owner" and seeding it onto the sub-account's owner AdminUser and owner Person, and requires the owner's IANA timezone (timezone) — refusing a blank or a non-IANA value — seeding it onto the owner UserProfile marked owner-set so scheduling reads the owner's real zone from the start rather than the box's server default, while a managing house admin (its userId) is optional — when supplied it is validated as a live house admin and recorded as the sub-account's managingAdminUserId, and when omitted the sub-account is unmanaged and its passive-intake tasks route to the install's primary admin; account_reassign_admin repoints a client sub-account's managing admin to another live house admin (the repair path for a dangling managingAdminUserId, or to record one on an unmanaged account); account_roster is the canonical per-account identity view — one row per sub-account giving business name, role, the managing house admin resolved to a name with status (assigned/absent/dangling), the business owner from the graph with status (named/nameless), and the WhatsApp account-manager binding if any, each field carrying what it means and does not (managing admin is the responsible house team member not the owner; account manager is WhatsApp routing only with no admin rights), so the agent never stitches admin-list + account.json + whatsapp-config; account_set_owner_name names (or corrects) a client sub-account's business owner by writing the owned Person — the canonical name source — refusing a blank or the placeholder "Owner", the guarded repair for a nameless owner (the standing identity-audit op=nameless-owned-person detector); account_channel_routing_set repoints the install's single WhatsApp/Telegram inbound stream to a different sub-account), admin-add (refuses on a client sub-account, where admin access is already install-wide so a per-operator seat grants nothing — the sub-account's one identity, its business owner, is set at account creation; the house account, where operators hold PIN logins and admins[] rows, is unchanged), admin-remove (refuses to remove a house admin who still manages one or more client sub-accounts, naming them so the operator reassigns each with account_reassign_admin first — never silently orphans them), admin-list, admin-update-pin, agent-list, agent-config-read, logs-read, plugin-read, skill-load (one-call resolve+read for SKILL.md by skill name, the canonical primitive for loading a named skill; plugin-read remains the reader for references/* and PLUGIN.md), skill-search (free-text intent → skill resolver: ranks every plugin skill's SKILL.md name+description against a query and returns the top owners with their canonical skills/<slug>/ path, so intent→skill mapping survives the available-skills menu leaving context after a compaction), store-skill (deterministic write counterpart to plugin-read; persists operator-authored skills as plugin files under the active account), session-reset, session-resume, wifi, and action-approval tools (action-pending, action-approve, action-reject, action-edit) for managing the Maxy platform."
|
|
4
|
+
description: "Platform administration plugin. Provides system-status, public-hostname (deterministic Cloudflare public-URL resolver, single call returning the operator's canonical hostname so agents never guess property names on :CloudflareHostname nodes), admin-hostname (the admin-host twin of public-hostname — resolves the host that serves /calendar and the rest of the admin shell behind sign-in from the same ingress + alias + operator files, so agents point the operator at the admin /calendar URL instead of the public host that 404s it), publish-site (move an extracted static-site tree under <accountDir>/sites/<slug>/, emit one canonical path slug, and refresh llms.txt at the site root — typed refusal taxonomy replaces the prior skill-prose contract), brand-settings, account-manage, capabilities-here (deterministic install-introspection — returns enabledPlugins, tier, models, installed platform + premium specialists, registered Claude Code agent types, and the brand block as a single JSON object; the canonical tool for grounding any claim about what is enabled on the current account), account-update, the multi-account managed-service lifecycle tools (account_list, account_create, account_delete, account_purge, account_channel_routing, account_channel_routing_set — one Rubytech operator hosting N client accounts on one house install; create/delete/purge are operator-only and purge is two-step with a typed confirmation; account_create requires the client business owner's real name (ownerName), refusing a blank or the placeholder "Owner" and seeding it onto the sub-account's owner AdminUser and owner Person, and requires the owner's IANA timezone (timezone) — refusing a blank or a non-IANA value — seeding it onto the owner UserProfile marked owner-set so scheduling reads the owner's real zone from the start rather than the box's server default, while a managing house admin (its userId) is optional — when supplied it is validated as a live house admin and recorded as the sub-account's managingAdminUserId, and when omitted the sub-account is unmanaged and its passive-intake tasks route to the install's primary admin; account_reassign_admin repoints a client sub-account's managing admin to another live house admin (the repair path for a dangling managingAdminUserId, or to record one on an unmanaged account); account_roster is the canonical per-account identity view — one row per sub-account giving business name, role, the managing house admin resolved to a name with status (assigned/absent/dangling), the business owner from the graph with status (named/nameless), and the WhatsApp account-manager binding if any, each field carrying what it means and does not (managing admin is the responsible house team member not the owner; account manager is WhatsApp routing only with no admin rights), so the agent never stitches admin-list + account.json + whatsapp-config; account_set_owner_name names (or corrects) a client sub-account's business owner by writing the owned Person — the canonical name source — refusing a blank or the placeholder "Owner", the guarded repair for a nameless owner (the standing identity-audit op=nameless-owned-person detector); account_channel_routing_set repoints the install's single WhatsApp/Telegram inbound stream to a different sub-account), admin-add (refuses on a client sub-account, where admin access is already install-wide so a per-operator seat grants nothing — the sub-account's one identity, its business owner, is set at account creation; the house account, where operators hold PIN logins and admins[] rows, is unchanged), admin-remove (refuses to remove a house admin who still manages one or more client sub-accounts, naming them so the operator reassigns each with account_reassign_admin first — never silently orphans them), admin-list, admin-update-pin, agent-list, agent-config-read, logs-read, cpu-triage-start and cpu-triage-read (the two-call device CPU diagnostic — start spawns a detached sampling run across at least one full 120s reconcile cycle and returns a run handle immediately, read returns running/complete/failed with the per-core distribution; no admin tool blocks through a sampling window, and no spot reading is obtainable), plugin-read, skill-load (one-call resolve+read for SKILL.md by skill name, the canonical primitive for loading a named skill; plugin-read remains the reader for references/* and PLUGIN.md), skill-search (free-text intent → skill resolver: ranks every plugin skill's SKILL.md name+description against a query and returns the top owners with their canonical skills/<slug>/ path, so intent→skill mapping survives the available-skills menu leaving context after a compaction), store-skill (deterministic write counterpart to plugin-read; persists operator-authored skills as plugin files under the active account), session-reset, session-resume, wifi, and action-approval tools (action-pending, action-approve, action-reject, action-edit) for managing the Maxy platform."
|
|
5
5
|
tools:
|
|
6
6
|
- name: system-status
|
|
7
7
|
publicAllowlist: false
|
|
@@ -75,6 +75,12 @@ tools:
|
|
|
75
75
|
- name: logs-read
|
|
76
76
|
publicAllowlist: false
|
|
77
77
|
adminAllowlist: false
|
|
78
|
+
- name: cpu-triage-start
|
|
79
|
+
publicAllowlist: false
|
|
80
|
+
adminAllowlist: true
|
|
81
|
+
- name: cpu-triage-read
|
|
82
|
+
publicAllowlist: false
|
|
83
|
+
adminAllowlist: true
|
|
78
84
|
- name: plugin-read
|
|
79
85
|
publicAllowlist: false
|
|
80
86
|
adminAllowlist: false
|
|
@@ -24,6 +24,7 @@ import { resolvePublicHostname } from "./lib/public-hostname.js";
|
|
|
24
24
|
import { resolveAdminHostname } from "./lib/admin-hostname.js";
|
|
25
25
|
import { publishSite } from "./tools/publish-site.js";
|
|
26
26
|
import { resolveCapabilitiesHere } from "./tools/capabilities-here.js";
|
|
27
|
+
import { startCpuTriage, readCpuTriage } from "./tools/cpu-triage.js";
|
|
27
28
|
import { runCatalogueCensus } from "./lib/catalogue-census.js";
|
|
28
29
|
import { lifelineTool } from "../../../../lib/mcp-lifeline/dist/index.js";
|
|
29
30
|
import { listAccountsFromDir, isHouseAccount, archiveAccountDir, provisionClientAccount, findHouseAccount, getChannelRoutingAccountId, setChannelRoutingAccountId, seedAccountGraphRoot, isPlaceholderOwnerName, isBlankBusinessName, isValidTimeZone, refuseClientAdminAdd, readHouseAdminUserIds, setManagingAdmin, findClientAccountsManagedBy, reassignManagingAdmin, ACCOUNT_NODE_COUNT_CYPHER, ACCOUNT_PURGE_CYPHER, classifyManagingAdmin, ownerNameStatus, readManagingAdminUserId, setOwnerName, } from "./tools/account-lifecycle.js";
|
|
@@ -1736,6 +1737,77 @@ eagerTool(server, "logs-read", "Read recent logs. Stream logs (type=agent-stream
|
|
|
1736
1737
|
return { content: [{ type: "text", text: `Failed to read logs: ${err instanceof Error ? err.message : String(err)}` }] };
|
|
1737
1738
|
}
|
|
1738
1739
|
});
|
|
1740
|
+
// ---------------------------------------------------------------------------
|
|
1741
|
+
// cpu-triage — device CPU diagnostic, split across two calls.
|
|
1742
|
+
//
|
|
1743
|
+
// The measurement is only trustworthy across at least one full 120s reconcile
|
|
1744
|
+
// cycle: the same process measured 0.2% in three consecutive 25s windows and
|
|
1745
|
+
// 10.8% in a 20s window, all correct, because the load is a sawtooth. Every
|
|
1746
|
+
// other admin tool completes inside 12s, so the run is detached and the caller
|
|
1747
|
+
// polls rather than blocking through a sampling window.
|
|
1748
|
+
//
|
|
1749
|
+
// What comes back is a DISTRIBUTION — per-core mean, peak, and share of the
|
|
1750
|
+
// window above threshold. There is deliberately no way to obtain a single
|
|
1751
|
+
// spot reading from these tools; that shape produced a session of
|
|
1752
|
+
// contradictory answers on 2026-07-20.
|
|
1753
|
+
// ---------------------------------------------------------------------------
|
|
1754
|
+
const CPU_TRIAGE_PATHS = {
|
|
1755
|
+
scriptPath: resolve(PLATFORM_ROOT, "scripts", "cpu-triage.sh"),
|
|
1756
|
+
runnerPath: resolve(PLATFORM_ROOT, "scripts", "cpu-triage-run.sh"),
|
|
1757
|
+
logsDir: resolve(CONFIG_DIR, "logs"),
|
|
1758
|
+
serverLog: resolve(CONFIG_DIR, "logs", "server.log"),
|
|
1759
|
+
};
|
|
1760
|
+
eagerTool(server, "cpu-triage-start", "Start a device CPU triage run and return immediately with a run handle. THIS DOES NOT RETURN A MEASUREMENT — " +
|
|
1761
|
+
"call cpu-triage-read after `expectedCompleteAt` to get the report. " +
|
|
1762
|
+
"Answers 'which cores are busy and what is on them' for the box this install runs on. " +
|
|
1763
|
+
"Samples across at least one full 120s reconcile cycle and reports a distribution (per-core mean, peak, " +
|
|
1764
|
+
"share of window above threshold, plus per-process and per-service deltas), never a spot reading — " +
|
|
1765
|
+
"shorter samples on this platform contradict each other. " +
|
|
1766
|
+
"`control` names a peer brand known to be on the PREVIOUS build: the single most valuable input to this " +
|
|
1767
|
+
"measurement, because a peer that did not get the change beats any before/after inference. " +
|
|
1768
|
+
"Measures the local box only; there is no remote targeting.", {
|
|
1769
|
+
windowSec: z.number().int().min(120).optional().describe("Total sampling window in seconds (default 300, minimum 120 — one reconcile cycle)"),
|
|
1770
|
+
intervalSec: z.number().int().min(1).optional().describe("Sample cadence in seconds (default 10)"),
|
|
1771
|
+
thresholdPct: z.number().int().min(1).max(100).optional().describe("Per-core busy %% treated as hot (default 15)"),
|
|
1772
|
+
control: z.string().optional().describe("A peer brand on the previous build, for like-for-like comparison"),
|
|
1773
|
+
}, async ({ windowSec, intervalSec, thresholdPct, control }) => {
|
|
1774
|
+
const out = startCpuTriage(CPU_TRIAGE_PATHS, { windowSec, intervalSec, thresholdPct, control });
|
|
1775
|
+
if (!out.ok) {
|
|
1776
|
+
return {
|
|
1777
|
+
content: [{ type: "text", text: JSON.stringify({ refused: out.reason, detail: out.detail }, null, 2) }],
|
|
1778
|
+
isError: out.fault,
|
|
1779
|
+
};
|
|
1780
|
+
}
|
|
1781
|
+
return {
|
|
1782
|
+
content: [{
|
|
1783
|
+
type: "text",
|
|
1784
|
+
text: JSON.stringify({
|
|
1785
|
+
runId: out.runId,
|
|
1786
|
+
startedAt: out.startedAt,
|
|
1787
|
+
expectedCompleteAt: out.expectedCompleteAt,
|
|
1788
|
+
windowSec: out.windowSec,
|
|
1789
|
+
resultPath: out.resultPath,
|
|
1790
|
+
next: `Sampling for ${out.windowSec}s. Call cpu-triage-read after ${out.expectedCompleteAt}. Reading earlier returns status=running, which is not a failure.`,
|
|
1791
|
+
}, null, 2),
|
|
1792
|
+
}],
|
|
1793
|
+
};
|
|
1794
|
+
});
|
|
1795
|
+
eagerTool(server, "cpu-triage-read", "Read a cpu-triage run started by cpu-triage-start. Defaults to the most recent run, so a later session can " +
|
|
1796
|
+
"read a run it did not start. Returns one of three states: `running` (still sampling — NOT a failure, and NOT " +
|
|
1797
|
+
"a reason to start another run), `complete` (the parsed report; exitCode 1 means one or more cores were " +
|
|
1798
|
+
"sustained above threshold and the finding is in the report, exitCode 0 means clean), or `failed` (the run hit " +
|
|
1799
|
+
"a precondition failure, with its exit code and stderr).", {
|
|
1800
|
+
runId: z.string().optional().describe("Run to read (default: most recent)"),
|
|
1801
|
+
}, async ({ runId }) => {
|
|
1802
|
+
const out = readCpuTriage(CPU_TRIAGE_PATHS, { runId });
|
|
1803
|
+
if (!out.ok) {
|
|
1804
|
+
return {
|
|
1805
|
+
content: [{ type: "text", text: JSON.stringify({ refused: out.reason, detail: out.detail }, null, 2) }],
|
|
1806
|
+
isError: out.fault,
|
|
1807
|
+
};
|
|
1808
|
+
}
|
|
1809
|
+
return { content: [{ type: "text", text: JSON.stringify(out, null, 2) }] };
|
|
1810
|
+
});
|
|
1739
1811
|
// operator-authored skills are written to disk as a plugin so the
|
|
1740
1812
|
// existing plugin-manifest loader picks them up (parsePluginFrontmatter +
|
|
1741
1813
|
// assemblePublicPluginContent + loadEmbeddedPlugins all read from
|