@rubytech/create-maxy-code 0.1.461 → 0.1.463

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.
Files changed (186) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/lib/graph-mcp/dist/index.d.ts +2 -1
  3. package/payload/platform/lib/graph-mcp/dist/index.d.ts.map +1 -1
  4. package/payload/platform/lib/graph-mcp/dist/index.js +14 -22
  5. package/payload/platform/lib/graph-mcp/dist/index.js.map +1 -1
  6. package/payload/platform/lib/graph-mcp/src/index.ts +15 -22
  7. package/payload/platform/lib/mcp-spawn-tee/dist/index.d.ts +8 -7
  8. package/payload/platform/lib/mcp-spawn-tee/dist/index.d.ts.map +1 -1
  9. package/payload/platform/lib/mcp-spawn-tee/dist/index.js +19 -19
  10. package/payload/platform/lib/mcp-spawn-tee/dist/index.js.map +1 -1
  11. package/payload/platform/lib/mcp-spawn-tee/src/__tests__/spawn-tee.test.ts +34 -5
  12. package/payload/platform/lib/mcp-spawn-tee/src/index.ts +22 -19
  13. package/payload/platform/lib/mcp-stderr-tee/dist/index.d.ts +35 -17
  14. package/payload/platform/lib/mcp-stderr-tee/dist/index.d.ts.map +1 -1
  15. package/payload/platform/lib/mcp-stderr-tee/dist/index.js +41 -48
  16. package/payload/platform/lib/mcp-stderr-tee/dist/index.js.map +1 -1
  17. package/payload/platform/lib/mcp-stderr-tee/src/index.ts +42 -51
  18. package/payload/platform/lib/storage-broker/dist/__tests__/audit.test.js +27 -0
  19. package/payload/platform/lib/storage-broker/dist/__tests__/audit.test.js.map +1 -1
  20. package/payload/platform/lib/storage-broker/dist/__tests__/cf-exec.test.js +49 -0
  21. package/payload/platform/lib/storage-broker/dist/__tests__/cf-exec.test.js.map +1 -1
  22. package/payload/platform/lib/storage-broker/dist/audit.d.ts +11 -0
  23. package/payload/platform/lib/storage-broker/dist/audit.d.ts.map +1 -1
  24. package/payload/platform/lib/storage-broker/dist/audit.js +16 -0
  25. package/payload/platform/lib/storage-broker/dist/audit.js.map +1 -1
  26. package/payload/platform/lib/storage-broker/dist/cf-exec.d.ts +18 -0
  27. package/payload/platform/lib/storage-broker/dist/cf-exec.d.ts.map +1 -1
  28. package/payload/platform/lib/storage-broker/dist/cf-exec.js +77 -0
  29. package/payload/platform/lib/storage-broker/dist/cf-exec.js.map +1 -1
  30. package/payload/platform/lib/storage-broker/dist/house-scoped-token.js +3 -0
  31. package/payload/platform/lib/storage-broker/dist/house-scoped-token.js.map +1 -1
  32. package/payload/platform/lib/storage-broker/dist/registry.d.ts +1 -1
  33. package/payload/platform/lib/storage-broker/dist/registry.d.ts.map +1 -1
  34. package/payload/platform/lib/storage-broker/dist/registry.js.map +1 -1
  35. package/payload/platform/lib/storage-broker/src/__tests__/audit.test.ts +32 -1
  36. package/payload/platform/lib/storage-broker/src/__tests__/cf-exec.test.ts +61 -0
  37. package/payload/platform/lib/storage-broker/src/audit.ts +45 -0
  38. package/payload/platform/lib/storage-broker/src/cf-exec.ts +91 -0
  39. package/payload/platform/lib/storage-broker/src/house-scoped-token.ts +3 -0
  40. package/payload/platform/lib/storage-broker/src/registry.ts +5 -1
  41. package/payload/platform/plugins/admin/lib/mcp-spawn-tee/index.js +19 -19
  42. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +23 -12
  43. package/payload/platform/plugins/aeo/lib/mcp-spawn-tee/index.js +19 -19
  44. package/payload/platform/plugins/browser/lib/mcp-spawn-tee/index.js +19 -19
  45. package/payload/platform/plugins/cloudflare/PLUGIN.md +1 -1
  46. package/payload/platform/plugins/cloudflare/mcp/dist/index.js +5 -5
  47. package/payload/platform/plugins/cloudflare/mcp/dist/index.js.map +1 -1
  48. package/payload/platform/plugins/cloudflare/references/hosting-sites.md +2 -0
  49. package/payload/platform/plugins/cloudflare/skills/calendar-site/SKILL.md +1 -1
  50. package/payload/platform/plugins/cloudflare/skills/site-deploy/SKILL.md +2 -0
  51. package/payload/platform/plugins/connector/lib/mcp-spawn-tee/index.js +19 -19
  52. package/payload/platform/plugins/contacts/lib/mcp-spawn-tee/index.js +19 -19
  53. package/payload/platform/plugins/docs/references/admin-session.md +3 -3
  54. package/payload/platform/plugins/docs/references/cloudflare.md +2 -0
  55. package/payload/platform/plugins/docs/references/internals.md +1 -1
  56. package/payload/platform/plugins/docs/references/outlook-guide.md +11 -4
  57. package/payload/platform/plugins/docs/references/plugins-guide.md +8 -6
  58. package/payload/platform/plugins/email/lib/mcp-spawn-tee/index.js +19 -19
  59. package/payload/platform/plugins/filesystem/lib/mcp-spawn-tee/index.js +19 -19
  60. package/payload/platform/plugins/graph-viewer/lib/mcp-spawn-tee/index.js +19 -19
  61. package/payload/platform/plugins/memory/PLUGIN.md +1 -1
  62. package/payload/platform/plugins/memory/lib/mcp-spawn-tee/index.js +19 -19
  63. package/payload/platform/plugins/memory/mcp/dist/lib/log-ingest.js +2 -2
  64. package/payload/platform/plugins/memory/mcp/dist/lib/log-ingest.js.map +1 -1
  65. package/payload/platform/plugins/outlook/.claude-plugin/plugin.json +1 -1
  66. package/payload/platform/plugins/outlook/PLUGIN.md +2 -2
  67. package/payload/platform/plugins/outlook/lib/mcp-spawn-tee/index.js +19 -19
  68. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-threaded.test.d.ts +2 -0
  69. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-threaded.test.d.ts.map +1 -0
  70. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-threaded.test.js +238 -0
  71. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-threaded.test.js.map +1 -0
  72. package/payload/platform/plugins/outlook/mcp/dist/index.js +9 -5
  73. package/payload/platform/plugins/outlook/mcp/dist/index.js.map +1 -1
  74. package/payload/platform/plugins/outlook/mcp/dist/lib/message.d.ts +1 -6
  75. package/payload/platform/plugins/outlook/mcp/dist/lib/message.d.ts.map +1 -1
  76. package/payload/platform/plugins/outlook/mcp/dist/lib/message.js +9 -17
  77. package/payload/platform/plugins/outlook/mcp/dist/lib/message.js.map +1 -1
  78. package/payload/platform/plugins/outlook/mcp/dist/lib/recipients.d.ts +56 -0
  79. package/payload/platform/plugins/outlook/mcp/dist/lib/recipients.d.ts.map +1 -0
  80. package/payload/platform/plugins/outlook/mcp/dist/lib/recipients.js +40 -0
  81. package/payload/platform/plugins/outlook/mcp/dist/lib/recipients.js.map +1 -0
  82. package/payload/platform/plugins/outlook/mcp/dist/tools/draft-edit.d.ts.map +1 -1
  83. package/payload/platform/plugins/outlook/mcp/dist/tools/draft-edit.js +1 -3
  84. package/payload/platform/plugins/outlook/mcp/dist/tools/draft-edit.js.map +1 -1
  85. package/payload/platform/plugins/outlook/mcp/dist/tools/draft.d.ts +58 -9
  86. package/payload/platform/plugins/outlook/mcp/dist/tools/draft.d.ts.map +1 -1
  87. package/payload/platform/plugins/outlook/mcp/dist/tools/draft.js +97 -18
  88. package/payload/platform/plugins/outlook/mcp/dist/tools/draft.js.map +1 -1
  89. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-fetch-body.d.ts.map +1 -1
  90. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-fetch-body.js +1 -3
  91. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-fetch-body.js.map +1 -1
  92. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-reply.d.ts.map +1 -1
  93. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-reply.js +3 -18
  94. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-reply.js.map +1 -1
  95. package/payload/platform/plugins/outlook/skills/outlook/SKILL.md +5 -2
  96. package/payload/platform/plugins/quickbooks/lib/mcp-spawn-tee/index.js +19 -19
  97. package/payload/platform/plugins/replicate/lib/mcp-spawn-tee/index.js +19 -19
  98. package/payload/platform/plugins/scheduling/lib/mcp-spawn-tee/index.js +19 -19
  99. package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/agent-turn-dispatch.test.js +6 -32
  100. package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/agent-turn-dispatch.test.js.map +1 -1
  101. package/payload/platform/plugins/scheduling/mcp/dist/scripts/agent-turn-dispatch.d.ts.map +1 -1
  102. package/payload/platform/plugins/scheduling/mcp/dist/scripts/agent-turn-dispatch.js +10 -30
  103. package/payload/platform/plugins/scheduling/mcp/dist/scripts/agent-turn-dispatch.js.map +1 -1
  104. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-cancel.test.js +24 -21
  105. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-cancel.test.js.map +1 -1
  106. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-cancel.d.ts +27 -18
  107. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-cancel.d.ts.map +1 -1
  108. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-cancel.js +31 -27
  109. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-cancel.js.map +1 -1
  110. package/payload/platform/plugins/storage-broker/PLUGIN.md +9 -1
  111. package/payload/platform/plugins/storage-broker/lib/mcp-spawn-tee/index.js +19 -19
  112. package/payload/platform/plugins/storage-broker/mcp/dist/confine-path.d.ts.map +1 -1
  113. package/payload/platform/plugins/storage-broker/mcp/dist/confine-path.js +16 -5
  114. package/payload/platform/plugins/storage-broker/mcp/dist/confine-path.js.map +1 -1
  115. package/payload/platform/plugins/storage-broker/mcp/dist/index.js +49 -0
  116. package/payload/platform/plugins/storage-broker/mcp/dist/index.js.map +1 -1
  117. package/payload/platform/plugins/telegram/lib/mcp-spawn-tee/index.js +19 -19
  118. package/payload/platform/plugins/url-get/lib/mcp-spawn-tee/index.js +19 -19
  119. package/payload/platform/plugins/voice-mirror/lib/mcp-spawn-tee/index.js +19 -19
  120. package/payload/platform/plugins/web-designer/PLUGIN.md +1 -1
  121. package/payload/platform/plugins/web-designer/commands/website.md +6 -0
  122. package/payload/platform/plugins/web-designer/skills/web-design/references/deploy.md +12 -0
  123. package/payload/platform/plugins/whatsapp/lib/mcp-spawn-tee/index.js +19 -19
  124. package/payload/platform/plugins/work/lib/mcp-spawn-tee/index.js +19 -19
  125. package/payload/platform/plugins/workflows/lib/mcp-spawn-tee/index.js +19 -19
  126. package/payload/platform/scripts/__tests__/logs-rotate.test.sh +24 -20
  127. package/payload/platform/scripts/logs-read.sh +31 -0
  128. package/payload/platform/scripts/logs-rotate.sh +21 -18
  129. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.d.ts.map +1 -1
  130. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js +2 -0
  131. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js.map +1 -1
  132. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
  133. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +37 -0
  134. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
  135. package/payload/platform/services/claude-session-manager/dist/rc-daemon.d.ts +5 -3
  136. package/payload/platform/services/claude-session-manager/dist/rc-daemon.d.ts.map +1 -1
  137. package/payload/platform/services/claude-session-manager/dist/rc-daemon.js.map +1 -1
  138. package/payload/platform/services/telegram-channel/dist/server.d.ts.map +1 -1
  139. package/payload/platform/services/telegram-channel/dist/server.js +0 -4
  140. package/payload/platform/services/telegram-channel/dist/server.js.map +1 -1
  141. package/payload/platform/services/webchat-channel/dist/server.d.ts.map +1 -1
  142. package/payload/platform/services/webchat-channel/dist/server.js +0 -4
  143. package/payload/platform/services/webchat-channel/dist/server.js.map +1 -1
  144. package/payload/platform/services/whatsapp-channel/dist/server.d.ts.map +1 -1
  145. package/payload/platform/services/whatsapp-channel/dist/server.js +0 -4
  146. package/payload/platform/services/whatsapp-channel/dist/server.js.map +1 -1
  147. package/payload/server/{chunk-JXWFVE5X.js → chunk-BBSPFN45.js} +65 -0
  148. package/payload/server/public/assets/{AdminLoginScreens-BFbpkEUS.js → AdminLoginScreens-Bed8SDxY.js} +1 -1
  149. package/payload/server/public/assets/{AdminShell-CdOGObaZ.js → AdminShell-BLh9O6dn.js} +1 -1
  150. package/payload/server/public/assets/{Checkbox-RFD_F0c0.js → Checkbox-BC_KSbpS.js} +1 -1
  151. package/payload/server/public/assets/{admin-CYGdL-1O.js → admin-DfWgJVI3.js} +1 -1
  152. package/payload/server/public/assets/{browser-DVfa_plj.js → browser-C0vkv-hH.js} +1 -1
  153. package/payload/server/public/assets/{calendar-JC_Dxa4n.js → calendar-_pfHw0Hp.js} +1 -1
  154. package/payload/server/public/assets/chat-DYT5H81o.js +1 -0
  155. package/payload/server/public/assets/chevron-left-BnpVBu51.js +1 -0
  156. package/payload/server/public/assets/data-CR-fbPWr.js +1 -0
  157. package/payload/server/public/assets/{graph-hBUrRSkr.js → graph-BZiudUGr.js} +1 -1
  158. package/payload/server/public/assets/{graph-labels-CHh6zs8t.js → graph-labels-BzmmJwCR.js} +1 -1
  159. package/payload/server/public/assets/{maximize-2-DjxE4rm9.js → maximize-2-Bhdsv0U-.js} +1 -1
  160. package/payload/server/public/assets/{operator-BqU9SQuu.js → operator-gm2eP4r6.js} +1 -1
  161. package/payload/server/public/assets/{page-DiQhU6lD.js → page-8AB3jUa_.js} +1 -1
  162. package/payload/server/public/assets/page-BoPorLg-.js +32 -0
  163. package/payload/server/public/assets/{public-Cu_eALgY.js → public-DqJk6qgW.js} +1 -1
  164. package/payload/server/public/assets/{rotate-ccw-F9DsSTSJ.js → rotate-ccw-D_5cETGt.js} +1 -1
  165. package/payload/server/public/assets/{tasks-CE-D0f50.js → tasks-EyKLXINi.js} +1 -1
  166. package/payload/server/public/assets/{time-entry-format-D2rsWhov.js → time-entry-format-DAwmmR3i.js} +1 -1
  167. package/payload/server/public/assets/{useCopyFeedback-QxOmoobV.js → useCopyFeedback-rOWd_lzl.js} +1 -1
  168. package/payload/server/public/assets/{useSelectionMode-RIzNqFjV.js → useSelectionMode-DDmLmPph.js} +1 -1
  169. package/payload/server/public/assets/{useSubAccountSwitcher-BW_fK1Nt.css → useSubAccountSwitcher-pu0jjLCz.css} +1 -1
  170. package/payload/server/public/assets/{useVoiceRecorder-WrWurSYI.js → useVoiceRecorder-azG8zSUI.js} +1 -1
  171. package/payload/server/public/browser.html +5 -5
  172. package/payload/server/public/calendar.html +6 -6
  173. package/payload/server/public/chat.html +12 -12
  174. package/payload/server/public/data.html +10 -10
  175. package/payload/server/public/graph.html +11 -11
  176. package/payload/server/public/index.html +13 -13
  177. package/payload/server/public/operator.html +14 -14
  178. package/payload/server/public/public.html +12 -12
  179. package/payload/server/public/tasks.html +5 -5
  180. package/payload/server/server.js +596 -235
  181. package/payload/server/{src-4F37OHLK.js → src-YLGAZY36.js} +5 -1
  182. package/payload/server/public/assets/chat-Yf8Nkop8.js +0 -1
  183. package/payload/server/public/assets/chevron-left-CAiAUBxO.js +0 -1
  184. package/payload/server/public/assets/data-hqdYXaEn.js +0 -1
  185. package/payload/server/public/assets/page-B4c4_4HP.js +0 -32
  186. /package/payload/server/public/assets/{useSubAccountSwitcher-wo_qGW-I.js → useSubAccountSwitcher-CU-lOxTd.js} +0 -0
@@ -52,6 +52,8 @@ export interface CfExec {
52
52
  r2ObjectGet(bucket: string, key: string): Promise<Uint8Array>;
53
53
  r2ObjectPut(bucket: string, key: string, body: Uint8Array): Promise<void>;
54
54
  r2ObjectDelete(bucket: string, key: string): Promise<void>;
55
+ pagesProjectList(): Promise<{ name: string }[]>;
56
+ pagesDeploy(dir: string, project: string, branch?: string): Promise<{ url: string }>;
55
57
  }
56
58
 
57
59
  // An object key carries `/` as a real path separator (the data portal's
@@ -301,6 +303,72 @@ export function makeCfExec(
301
303
  await run("npx", ["wrangler", "r2", "bucket", "create", name], env);
302
304
  },
303
305
 
306
+ // --- Pages (Task 1728) -------------------------------------------------
307
+ //
308
+ // Publishing is house-run: a client account's secrets carry no master after
309
+ // the 1631 remediation, and cf-token.sh refuses to mint the pages scope for
310
+ // a non-house caller. These two run under the house credential only.
311
+ async pagesProjectList() {
312
+ // `wrangler pages project list` has no --json flag and emits human prose,
313
+ // so it can neither be parsed nor honour the "never grep a CLI's prose"
314
+ // doctrine — the same reason r2List uses the API. Paginate to exhaustion:
315
+ // a single request returns one page, and the audit would read the missing
316
+ // remainder as "absent", reporting live projects as orphans. per_page is
317
+ // deliberately never sent (default and maximum unconfirmed).
318
+ const base = `https://api.cloudflare.com/client/v4/accounts/${cred.accountId}/pages/projects`;
319
+ const projects: { name: string }[] = [];
320
+ const seen = new Set<string>();
321
+ let cursor: string | undefined;
322
+ for (;;) {
323
+ const url = cursor === undefined ? base : `${base}?cursor=${encodeURIComponent(cursor)}`;
324
+ const { parsed } = await cfApiJson(
325
+ fetchFn,
326
+ url,
327
+ { method: "GET", headers: { Authorization: `Bearer ${cred.apiToken}` } },
328
+ "pages project list",
329
+ );
330
+ const page = parsed as {
331
+ result?: { name: string }[];
332
+ result_info?: { cursor?: string | null };
333
+ };
334
+ for (const p of page.result ?? []) projects.push({ name: p.name });
335
+ const next = page.result_info?.cursor;
336
+ if (!next) return projects;
337
+ // A repeated cursor means the API is cycling rather than advancing;
338
+ // surface it instead of looping forever (mirrors r2List).
339
+ if (seen.has(next)) {
340
+ throw new Error(
341
+ `storage-broker: pages project list returned a repeated pagination cursor: ${next}`,
342
+ );
343
+ }
344
+ seen.add(next);
345
+ cursor = next;
346
+ }
347
+ },
348
+ async pagesDeploy(dir, project, branch) {
349
+ // wrangler is the only supported Pages upload path (it hashes the tree and
350
+ // uploads only what is missing); the API has no single-call equivalent.
351
+ const args = ["wrangler", "pages", "deploy", dir, "--project-name", project];
352
+ if (branch !== undefined) args.push("--branch", branch);
353
+ const { stdout } = await run("npx", args, env);
354
+ // The deployment URL is what the caller relays, so parse it strictly.
355
+ // Strip ANSI first: wrangler colours its output when it thinks it has a
356
+ // terminal, and a colour reset adjacent to the URL would otherwise be
357
+ // swallowed by the host character class and returned as part of it.
358
+ const plain = stdout.replace(/\[[0-9;]*m/g, "");
359
+ // Take the LAST match, not the first: wrangler prints the deployment URL
360
+ // on its closing line, and any earlier .pages.dev mention (an alias or a
361
+ // progress line) would otherwise win and hand back the wrong address.
362
+ const urls = plain.match(/https:\/\/[A-Za-z0-9._-]+\.pages\.dev/g);
363
+ const url = urls?.[urls.length - 1];
364
+ // wrangler exits 0 on some partial paths, so a url-less success is an
365
+ // anomaly to surface with its output, never a silent "deployed".
366
+ if (!url) {
367
+ throw new Error(`storage-broker: pages deploy returned no deployment url: ${stdout}`);
368
+ }
369
+ return { url };
370
+ },
371
+
304
372
  // --- R2 objects (Task 1691) -------------------------------------------
305
373
  //
306
374
  // The v4 REST API exposes the object surface under
@@ -418,3 +486,26 @@ export async function makeHouseCfExec(
418
486
  const scoped = await resolveHouseScopedToken("storage", houseEnvPath, cfTokenSh, run);
419
487
  return makeCfExec({ apiToken: scoped, accountId: cred.accountId }, run, fetchFn);
420
488
  }
489
+
490
+ /**
491
+ * The Pages twin of makeHouseCfExec (Task 1728). Same house minter, different
492
+ * scope: "pages" resolves CF_PAGES_D1_TOKEN rather than CF_STORAGE_TOKEN.
493
+ *
494
+ * The minter is handed the HOUSE env, which carries a master, so cf-token.sh's
495
+ * house-fallback branch never runs and caller_is_house() is never consulted.
496
+ * That is exactly why this works from the ui-server process regardless of its
497
+ * ACCOUNT_ID, while a client session shelling cf-token.sh directly is denied
498
+ * reason=not-house. Separate from makeHouseCfExec so hosting never widens the
499
+ * storage token's scope, and storage never gains Pages Write.
500
+ */
501
+ export async function makeHousePagesExec(
502
+ platformRoot: string,
503
+ run: RunFn = defaultRun,
504
+ fetchFn: FetchFn = defaultFetch,
505
+ ): Promise<CfExec> {
506
+ const cred = readHouseCredential(platformRoot);
507
+ const houseEnvPath = join(platformRoot, "config", "cloudflare-house.env");
508
+ const cfTokenSh = join(platformRoot, "plugins/cloudflare/bin/cf-token.sh");
509
+ const scoped = await resolveHouseScopedToken("pages", houseEnvPath, cfTokenSh, run);
510
+ return makeCfExec({ apiToken: scoped, accountId: cred.accountId }, run, fetchFn);
511
+ }
@@ -66,6 +66,9 @@ function keyForScope(scope: string): string {
66
66
  return "CF_STORAGE_TOKEN";
67
67
  case "calendar-d1":
68
68
  return "CF_CALENDAR_D1_TOKEN";
69
+ case "pages":
70
+ // cf-token.sh's scope table maps pages and d1 to the same minted token.
71
+ return "CF_PAGES_D1_TOKEN";
69
72
  default:
70
73
  return "";
71
74
  }
@@ -22,7 +22,11 @@ export interface SessionLike {
22
22
  ): PromiseLike<{ records: Array<{ get(key: string): any }> }>;
23
23
  }
24
24
 
25
- export type ResourceKind = "d1" | "r2";
25
+ // "pages" (Task 1728) is an ownership record, not a storage resource: it binds a
26
+ // Cloudflare Pages project to the account allowed to publish it. It shares this
27
+ // registry because the ownership question ("may this caller act on this named
28
+ // thing?") and its answer (resolveOwner + authorizeAccess) are identical.
29
+ export type ResourceKind = "d1" | "r2" | "pages";
26
30
 
27
31
  export interface StorageResource {
28
32
  accountId: string;
@@ -13,14 +13,15 @@
13
13
  *
14
14
  * Claude Code CLI → shim (this file) = node running <real-entry> in-process
15
15
  * stdin/stdout : untouched (JSON-RPC channel)
16
- * stderr : process.stderr.write teed → per-date + per-session + passthrough
16
+ * stderr : process.stderr.write teed → raw sink + passthrough
17
17
  *
18
- * Destinations (Task 706) — unchanged:
19
- * - `${LOG_DIR}/mcp-<name>-stderr-<date>.log` per-date raw (back-compat;
20
- * the in-process mcp-stderr-tee and the [mcp-init-error] probe read it).
21
- * - `${LOG_DIR}/mcp-<name>-<SESSION_ID>.log` — per-session raw + lifecycle
22
- * lines. Authoritative sink. Absent when SESSION_ID is unset (enumeration
23
- * spawn)then the per-date file is the fallback.
18
+ * Destinations:
19
+ * - `${LOG_DIR}/mcp-<name>-<SESSION_ID>.log` — raw stderr + lifecycle lines.
20
+ * The sole raw sink. Task 1721 retired the per-date twin: its date was
21
+ * pinned at spawn, no reader ever parsed it, and mcp-stderr-tee wrote the
22
+ * same file, so every line landed twice.
23
+ * - `${LOG_DIR}/mcp-<name>-nosession.log` — the same, for a spawn with
24
+ * no SESSION_ID (enumeration). One fixed file per server.
24
25
  * - `server.log` via the loopback log-ingest route — best-effort mirror of
25
26
  * the [mcp-helper] lifecycle lines.
26
27
  *
@@ -49,13 +50,13 @@ const PLATFORM_PORT = process.env.PLATFORM_PORT;
49
50
  const ENTRY = process.argv[2];
50
51
  const SESSION_ID8 = SESSION_ID ? SESSION_ID.slice(0, 8) : "—";
51
52
  const spawnStamp = Date.now();
52
- // Per-session raw + lifecycle log (Task 706). Empty SESSION_ID (enumeration
53
- // spawn) undefined, and the per-date file is the fallback.
54
- const perSessionPath = LOG_DIR && SESSION_ID
55
- ? (0, node_path_1.resolve)(LOG_DIR, `mcp-${SERVER_NAME}-${SESSION_ID}.log`)
56
- : undefined;
57
- const perDatePath = LOG_DIR
58
- ? (0, node_path_1.resolve)(LOG_DIR, `mcp-${SERVER_NAME}-stderr-${new Date(spawnStamp).toISOString().slice(0, 10)}.log`)
53
+ // The one raw sink (Task 1721). A sessionless spawn (rc-daemon stamps
54
+ // SESSION_ID='' rc-daemon.ts:513 while still stamping LOG_DIR) shares one
55
+ // fixed file per server rather than a date-named one: the date was never read
56
+ // by any reader and named a process lifetime, not a day. Interleaved spawns in
57
+ // the nosession file are separated by the [mcp-helper] op=spawn pid= lines.
58
+ const rawSinkPath = LOG_DIR
59
+ ? (0, node_path_1.resolve)(LOG_DIR, SESSION_ID ? `mcp-${SERVER_NAME}-${SESSION_ID}.log` : `mcp-${SERVER_NAME}-nosession.log`)
59
60
  : undefined;
60
61
  // Rolling tail of entry stderr for op=exit. Capped so a chatty server cannot
61
62
  // grow the buffer without bound.
@@ -66,7 +67,7 @@ let exitEmitted = false;
66
67
  let exitSignal;
67
68
  // The real stderr writer, captured before the tee replaces it. Lifecycle lines
68
69
  // and stderr passthrough both go through this so they are never re-teed into
69
- // the per-date sink nor recursed back into the patched writer.
70
+ // the raw sink nor recursed back into the patched writer.
70
71
  const rawStderrWrite = process.stderr.write.bind(process.stderr);
71
72
  // LOG_DIR is created once, lazily, on the first successful append — not per
72
73
  // chunk. teeWrite runs on the server's own stderr hot path now, so a per-call
@@ -115,7 +116,7 @@ function postToServerLog(suffix, level) {
115
116
  // log-ingest route rejects newlines).
116
117
  function emitLifecycle(suffix, level) {
117
118
  const line = `[mcp-helper] ${suffix}\n`;
118
- appendSafe(perSessionPath, line);
119
+ appendSafe(rawSinkPath, line);
119
120
  try {
120
121
  rawStderrWrite(line);
121
122
  }
@@ -127,12 +128,11 @@ if (!ENTRY) {
127
128
  process.exit(2);
128
129
  }
129
130
  // Replace process.stderr.write with a tee: mirror every server stderr byte to
130
- // the per-date and per-session sinks, keep a rolling tail for op=exit, emit
131
+ // the raw sink, keep a rolling tail for op=exit, emit
131
132
  // op=boot on the first bytes, then pass the write through to the real stderr.
132
133
  const teeWrite = ((...args) => {
133
134
  const chunk = args[0];
134
- appendSafe(perDatePath, chunk); // per-date raw (back-compat)
135
- appendSafe(perSessionPath, chunk); // per-session raw (Task 706)
135
+ appendSafe(rawSinkPath, chunk); // raw sink (Task 1721)
136
136
  const text = typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8");
137
137
  stderrTail = (stderrTail + text).slice(-TAIL_CAP);
138
138
  if (!bootEmitted) {
@@ -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:c7d0d9f383f8d9ee8ed50b8869ce43477e3cb5ebf62305a39041c072e34bfeb1
4
+ content-hash: sha256:ddd3d65ece37d8e4ddcc7094ade79a1113d56ec2b0ca43e7d36e7f40c3f00efb
5
5
  brand: maxy-code
6
6
  product-name: Maxy
7
7
  ---
@@ -485,7 +485,9 @@ import { initStderrTee } from "../../../../lib/mcp-stderr-tee/dist/index.js";
485
485
  initStderrTee("your-plugin-name");
486
486
  ```
487
487
 
488
- After this, every `console.error("[your-tool]...")` from any tool in the plugin appears as `[<iso-ts>] [mcp:your-plugin-name] [your-tool]...` in the per-conversation stream log `claude-agent-stream-{sessionId}.log`, alongside the usual agent events. The raw per-server file `mcp-your-plugin-name-stderr-{date}.log` is still produced for deep-dive grep.
488
+ After this, every `console.error("[your-tool]...")` from any tool in the plugin is captured in the raw per-server file `mcp-your-plugin-name-{sessionId}.log`, written by the `mcp-spawn-tee` wrapper. That is where you grep.
489
+
490
+ `initStderrTee` is *intended* to also mirror each line into the per-conversation stream log `claude-agent-stream-{sessionId}.log` as `[<iso-ts>] [mcp:your-plugin-name] [your-tool]...`, alongside the usual agent events. It does not currently do so: that destination is gated on `STREAM_LOG_PATH`, which no spawner sets, so the call is a no-op in production. Do not expect `[mcp:<name>]` lines in a conversation's stream log, and do not read their absence as the server having emitted nothing. Tracked as [`.tasks/1736-*.md`](../../../.tasks/1736-nothing-sets-stream-log-path-so-the-per-conversation-mcp-stream-log-does-not-exist.md).
489
491
 
490
492
  **Premium plugins.** Source lives at `premium-plugins/<bundle>/plugins/<name>/mcp/src/` — deeper than platform plugins, so the source-relative import to `platform/lib/mcp-stderr-tee/dist/index.js` uses more `../` segments. The bundler rewrites the compiled output to the canonical `../../../../lib/mcp-stderr-tee/dist/index.js` at staging time and ships `platform/lib/mcp-stderr-tee/{dist,package.json}` into `premium-plugins/<bundle>/lib/mcp-stderr-tee/` so the import resolves at deployed depth. The bundler fails loudly if `platform/lib/mcp-stderr-tee/package.json` is missing (it must pin `type` so install-location parent walks cannot mis-classify the dist file) or if any lib referenced by a rewritten import has no source dist.
491
493
 
@@ -496,7 +498,7 @@ After this, every `console.error("[your-tool]...")` from any tool in the plugin
496
498
  **Retrieve MCP diagnostic lines for a conversation:**
497
499
 
498
500
  - All servers: `logs-read { type: "system", sessionId: "..." }` → grep `[mcp:<name>]` on the returned stream log.
499
- - One server raw feed: `logs-read { type: "mcp" }` → tails the most recent `mcp-<name>-stderr-*.log` (per-plugin, not per-conversation).
501
+ - One server raw feed: `logs-read { type: "mcp" }` → tails the most recent `mcp-<name>-*.log` (per-plugin, not per-conversation).
500
502
 
501
503
  **Tee-state markers** land in the stream log: `[platform] [mcp-tee-attach] server=<name> streamLogPath=...` when the tee wires up, `[platform] [mcp-tee-skip] server=<name> destination=... reason=...` when a destination fails (missing `LOG_DIR`, unwritable path, `STREAM_LOG_PATH` not set, etc.), `[platform] [mcp-tee-detach] server=<name>` on graceful shutdown. If a server invoked tools but no `[mcp:<name>]` lines appear in the conversation's log, look for the skip marker first.
502
504
 
@@ -504,17 +506,17 @@ After this, every `console.error("[your-tool]...")` from any tool in the plugin
504
506
 
505
507
  ## Failure-path observability contract (earlier platform fixes + earlier platform fixes)
506
508
 
507
- The `initStderrTee` wrapper writes to the per-conversation stream log and per-server raw file via `createWriteStream` — async, buffered. Any diagnostic `console.error(…)` followed by an immediate `process.exit(…)` is lost: the event loop never drains the WriteStream before the process terminates. Same race for any synchronous module-load throw: Node's uncaught-exception handler writes the stack to raw fd 2 and exits before the patched async stream flushes. The platform's `[mcp-init-error] tail="(no stderr file)"` line operationally useless is the public symptom of this race.
509
+ The `initStderrTee` wrapper writes to the per-conversation stream log via `createWriteStream` — async, buffered. Any diagnostic `console.error(…)` followed by an immediate `process.exit(…)` is lost from *that* destination: the event loop never drains the WriteStream before the process terminates. Same race for any synchronous module-load throw: Node's uncaught-exception handler writes the stack to raw fd 2 and exits before the patched async stream flushes. The raw sink does not have this race — removed `initStderrTee`'s own raw file, and `mcp-spawn-tee` writes the sink with `appendFileSync`, which returns once the kernel has queued the write and so survives an immediate exit.
508
510
 
509
511
  **Two layers now close the gap, each load-bearing on its own:**
510
512
 
511
513
  1. **Plugin-side sync-write discipline.** Plugins that call `process.exit` during module load (rare — `graph-mcp` is the in-tree example; it spawns a child at boot to proxy upstream stdio) use `fs.appendFileSync` at every named exit path to guarantee the cause lands in both log destinations before exit. Lines follow the `[mcp:<name>] [<plugin-prefix>] <cause>` format so existing `grep '[mcp:<name>]'` investigator paths work. Each destination is wrapped in its own try/catch — an unwritable log must not mask the primary failure. This is the discipline propagated to any plugin author who knows their failure paths.
512
514
 
513
- 2. **Parent-side `mcp-spawn-tee` wrapper.** Every node-based core MCP server is spawned via the `lib/mcp-spawn-tee` wrapper rather than `node <entry>` directly. The wrapper spawns the real entry with `stdio: ['inherit', 'inherit', 'pipe']` and writes child stderr chunks to `${LOG_DIR}/mcp-${name}-stderr-<date>.log` via `appendFileSync` while passing the same chunks through to its own stderr (Claude Code's consumer is unchanged). Synchronous `appendFileSync` survives `process.exit`, so the per-server file captures even (a) module-load throws before `initStderrTee` runs, (b) `MODULE_NOT_FOUND` on the entry script itself, and (c) anything else a plugin author missed. The wrapper writes `[mcp-spawn-tee-attached] server=<name> pid=<n>` on attach and forwards SIGTERM/SIGINT to the child. This is the layer that makes capture independent of plugin discipline. Playwright stays unwrapped because it spawns via `npx`, not `node`.
515
+ 2. **Parent-side `mcp-spawn-tee` wrapper.** Every node-based core MCP server is spawned via the `lib/mcp-spawn-tee` wrapper rather than `node <entry>` directly. The wrapper runs the real entry in its own process and writes stderr chunks to `${LOG_DIR}/mcp-${name}-${SESSION_ID}.log` (or `mcp-${name}-nosession.log` when `SESSION_ID` is unset) via `appendFileSync` while passing the same chunks through to its own stderr (Claude Code's consumer is unchanged). It is the only writer of that raw sink: retired the `mcp-${name}-stderr-<date>.log` twin, which `mcp-stderr-tee` also wrote, so every line used to land there twice. Synchronous `appendFileSync` survives `process.exit`, so the sink captures even (a) module-load throws before `initStderrTee` runs, (b) `MODULE_NOT_FOUND` on the entry script itself, and (c) anything else a plugin author missed. The wrapper writes `[mcp-spawn-tee-attached] server=<name> pid=<n>` on attach and forwards SIGTERM/SIGINT to the child. This is the layer that makes capture independent of plugin discipline. Playwright stays unwrapped because it spawns via `npx`, not `node`.
514
516
 
515
- A third layer closes the same gap from the platform side: when `claude-agent.ts` observes an `init` event with any MCP server reporting `status:"failed"`, it reads the last 512 bytes of `${LOG_DIR}/mcp-<name>-stderr-<date>.log` and emits `[mcp-init-error] server=<name> tail=<quoted>` into the stream log. Absent file `tail="(no stderr file)"`; empty file `tail="(empty)"`. With the spawn-tee wrapper now interposing on every core MCP, `tail="(no stderr file)"` means the wrapper itself is broken file follow-up.
517
+ A `[mcp-init-error]` probe on the platform side used to read the retired per-date file and tail it into the stream log. found no such probe in live code `claude-agent.ts` has not carried one for some time so the wrapper's own `[mcp-helper] op=boot` and `op=exit` lines are the record of a failed MCP boot.
516
518
 
517
- **Signal inventory after a failed session:** `[init] FAILED MCP servers: <names>` (names), `[mcp-init-error] server=<name> tail=…` (cause for each, from the platform's tail probe), `[mcp-spawn-tee-attached] server=<name> pid=<n>` (proof the wrapper attached), `[mcp-spawn-tee-exit] server=<name> code=<n>|signal=<s>` (proof the wrapper saw the exit), and optionally `[mcp:<name>] [<plugin>] …` from plugin-side sync-writes. Their union gives the investigator three independent sources for the same failure.
519
+ **Signal inventory after a failed session:** `[init] FAILED MCP servers: <names>` (names), `[mcp-spawn-tee-attached] server=<name> pid=<n>` (proof the wrapper attached), `[mcp-spawn-tee-exit] server=<name> code=<n>|signal=<s>` (proof the wrapper saw the exit), and optionally `[mcp:<name>] [<plugin>] …` from plugin-side sync-writes. Their union gives the investigator independent sources for the same failure. (The two `[mcp-spawn-tee-*]` names are stale — the shim emits `[mcp-helper] op=spawn|boot|exit`; see [`.tasks/1737-*.md`](../../../.tasks/1737-mcp-spawn-tee-docs-promise-lifecycle-markers-and-a-child-process-that-task-989-removed.md).)
518
520
 
519
521
  **Boot-smoke as publish-time gate.** The memory MCP carries `scripts/boot-smoke.sh` that spawns `dist/index.js` with stub env, sleeps 2s, asserts `kill -0 <pid>`, and reports `[boot-smoke] memory ok|FAILED tail=<n-lines>`. Wired to `prepublish` in `plugins/memory/mcp/package.json`. The pattern is propagatable to other plugin MCPs — it's deliberately not generalised yet because each plugin's stub-env requirements differ (memory needs ACCOUNT_ID + PLATFORM_ROOT + NEO4J_URI + SESSION_ID; others differ).
520
522
 
@@ -1146,6 +1148,8 @@ Source: https://docs.getmaxy.com/cloudflare.md
1146
1148
 
1147
1149
  Each installation has its own Cloudflare account. The **tunnel** sign-in is OAuth: the agent invokes `cloudflared tunnel login` via Bash; the Cloudflare Authorize URL streams into the admin chat PTY and the native terminal renders it as a clickable link. Click it, authorise in your own browser, and `cloudflared` writes `cert.pem` to the brand's config directory. For **everything else** (DNS, Pages, D1, Access) the agent uses the Cloudflare API, authenticated by a short-lived narrow token it mints from a master token you provision once in the dashboard (an advanced step the agent never automates). Some account-side jobs — adding a domain, switching accounts — are still easiest in your browser, and the agent relays those click-paths; the rest it can do directly via the API.
1148
1150
 
1151
+ Where one installation serves several client accounts from one Cloudflare account, that master is held by the house account alone, and a client account cannot mint from it — it is account-wide, so handing it to one client would hand them every other client's data. Publishing a client's website therefore runs house-side on the client's behalf: the client's session asks, the house does the deploy, and no Cloudflare token ever enters the client's account. Which website belongs to which client is recorded once by the house and is never inferred from folder names, so a site nobody has recorded cannot be published by anyone until it is.
1152
+
1149
1153
  ## Identity model
1150
1154
 
1151
1155
  | Concept | Source |
@@ -2072,7 +2076,7 @@ The `outlook` plugin gives the admin agent access to Microsoft 365 / Outlook.com
2072
2076
  | `outlook-mailbox-info` | Health probe for one mailbox — auth state, refresh-window, folder count. Name the mailbox with `mailbox` when several are attached. |
2073
2077
  | `outlook-mailbox-list` | Every connected mailbox with its email, scopes, and token health. Local only — no network — so it also flags a mailbox whose stored credentials cannot be read. |
2074
2078
  | `outlook-mail-send` | Send a message. No attachments → `POST /me/sendMail`; with attachments → draft, attach, send (`/me/sendMail` carries attachments inline and cannot fit a large file). to/cc/bcc arrays, subject, body, isHtml, attachments (account-dir paths, 25 MB/file). Success asserted on Graph 202; no message id on either route. Needs `Mail.Send` (+ `Mail.ReadWrite` to attach). |
2075
- | `outlook-draft` | Create a draft in Drafts (`POST /me/messages`). Same body model, including attachments files are attached before the id is returned, so the draft is ready to send. Returns the draft id. Needs `Mail.ReadWrite`. |
2079
+ | `outlook-draft` | Create a draft in Drafts, either a new message or a threaded reply. Returns the draft id; never sends. Attachments are attached before the id is returned, so the draft is ready to send. Needs `Mail.ReadWrite`. **New message** (no `replyToMessageId`): `POST /me/messages`, requires `to` + `subject`, same body model as `outlook-mail-send`, threads under nothing. **Threaded reply** (`replyToMessageId` = the id of the message being answered): `POST /me/messages/{id}/createReply` — the draft carries the parent's conversationId, so it lands under the original. This is the only way to draft a reply without sending it: `outlook-mail-reply` threads but always sends. Graph sets the recipients and `RE:` subject from the original and composes the body above its own quote, matching what `outlook-mail-reply` sends, so the threaded path is **plain text only** — `to`, `subject`, and `isHtml: true` are **refused** (not ignored) when replying. `replyAll` includes everyone on the original; `cc`/`bcc` add recipients. |
2076
2080
  | `outlook-draft-edit` | Update an existing draft in place (`PATCH /me/messages/{draftId}`) and/or add and remove attachments. Graph drafts are mutable — pass draftId plus the fields to change (to/cc/bcc/subject/body/isHtml/attachments/removeAttachments). `attachments` adds; `removeAttachments` takes files off by name or attachment id; both together replace a file in one call. Sent messages cannot be edited. Needs `Mail.ReadWrite`. |
2077
2081
  | `outlook-draft-send` | Send an existing draft by id (`POST /me/messages/{id}/send`). Graph consumes the draft, returns 202. Needs `Mail.Send`. |
2078
2082
 
@@ -2114,9 +2118,9 @@ All log lines start with `[outlook-mcp]` and write to `server.log`. They are key
2114
2118
  | Send args | `send-args account=<id> recipients=<N> hasBody=<bool>` |
2115
2119
  | Sent | `sent account=<id> status=202 messageId=<id-or-null>` |
2116
2120
  | Send failed | `send-failed account=<id> error=<msg>` |
2117
- | Draft request | `draft-request account=<id>` |
2118
- | Draft created | `draft-created account=<id> id=<draftId>` |
2119
- | Draft failed | `draft-failed account=<id> error=<msg>` |
2121
+ | Draft request | `draft-request account=<id> threaded=<bool>` |
2122
+ | Draft created | `draft-created account=<id> id=<draftId> threaded=<bool> convId=<id\|null>` |
2123
+ | Draft failed | `draft-failed account=<id> threaded=<bool> error=<msg>` |
2120
2124
  | Draft-send request | `draft-send-request account=<id> id=<draftId>` |
2121
2125
  | Draft sent | `draft-sent account=<id> id=<draftId> status=202` |
2122
2126
  | Draft-send failed | `draft-send-failed account=<id> id=<draftId> error=<msg>` |
@@ -2129,8 +2133,15 @@ ssh laptop 'grep -E "^\[outlook-mcp\]" ~/.maxy/logs/server.log | grep "account=<
2129
2133
 
2130
2134
  # Token-leak audit — must always return zero
2131
2135
  grep -rn -iE "Bearer |access_token=" ~/.maxy/logs/server.log | head
2136
+
2137
+ # Threading audit — a threaded write that reports no conversation LOST its thread.
2138
+ # Must always return zero. Covers both outbound threaded paths (mail-reply and draft-created).
2139
+ grep -E "^\[outlook-mcp\] (mail-reply|draft-created)" ~/.maxy/logs/server.log \
2140
+ | grep -E "threaded=true|op=reply" | grep -E "convId=(null)?( |$)" | head
2132
2141
  ```
2133
2142
 
2143
+ Threading triage: **absence of a `convId` on a threaded write means threading was lost.** `draft-created threaded=false convId=null` is correct and expected — a new message has no parent conversation. `draft-created threaded=true convId=null` is a fault: Graph returned a reply draft that names no conversation, and the reply will land detached from the original.
2144
+
2134
2145
  Latency triage: `mail-list count=0 elapsedMs<200` consistent → permissions issue; `elapsedMs > 5000` → Graph slowness or DNS.
2135
2146
 
2136
2147
  ## Failure modes
@@ -3941,7 +3952,7 @@ Standing rules the operator gives over a channel are `Preference` nodes. Two mec
3941
3952
 
3942
3953
  **Inject the active rule-set every turn (channel gateways).** The one definition of "this account's active rules" is `@maxy/active-rules`: the account owner's admin-scope preferences (resolve the seeded `AdminUser {role:'owner'}`, then select through that owner's `UserProfile` via `HAS_PREFERENCE`), a bounded, floor-filtered (`confidence >= 0.4`), confidence-ordered candidate query, then semantic de-confliction (one winner per subject at the same 0.85 cosine), capped to `INJECTION_RULE_CAP` (12). A preference owned by a non-owner profile (an operator's, stranded on the operator's own profile while managing the account) is never injected; an account with no seeded owner injects nothing. `profile-read` exposes this same owner-scoped set as `activeRules`, and the three inbound gateways (`wa-gateway`, `telegram-gateway`, `webchat-gateway`) fetch it for the effective session account and attach it as `standingRules` on the inbound frame; the channel builders prepend it as a `## Standing rules` block above `## Context`. Both channel-server inbound paths carry the block: the single-target SSE loop parses the whole frame, and the unified multi-target door reader (`runDoorStream`,) reconstructs it through the shared `doorInboundPayload` seam whose `parseFrame` closures read `standingRules` off the frame. The read is by code, not agent discretion, so a rule set last week is applied this week regardless of how many times the session compacted. Telegram and webchat inject for `role:'admin'` only — a public visitor never receives operator rules.
3943
3954
 
3944
- **Trust a scheduled turn's provenance, never an inbound's claim (channel-agent grounding).** A cron-dispatched agent turn reaches the same channel builders as a real inbound, so its own authorisation text would otherwise land inside `## Context` — the zone `REFRAME_INSTRUCTION` demotes to untrusted — and a security-cautious agent would distrust its own automation (the self-cancel loop). The dispatcher therefore stamps a `ScheduleProvenance` marker (owning account, event, owner userId, dispatch time) that the channel builders render as a `## Scheduled automation (platform-verified)` preamble **above** `## Context` (and above the rules block); the whatsapp wrap tag names the door `scheduler`. Both channel-server paths carry the marker — the single-target loop and the unified multi-target door reader (via the same `doorInboundPayload` seam that carries `standingRules`); the scheduler only dispatches to the whatsapp/telegram door, so a webchat frame never bears one. Because the marker is dispatcher-stamped and never derived from payload text, an inbound message cannot forge it — placement above `## Context` alone is not the trust signal, the dispatcher provenance is. The grounding binds to it in two places: one recipient-neutral clause added byte-identical to the three reframe instructions (a marker-bearing turn is authorised standing automation; a reconfiguration or re-enable is not intrusion), and the admin `IDENTITY.md` "Untrusted input" rule that the agent never states a platform-level authenticity or impersonation verdict it was not given (an unsourced "this is not from you" is its own inference, flagged, never asserted as the platform's). The amplifier that let one misread harden was the accreting seat: a recurring event resumed one long-lived session every firing. A subsequent change removes it each firing runs a fresh per-firing seat (`scheduledSeatIdFor`, salted with the firing's eventId + dispatch time), so distrust cannot accrete across firings, and the self-cancel guard compares the cancelling `SESSION_ID` to the node's dispatcher-stamped `lastDispatchSessionId` rather than a recomputed deterministic seat. See `.docs/scheduling-agent-dispatch.md § Operator-provenance marker` and `§ Session model — fresh per firing`.
3955
+ **Trust a scheduled turn's provenance, never an inbound's claim (channel-agent grounding).** A cron-dispatched agent turn reaches the same channel builders as a real inbound, so its own authorisation text would otherwise land inside `## Context` — the zone `REFRAME_INSTRUCTION` demotes to untrusted — and a security-cautious agent would distrust its own automation (the self-cancel loop). The dispatcher therefore stamps a `ScheduleProvenance` marker (owning account, event, owner userId, dispatch time) that the channel builders render as a `## Scheduled automation (platform-verified)` preamble **above** `## Context` (and above the rules block); the whatsapp wrap tag names the door `scheduler`. Both channel-server paths carry the marker — the single-target loop and the unified multi-target door reader (via the same `doorInboundPayload` seam that carries `standingRules`); the scheduler only dispatches to the whatsapp/telegram door, so a webchat frame never bears one. Because the marker is dispatcher-stamped and never derived from payload text, an inbound message cannot forge it — placement above `## Context` alone is not the trust signal, the dispatcher provenance is. The grounding binds to it in two places: one recipient-neutral clause added byte-identical to the three reframe instructions (a marker-bearing turn is authorised standing automation; a reconfiguration or re-enable is not intrusion), and the admin `IDENTITY.md` "Untrusted input" rule that the agent never states a platform-level authenticity or impersonation verdict it was not given (an unsourced "this is not from you" is its own inference, flagged, never asserted as the platform's). A sender has exactly one session: every inbound, real or scheduled, resolves to the deterministic `adminSessionIdFor(effectiveAccount, destination)`. An earlier change gave each firing its own salted seat so distrust could not accrete across firings, but the inbound hub keys on `senderId` and replaces its subscriber outright, so a seat's channel server attached under the sender's own key and swallowed the sender's REAL messages until it detached — splitting one conversation across two sessions that each denied what the other held. The seat is removed; the authoritative `agentPrompt`, this provenance marker, and the injected standing rules ground each firing without one. Self-cancel attribution is disarmed while that model beds in: the guard used to compare the cancelling turn's session id to a dispatcher stamp, which discriminated only because each firing ran its own seat, so with one session per sender it would report an autonomous loop on every operator cancel. It now always reports false — under-counting a real loop rather than inventing one until the discriminator is rebuilt from the turn's own dispatched eventId. See `.docs/scheduling-agent-dispatch.md § Operator-provenance marker`, `§ Session model — one session per sender`, and `§ Self-cancel attribution — disarmed`.
3945
3956
 
3946
3957
  **Observability.** `[preference-write] op=reconcile … mode=<reinforce|update|contradict|new> similarity=…` — a stream of `mode=new` for subjects that already exist is the duplicate-minting signature. `[preference-inject] op=inject channel=<wa|tg|web> injected=<N> owner=<8> source=<owner|no-owner|owner-error|fetch-error>` — injection selects only the account owner's admin preferences, so `source=owner injected=0` is a genuinely empty owner, `source=no-owner` is an account with no seeded owner (injects nothing, a missed owner seeding), and `injected=0 source=owner` on an account whose owner holds rules is the write-only regression re-appearing. `[preference-audit] op=drift accountId=… storedActive=… injectedActive=… duplicateClusters=…` — the standing reconciliation audit (claude-session-manager, 5-minute tick) flags any account whose store holds more active rows than injection would surface, the no-event detector for drift.
3947
3958
 
@@ -13,14 +13,15 @@
13
13
  *
14
14
  * Claude Code CLI → shim (this file) = node running <real-entry> in-process
15
15
  * stdin/stdout : untouched (JSON-RPC channel)
16
- * stderr : process.stderr.write teed → per-date + per-session + passthrough
16
+ * stderr : process.stderr.write teed → raw sink + passthrough
17
17
  *
18
- * Destinations (Task 706) — unchanged:
19
- * - `${LOG_DIR}/mcp-<name>-stderr-<date>.log` per-date raw (back-compat;
20
- * the in-process mcp-stderr-tee and the [mcp-init-error] probe read it).
21
- * - `${LOG_DIR}/mcp-<name>-<SESSION_ID>.log` — per-session raw + lifecycle
22
- * lines. Authoritative sink. Absent when SESSION_ID is unset (enumeration
23
- * spawn)then the per-date file is the fallback.
18
+ * Destinations:
19
+ * - `${LOG_DIR}/mcp-<name>-<SESSION_ID>.log` — raw stderr + lifecycle lines.
20
+ * The sole raw sink. Task 1721 retired the per-date twin: its date was
21
+ * pinned at spawn, no reader ever parsed it, and mcp-stderr-tee wrote the
22
+ * same file, so every line landed twice.
23
+ * - `${LOG_DIR}/mcp-<name>-nosession.log` — the same, for a spawn with
24
+ * no SESSION_ID (enumeration). One fixed file per server.
24
25
  * - `server.log` via the loopback log-ingest route — best-effort mirror of
25
26
  * the [mcp-helper] lifecycle lines.
26
27
  *
@@ -49,13 +50,13 @@ const PLATFORM_PORT = process.env.PLATFORM_PORT;
49
50
  const ENTRY = process.argv[2];
50
51
  const SESSION_ID8 = SESSION_ID ? SESSION_ID.slice(0, 8) : "—";
51
52
  const spawnStamp = Date.now();
52
- // Per-session raw + lifecycle log (Task 706). Empty SESSION_ID (enumeration
53
- // spawn) undefined, and the per-date file is the fallback.
54
- const perSessionPath = LOG_DIR && SESSION_ID
55
- ? (0, node_path_1.resolve)(LOG_DIR, `mcp-${SERVER_NAME}-${SESSION_ID}.log`)
56
- : undefined;
57
- const perDatePath = LOG_DIR
58
- ? (0, node_path_1.resolve)(LOG_DIR, `mcp-${SERVER_NAME}-stderr-${new Date(spawnStamp).toISOString().slice(0, 10)}.log`)
53
+ // The one raw sink (Task 1721). A sessionless spawn (rc-daemon stamps
54
+ // SESSION_ID='' rc-daemon.ts:513 while still stamping LOG_DIR) shares one
55
+ // fixed file per server rather than a date-named one: the date was never read
56
+ // by any reader and named a process lifetime, not a day. Interleaved spawns in
57
+ // the nosession file are separated by the [mcp-helper] op=spawn pid= lines.
58
+ const rawSinkPath = LOG_DIR
59
+ ? (0, node_path_1.resolve)(LOG_DIR, SESSION_ID ? `mcp-${SERVER_NAME}-${SESSION_ID}.log` : `mcp-${SERVER_NAME}-nosession.log`)
59
60
  : undefined;
60
61
  // Rolling tail of entry stderr for op=exit. Capped so a chatty server cannot
61
62
  // grow the buffer without bound.
@@ -66,7 +67,7 @@ let exitEmitted = false;
66
67
  let exitSignal;
67
68
  // The real stderr writer, captured before the tee replaces it. Lifecycle lines
68
69
  // and stderr passthrough both go through this so they are never re-teed into
69
- // the per-date sink nor recursed back into the patched writer.
70
+ // the raw sink nor recursed back into the patched writer.
70
71
  const rawStderrWrite = process.stderr.write.bind(process.stderr);
71
72
  // LOG_DIR is created once, lazily, on the first successful append — not per
72
73
  // chunk. teeWrite runs on the server's own stderr hot path now, so a per-call
@@ -115,7 +116,7 @@ function postToServerLog(suffix, level) {
115
116
  // log-ingest route rejects newlines).
116
117
  function emitLifecycle(suffix, level) {
117
118
  const line = `[mcp-helper] ${suffix}\n`;
118
- appendSafe(perSessionPath, line);
119
+ appendSafe(rawSinkPath, line);
119
120
  try {
120
121
  rawStderrWrite(line);
121
122
  }
@@ -127,12 +128,11 @@ if (!ENTRY) {
127
128
  process.exit(2);
128
129
  }
129
130
  // Replace process.stderr.write with a tee: mirror every server stderr byte to
130
- // the per-date and per-session sinks, keep a rolling tail for op=exit, emit
131
+ // the raw sink, keep a rolling tail for op=exit, emit
131
132
  // op=boot on the first bytes, then pass the write through to the real stderr.
132
133
  const teeWrite = ((...args) => {
133
134
  const chunk = args[0];
134
- appendSafe(perDatePath, chunk); // per-date raw (back-compat)
135
- appendSafe(perSessionPath, chunk); // per-session raw (Task 706)
135
+ appendSafe(rawSinkPath, chunk); // raw sink (Task 1721)
136
136
  const text = typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8");
137
137
  stderrTail = (stderrTail + text).slice(-TAIL_CAP);
138
138
  if (!bootEmitted) {
@@ -13,14 +13,15 @@
13
13
  *
14
14
  * Claude Code CLI → shim (this file) = node running <real-entry> in-process
15
15
  * stdin/stdout : untouched (JSON-RPC channel)
16
- * stderr : process.stderr.write teed → per-date + per-session + passthrough
16
+ * stderr : process.stderr.write teed → raw sink + passthrough
17
17
  *
18
- * Destinations (Task 706) — unchanged:
19
- * - `${LOG_DIR}/mcp-<name>-stderr-<date>.log` per-date raw (back-compat;
20
- * the in-process mcp-stderr-tee and the [mcp-init-error] probe read it).
21
- * - `${LOG_DIR}/mcp-<name>-<SESSION_ID>.log` — per-session raw + lifecycle
22
- * lines. Authoritative sink. Absent when SESSION_ID is unset (enumeration
23
- * spawn)then the per-date file is the fallback.
18
+ * Destinations:
19
+ * - `${LOG_DIR}/mcp-<name>-<SESSION_ID>.log` — raw stderr + lifecycle lines.
20
+ * The sole raw sink. Task 1721 retired the per-date twin: its date was
21
+ * pinned at spawn, no reader ever parsed it, and mcp-stderr-tee wrote the
22
+ * same file, so every line landed twice.
23
+ * - `${LOG_DIR}/mcp-<name>-nosession.log` — the same, for a spawn with
24
+ * no SESSION_ID (enumeration). One fixed file per server.
24
25
  * - `server.log` via the loopback log-ingest route — best-effort mirror of
25
26
  * the [mcp-helper] lifecycle lines.
26
27
  *
@@ -49,13 +50,13 @@ const PLATFORM_PORT = process.env.PLATFORM_PORT;
49
50
  const ENTRY = process.argv[2];
50
51
  const SESSION_ID8 = SESSION_ID ? SESSION_ID.slice(0, 8) : "—";
51
52
  const spawnStamp = Date.now();
52
- // Per-session raw + lifecycle log (Task 706). Empty SESSION_ID (enumeration
53
- // spawn) undefined, and the per-date file is the fallback.
54
- const perSessionPath = LOG_DIR && SESSION_ID
55
- ? (0, node_path_1.resolve)(LOG_DIR, `mcp-${SERVER_NAME}-${SESSION_ID}.log`)
56
- : undefined;
57
- const perDatePath = LOG_DIR
58
- ? (0, node_path_1.resolve)(LOG_DIR, `mcp-${SERVER_NAME}-stderr-${new Date(spawnStamp).toISOString().slice(0, 10)}.log`)
53
+ // The one raw sink (Task 1721). A sessionless spawn (rc-daemon stamps
54
+ // SESSION_ID='' rc-daemon.ts:513 while still stamping LOG_DIR) shares one
55
+ // fixed file per server rather than a date-named one: the date was never read
56
+ // by any reader and named a process lifetime, not a day. Interleaved spawns in
57
+ // the nosession file are separated by the [mcp-helper] op=spawn pid= lines.
58
+ const rawSinkPath = LOG_DIR
59
+ ? (0, node_path_1.resolve)(LOG_DIR, SESSION_ID ? `mcp-${SERVER_NAME}-${SESSION_ID}.log` : `mcp-${SERVER_NAME}-nosession.log`)
59
60
  : undefined;
60
61
  // Rolling tail of entry stderr for op=exit. Capped so a chatty server cannot
61
62
  // grow the buffer without bound.
@@ -66,7 +67,7 @@ let exitEmitted = false;
66
67
  let exitSignal;
67
68
  // The real stderr writer, captured before the tee replaces it. Lifecycle lines
68
69
  // and stderr passthrough both go through this so they are never re-teed into
69
- // the per-date sink nor recursed back into the patched writer.
70
+ // the raw sink nor recursed back into the patched writer.
70
71
  const rawStderrWrite = process.stderr.write.bind(process.stderr);
71
72
  // LOG_DIR is created once, lazily, on the first successful append — not per
72
73
  // chunk. teeWrite runs on the server's own stderr hot path now, so a per-call
@@ -115,7 +116,7 @@ function postToServerLog(suffix, level) {
115
116
  // log-ingest route rejects newlines).
116
117
  function emitLifecycle(suffix, level) {
117
118
  const line = `[mcp-helper] ${suffix}\n`;
118
- appendSafe(perSessionPath, line);
119
+ appendSafe(rawSinkPath, line);
119
120
  try {
120
121
  rawStderrWrite(line);
121
122
  }
@@ -127,12 +128,11 @@ if (!ENTRY) {
127
128
  process.exit(2);
128
129
  }
129
130
  // Replace process.stderr.write with a tee: mirror every server stderr byte to
130
- // the per-date and per-session sinks, keep a rolling tail for op=exit, emit
131
+ // the raw sink, keep a rolling tail for op=exit, emit
131
132
  // op=boot on the first bytes, then pass the write through to the real stderr.
132
133
  const teeWrite = ((...args) => {
133
134
  const chunk = args[0];
134
- appendSafe(perDatePath, chunk); // per-date raw (back-compat)
135
- appendSafe(perSessionPath, chunk); // per-session raw (Task 706)
135
+ appendSafe(rawSinkPath, chunk); // raw sink (Task 1721)
136
136
  const text = typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8");
137
137
  stderrTail = (stderrTail + text).slice(-TAIL_CAP);
138
138
  if (!bootEmitted) {
@@ -44,7 +44,7 @@ The plugin registers no agent-facing MCP tools. Every Cloudflare operation is th
44
44
  | [api.md](references/api.md) | Cloudflare API library — canonical docs URL + curated endpoint map (DNS, zones, tunnels, token list/create/verify/revoke), the advanced master-token creation walkthrough, the account-scoped secrets-file storage convention, the agent's reuse-a-stable-per-scope-token discipline, and the standing reconcile pass that revokes strays. |
45
45
  | [d1-data-capture.md](references/d1-data-capture.md) | Form → Pages Function → D1 store → read/sweep. The Pages-Edit **and** D1-Edit token-scope requirement, `wrangler d1 create`/`execute --remote`, the `swept` column, plus the establishment discipline: `<accountId>-<purpose>` naming, one store per purpose, and the canonical `acceptances` schema. |
46
46
  | [r2-object-storage.md](references/r2-object-storage.md) | R2 buckets under the same establishment discipline as D1: `<accountId>-<purpose>` naming via `cf-store-name.sh`, one bucket per purpose, `wrangler r2 bucket create`, and the store-provision breadcrumb. |
47
- | [hosting-sites.md](references/hosting-sites.md) | Deploy a static or Next.js site to Cloudflare Pages via `wrangler pages deploy`. |
47
+ | [hosting-sites.md](references/hosting-sites.md) | Deploy a static or Next.js site to Cloudflare Pages via `wrangler pages deploy`. House/single-tenant path: on a multi-tenant install a client account is denied the `pages` scope (`reason=not-house`) and publishes with `storage-broker:storage-pages-deploy` instead. |
48
48
  | [web-analytics.md](references/web-analytics.md) | Why a registered site reports 0 visitors — the RUM API is blocked (no permission group), registered ≠ collecting, edge auto-injection is impossible for a Pages-only custom domain, and the beacon must be injected into the site HTML, rebuilt, and redeployed. Outcome: beacon present in live HTML. |
49
49
  | [dashboard-guide.md](references/dashboard-guide.md) | Click-paths for operations the operator prefers to do by hand — sign in, switch accounts, add a site, edit an apex CNAME, verify nameservers, delete a tunnel, manage CNAME records, author an Access policy. |
50
50
  | [reset-guide.md](references/reset-guide.md) | Decision tree for reset vs. patch, the exact `pkill` incantation for token-mode connectors, and the dashboard cleanup paths for stray records and rogue entries. |
@@ -1,14 +1,14 @@
1
- import { initStderrTee } from "../../../../lib/mcp-stderr-tee/dist/index.js";
2
- initStderrTee("cloudflare");
3
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
3
  // The Cloudflare plugin exposes zero agent-facing tools. Every
6
4
  // Cloudflare operation is the agent invoking `cloudflared` directly via
7
5
  // the Bash tool, following the runbook in
8
6
  // platform/plugins/cloudflare/references/manual-setup.md, with
9
- // cloudflared stdout/stderr streamed verbatim into the PTY. The MCP
10
- // server process still spawns for lifecycle and stderr-tee consistency
11
- // with other plugins, but its tool list is empty by design.
7
+ // cloudflared stdout/stderr streamed verbatim into the PTY. The plugin
8
+ // declares `mcp-manifest: skip` and no `mcp:` block, so this entry is never
9
+ // spawned as an MCP server: only the generated .claude-plugin/plugin.json
10
+ // mcpServers block makes Claude Code spawn one (tool-registration-drift.ts).
11
+ // Task 1721 removed its unreachable stderr-tee call.
12
12
  const server = new McpServer({
13
13
  name: "cloudflare",
14
14
  version: "0.4.0",
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,8CAA8C,CAAC;AAC7E,aAAa,CAAC,YAAY,CAAC,CAAC;AAE5B,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AAEjF,+DAA+D;AAC/D,wEAAwE;AACxE,0CAA0C;AAC1C,+DAA+D;AAC/D,oEAAoE;AACpE,uEAAuE;AACvE,4DAA4D;AAE5D,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC;IAC3B,IAAI,EAAE,YAAY;IAClB,OAAO,EAAE,OAAO;CACjB,CAAC,CAAC;AAEH,KAAK,UAAU,IAAI;IACjB,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;IAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;AAClC,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IACnB,OAAO,CAAC,KAAK,CAAC,2BAA2B,GAAG,EAAE,CAAC,CAAC;IAChD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AAEjF,+DAA+D;AAC/D,wEAAwE;AACxE,0CAA0C;AAC1C,+DAA+D;AAC/D,uEAAuE;AACvE,4EAA4E;AAC5E,0EAA0E;AAC1E,6EAA6E;AAC7E,qDAAqD;AAErD,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC;IAC3B,IAAI,EAAE,YAAY;IAClB,OAAO,EAAE,OAAO;CACjB,CAAC,CAAC;AAEH,KAAK,UAAU,IAAI;IACjB,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;IAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;AAClC,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IACnB,OAAO,CAAC,KAAK,CAAC,2BAA2B,GAAG,EAAE,CAAC,CAAC;IAChD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
@@ -33,6 +33,8 @@ If the site captures form data, add the `[[d1_databases]]` binding from `d1-data
33
33
 
34
34
  ## 3. Load the reused deploy token
35
35
 
36
+ > **On a multi-tenant install, a client account does not run this step at all.** After the 1631 storage isolation, a client's `cloudflare.env` carries no master, and `cf-token.sh` denies it the `pages` scope (`reason=not-house`) — correctly, since that master is account-wide. A client account publishes with the `storage-pages-deploy` tool, which runs the deploy house-side; the project must already be recorded to the account by the house (`storage-pages-adopt`), because the project-to-folder mapping is not derivable and is never guessed. The rest of this section applies to the **house account and single-tenant installs**, where the master still lives in the account secrets file. If you are a client account and you reached a `not-house` denial here, that is this branch, not a broken token: use the tool.
37
+
36
38
  A Pages deploy needs **Account · Cloudflare Pages · Edit**. If the site has Pages Functions hitting D1, the token also needs **Account · D1 · Edit** (the single most common breakage — see `d1-data-capture.md`). The `pages` scope resolves the stable `<brand>-pages-d1` token, which carries both. Resolve it with the deterministic helper, which mints it once only if absent and reuses it thereafter. This is the one command; do not source the secrets file and hand the resulting `CLOUDFLARE_API_TOKEN` to `wrangler` — that variable holds the **master**, not the per-scope token, and `wrangler pages` then returns `Authentication error [code: 10000]`:
37
39
 
38
40
  ```bash
@@ -56,7 +56,7 @@ The booking-specific schema ships with the template: `schema.sql` creates both t
56
56
 
57
57
  ## Deploy through site-deploy
58
58
 
59
- Hand the assembled tree to `cloudflare:site-deploy`: it builds nothing for a static tree, runs `wrangler pages deploy public --project-name <project> --branch=main`, attaches the booking domain (creating the DNS record with the `<brand>-dns` token when the attach leaves validation pending), and reuses the same per-scope token. Everything in `site-deploy`'s auth, custom-domain, and DNS guidance applies unchanged.
59
+ Hand the assembled tree to `cloudflare:site-deploy`: it builds nothing for a static tree, runs `wrangler pages deploy public --project-name <project> --branch=main`, attaches the booking domain (creating the DNS record with the `<brand>-dns` token when the attach leaves validation pending), and reuses the same per-scope token. Everything in `site-deploy`'s auth, custom-domain, and DNS guidance applies unchanged — including its multi-tenant branch: "admin-run" means a `role:"admin"` session, which on a multi-tenant install is **not** the same thing as the house. A client account's admin session is denied the `pages` scope (`reason=not-house`) and publishes with `storage-pages-deploy` instead.
60
60
 
61
61
  ## Done-gate
62
62
 
@@ -33,6 +33,8 @@ There is never a `sites/<brand>/` versus `output/<brand>-site/` pair, a `.bak` c
33
33
 
34
34
  ## Auth: reuse the per-scope token, never mint ad hoc
35
35
 
36
+ > **Client accounts on a multi-tenant install do not use this section.** Post-1631 their secrets carry no master and `cf-token.sh` denies them the `pages` scope (`reason=not-house`). They publish with `storage-pages-deploy`, which deploys house-side against a project the house has recorded to them (`storage-pages-adopt`). A `not-house` denial here is that boundary working, not a token fault — do not try to mint around it. Everything below is the **house/single-tenant** path.
37
+
36
38
  Resolve the Pages-and-D1 token with the deterministic helper **before** any token `curl`, never a hand-built one:
37
39
 
38
40
  ```bash