@rubytech/create-maxy-code 0.1.283 → 0.1.285

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 (83) hide show
  1. package/dist/__tests__/installer-settings-permissions.test.js +86 -8
  2. package/dist/index.js +1 -1
  3. package/dist/permissions-seed.js +72 -11
  4. package/package.json +1 -1
  5. package/payload/platform/plugins/admin/mcp/dist/__tests__/skill-search.test.js +3 -3
  6. package/payload/platform/plugins/admin/mcp/dist/__tests__/skill-search.test.js.map +1 -1
  7. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +5 -1
  8. package/payload/platform/plugins/business-assistant/skills/e-sign/SKILL.md +28 -4
  9. package/payload/platform/plugins/cloudflare/references/api.md +2 -1
  10. package/payload/platform/plugins/docs/references/admin-ui.md +2 -0
  11. package/payload/platform/plugins/docs/references/platform.md +2 -0
  12. package/payload/server/public/assets/AdminShell-DwPVr9K1.js +1 -0
  13. package/payload/server/public/assets/{Checkbox-syHoU9nY.js → Checkbox-BdDmGlvK.js} +1 -1
  14. package/payload/server/public/assets/{admin-DyuZ4NT5.js → admin-BYtGQJ0d.js} +1 -1
  15. package/payload/server/public/assets/{architectureDiagram-Q4EWVU46-DsionCio.js → architectureDiagram-Q4EWVU46-CBx_GoUY.js} +1 -1
  16. package/payload/server/public/assets/{blockDiagram-DXYQGD6D-NOG5yVZk.js → blockDiagram-DXYQGD6D-DvWR4fpv.js} +1 -1
  17. package/payload/server/public/assets/{browser-XrVAUQdG.js → browser-2PXsTraZ.js} +1 -1
  18. package/payload/server/public/assets/{c4Diagram-AHTNJAMY-DwFV1k9I.js → c4Diagram-AHTNJAMY-BbSOxeeU.js} +1 -1
  19. package/payload/server/public/assets/channel-C4NqYS8u.js +1 -0
  20. package/payload/server/public/assets/{chunk-336JU56O-iJ4ISPCi.js → chunk-336JU56O-DJzmXTk5.js} +2 -2
  21. package/payload/server/public/assets/{chunk-426QAEUC-sWe0Iexz.js → chunk-426QAEUC-BSwzskM5.js} +1 -1
  22. package/payload/server/public/assets/{chunk-4TB4RGXK-kKNYi2_i.js → chunk-4TB4RGXK-C-fH8BTZ.js} +1 -1
  23. package/payload/server/public/assets/{chunk-5FUZZQ4R-Car3r0he.js → chunk-5FUZZQ4R-DW-MaiGX.js} +1 -1
  24. package/payload/server/public/assets/{chunk-5PVQY5BW-DR9EfLvj.js → chunk-5PVQY5BW-DlKbwC1X.js} +1 -1
  25. package/payload/server/public/assets/{chunk-EDXVE4YY-DLUWVRZV.js → chunk-EDXVE4YY-DnkdxwAt.js} +1 -1
  26. package/payload/server/public/assets/{chunk-ENJZ2VHE-B4bNJo6C.js → chunk-ENJZ2VHE-BTfezWsj.js} +1 -1
  27. package/payload/server/public/assets/{chunk-ICPOFSXX-Qzl8Ki0l.js → chunk-ICPOFSXX-BGg8uTtu.js} +1 -1
  28. package/payload/server/public/assets/{chunk-OYMX7WX6-BenBO6O_.js → chunk-OYMX7WX6-CYJInav8.js} +1 -1
  29. package/payload/server/public/assets/{chunk-U2HBQHQK-DewM8R0D.js → chunk-U2HBQHQK-B6kD6ZHU.js} +1 -1
  30. package/payload/server/public/assets/{chunk-X2U36JSP-C22Q6Ea4.js → chunk-X2U36JSP-7tvWkjym.js} +1 -1
  31. package/payload/server/public/assets/{chunk-YZCP3GAM-DfUUNTnA.js → chunk-YZCP3GAM-D6NjOb5l.js} +1 -1
  32. package/payload/server/public/assets/{chunk-ZZ45TVLE-CUT6zma2.js → chunk-ZZ45TVLE-Dqz69eJy.js} +1 -1
  33. package/payload/server/public/assets/classDiagram-6PBFFD2Q-CI3_kz04.js +1 -0
  34. package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-UP3RcKH9.js +1 -0
  35. package/payload/server/public/assets/clone-CvAPweMB.js +1 -0
  36. package/payload/server/public/assets/{dagre-KV5264BT-D4EGb60h.js → dagre-KV5264BT-DswETLwm.js} +1 -1
  37. package/payload/server/public/assets/{dagre-CYXRIdv0.js → dagre-di-KHpsq.js} +1 -1
  38. package/payload/server/public/assets/{data-tcu3MXmK.js → data-B6vpe0Zj.js} +1 -1
  39. package/payload/server/public/assets/{diagram-5BDNPKRD-BzieCEfT.js → diagram-5BDNPKRD-DWmq_Zkl.js} +1 -1
  40. package/payload/server/public/assets/{diagram-G4DWMVQ6-DDbO71XD.js → diagram-G4DWMVQ6-BVi6yjLL.js} +1 -1
  41. package/payload/server/public/assets/{diagram-MMDJMWI5-DqQ_UXDA.js → diagram-MMDJMWI5-D6XKyUAT.js} +1 -1
  42. package/payload/server/public/assets/{diagram-TYMM5635-CFyxA1aZ.js → diagram-TYMM5635-sU1d7P6W.js} +1 -1
  43. package/payload/server/public/assets/{erDiagram-SMLLAGMA-BMaEBbOK.js → erDiagram-SMLLAGMA-CY6qPZvu.js} +1 -1
  44. package/payload/server/public/assets/{flowDiagram-DWJPFMVM-De5ChZQP.js → flowDiagram-DWJPFMVM-CE9hd869.js} +1 -1
  45. package/payload/server/public/assets/{ganttDiagram-T4ZO3ILL-CXEMPVaK.js → ganttDiagram-T4ZO3ILL-Cbb885gt.js} +1 -1
  46. package/payload/server/public/assets/{gitGraphDiagram-UUTBAWPF-OZedr_6y.js → gitGraphDiagram-UUTBAWPF-BNzO3jBu.js} +1 -1
  47. package/payload/server/public/assets/{graph-I4GyC2MW.js → graph-DfegJdnT.js} +1 -1
  48. package/payload/server/public/assets/{graph-labels-DSDTSBfM.js → graph-labels-BG4aqzs6.js} +1 -1
  49. package/payload/server/public/assets/{graphlib-CN4nhD2U.js → graphlib-CIzKPoW_.js} +1 -1
  50. package/payload/server/public/assets/{infoDiagram-42DDH7IO-CSqg4nRL.js → infoDiagram-42DDH7IO-DWGtofAT.js} +1 -1
  51. package/payload/server/public/assets/{ishikawaDiagram-UXIWVN3A-ChlzrZyx.js → ishikawaDiagram-UXIWVN3A-UG1rus6G.js} +1 -1
  52. package/payload/server/public/assets/{journeyDiagram-VCZTEJTY-DsrK55ZO.js → journeyDiagram-VCZTEJTY-CYhHYXmj.js} +1 -1
  53. package/payload/server/public/assets/{kanban-definition-6JOO6SKY-DYF-_Rym.js → kanban-definition-6JOO6SKY-CWHj9DHX.js} +1 -1
  54. package/payload/server/public/assets/{line-3zmwI8XK.js → line-vMN9CmXx.js} +1 -1
  55. package/payload/server/public/assets/{mermaid-parser.core-BrcX8-cl.js → mermaid-parser.core-5X7WWCZe.js} +1 -1
  56. package/payload/server/public/assets/{mermaid.core-CBdz3b1N.js → mermaid.core-B0zUZsT6.js} +3 -3
  57. package/payload/server/public/assets/{mindmap-definition-QFDTVHPH-BqCqlYZb.js → mindmap-definition-QFDTVHPH-BGr68ynR.js} +1 -1
  58. package/payload/server/public/assets/{pieDiagram-DEJITSTG-D5LiILuM.js → pieDiagram-DEJITSTG-Bo3zU_w2.js} +1 -1
  59. package/payload/server/public/assets/{public-udi_Z7la.js → public-DCZ92LR9.js} +3 -3
  60. package/payload/server/public/assets/{quadrantDiagram-34T5L4WZ-C3iuI5l_.js → quadrantDiagram-34T5L4WZ-o-dLMRj-.js} +1 -1
  61. package/payload/server/public/assets/{requirementDiagram-MS252O5E-CTPOw5qQ.js → requirementDiagram-MS252O5E-D_O_ZclX.js} +1 -1
  62. package/payload/server/public/assets/{sankeyDiagram-XADWPNL6-D_5aVlYp.js → sankeyDiagram-XADWPNL6-Doz7uWIn.js} +1 -1
  63. package/payload/server/public/assets/{sequenceDiagram-FGHM5R23-D7h5vocM.js → sequenceDiagram-FGHM5R23-r1VW_jgc.js} +1 -1
  64. package/payload/server/public/assets/{stateDiagram-FHFEXIEX-DyZjuwPj.js → stateDiagram-FHFEXIEX-B7O06aua.js} +1 -1
  65. package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-Bo1g66pA.js +1 -0
  66. package/payload/server/public/assets/{timeline-definition-GMOUNBTQ-BqpVYJz_.js → timeline-definition-GMOUNBTQ-WS2AL0yL.js} +1 -1
  67. package/payload/server/public/assets/{useSelectionMode-BnRcJeg2.css → useSelectionMode-lFraIQzT.css} +1 -1
  68. package/payload/server/public/assets/{vennDiagram-DHZGUBPP-Czflml0e.js → vennDiagram-DHZGUBPP-_-FLcVAh.js} +1 -1
  69. package/payload/server/public/assets/{wardleyDiagram-NUSXRM2D-Cqz3pSTK.js → wardleyDiagram-NUSXRM2D-nZ3oO0ih.js} +1 -1
  70. package/payload/server/public/assets/{xychartDiagram-5P7HB3ND-CS5dEuUO.js → xychartDiagram-5P7HB3ND-CqTdRM37.js} +1 -1
  71. package/payload/server/public/browser.html +4 -4
  72. package/payload/server/public/data.html +5 -5
  73. package/payload/server/public/graph.html +6 -6
  74. package/payload/server/public/index.html +5 -5
  75. package/payload/server/public/public.html +4 -4
  76. package/payload/server/server.js +236 -155
  77. package/payload/server/public/assets/AdminShell-8Y7-maNi.js +0 -1
  78. package/payload/server/public/assets/channel-D8Go_zJk.js +0 -1
  79. package/payload/server/public/assets/classDiagram-6PBFFD2Q-BLb1mzrT.js +0 -1
  80. package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-Ctj5qYTS.js +0 -1
  81. package/payload/server/public/assets/clone-DGZ2vydA.js +0 -1
  82. package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-BiW9pGXy.js +0 -1
  83. /package/payload/server/public/assets/{useSelectionMode-BPzPjKP_.js → useSelectionMode-mzxTOyBv.js} +0 -0
@@ -7,30 +7,51 @@
7
7
  // the rc-spawn bind — Task 664). Existing top-level keys must be preserved;
8
8
  // an already-correct file must be a true no-op (mtime unchanged); a
9
9
  // "*"-bearing file must be rewritten on upgrade.
10
+ //
11
+ // Task 739 — the same writer also seeds an `autoMode` block so that when a
12
+ // session runs in `auto` (not bypass) mode, the harness auto-classifier
13
+ // treats this install's own Cloudflare token mint/persist as sanctioned
14
+ // (it otherwise soft-denies it as a credential escalation). The block is
15
+ // belt-and-suspenders: bypassPermissions skips the classifier entirely, so
16
+ // autoMode only takes effect on a session toggled to auto at runtime. A
17
+ // pre-739 file (permissions correct, no autoMode) must upgrade in place;
18
+ // a file with both blocks correct must be a true no-op.
10
19
  import test from "node:test";
11
20
  import assert from "node:assert/strict";
12
21
  import { mkdtempSync, readFileSync, writeFileSync, statSync, rmSync } from "node:fs";
13
22
  import { tmpdir } from "node:os";
14
23
  import { join } from "node:path";
15
- import { seedBypassPermissionsSettings, assertBypassPermissionsSeed, BYPASS_PERMISSIONS_BLOCK } from "../permissions-seed.js";
24
+ import { seedBypassPermissionsSettings, assertBypassPermissionsSeed, BYPASS_PERMISSIONS_BLOCK, AUTO_MODE_BLOCK } from "../permissions-seed.js";
16
25
  function freshDir() {
17
26
  return mkdtempSync(join(tmpdir(), "t583-"));
18
27
  }
19
- test("empty target: creates settings.json with the permissions block", () => {
28
+ // A plain (non-frozen) deep copy of the seeded autoMode block, for building
29
+ // already-correct fixtures and deep-equal assertions.
30
+ function autoModeFixture() {
31
+ return { environment: [...AUTO_MODE_BLOCK.environment], allow: [...AUTO_MODE_BLOCK.allow] };
32
+ }
33
+ test("empty target: creates settings.json with both the permissions and autoMode blocks", () => {
20
34
  const dir = freshDir();
21
35
  try {
22
36
  const result = seedBypassPermissionsSettings(dir);
23
37
  assert.equal(result.action, "seeded");
38
+ assert.equal(result.autoMode, "seeded");
24
39
  assert.equal(result.path, join(dir, "settings.json"));
25
40
  const parsed = JSON.parse(readFileSync(result.path, "utf-8"));
26
- assert.deepEqual(parsed, { permissions: { defaultMode: "bypassPermissions", allow: [], deny: [] } });
41
+ assert.deepEqual(parsed, {
42
+ permissions: { defaultMode: "bypassPermissions", allow: [], deny: [] },
43
+ autoMode: autoModeFixture(),
44
+ });
45
+ // "$defaults" must lead both arrays so built-in guards stay inherited.
46
+ assert.equal(parsed.autoMode.environment[0], "$defaults");
47
+ assert.equal(parsed.autoMode.allow[0], "$defaults");
27
48
  assert.equal(assertBypassPermissionsSeed(dir).status, "ok");
28
49
  }
29
50
  finally {
30
51
  rmSync(dir, { recursive: true, force: true });
31
52
  }
32
53
  });
33
- test("existing keys preserved verbatim; permissions added", () => {
54
+ test("existing keys preserved verbatim; permissions and autoMode added", () => {
34
55
  const dir = freshDir();
35
56
  try {
36
57
  const settingsPath = join(dir, "settings.json");
@@ -51,17 +72,49 @@ test("existing keys preserved verbatim; permissions added", () => {
51
72
  assert.equal(parsed.agentPushNotifEnabled, false);
52
73
  assert.equal(parsed.model, "opus");
53
74
  assert.deepEqual(parsed.permissions, { defaultMode: "bypassPermissions", allow: [], deny: [] });
75
+ assert.deepEqual(parsed.autoMode, autoModeFixture());
54
76
  assert.equal(assertBypassPermissionsSeed(dir).status, "ok");
55
77
  }
56
78
  finally {
57
79
  rmSync(dir, { recursive: true, force: true });
58
80
  }
59
81
  });
60
- test("idempotent: already-correct block triggers no write", () => {
82
+ test("pre-739 upgrade: permissions correct but no autoMode adds autoMode, preserves other keys", () => {
61
83
  const dir = freshDir();
62
84
  try {
63
85
  const settingsPath = join(dir, "settings.json");
64
- writeFileSync(settingsPath, JSON.stringify({ model: "opus", permissions: { defaultMode: "bypassPermissions", allow: [], deny: [] } }, null, 2));
86
+ // A file written by the Task 664 seed: permissions correct, no autoMode.
87
+ const preTask739 = {
88
+ model: "opus",
89
+ enabledPlugins: { "memory@claude-plugins-official": true },
90
+ permissions: { defaultMode: "bypassPermissions", allow: [], deny: [] },
91
+ };
92
+ writeFileSync(settingsPath, JSON.stringify(preTask739, null, 2));
93
+ const result = seedBypassPermissionsSettings(dir);
94
+ assert.equal(result.action, "seeded");
95
+ assert.equal(result.autoMode, "seeded", "this run added the autoMode block");
96
+ const parsed = JSON.parse(readFileSync(settingsPath, "utf-8"));
97
+ // permissions and unrelated keys untouched.
98
+ assert.deepEqual(parsed.permissions, { defaultMode: "bypassPermissions", allow: [], deny: [] });
99
+ assert.equal(parsed.model, "opus");
100
+ assert.deepEqual(parsed.enabledPlugins, preTask739.enabledPlugins);
101
+ // autoMode now present and correct.
102
+ assert.deepEqual(parsed.autoMode, autoModeFixture());
103
+ assert.equal(assertBypassPermissionsSeed(dir).status, "ok");
104
+ }
105
+ finally {
106
+ rmSync(dir, { recursive: true, force: true });
107
+ }
108
+ });
109
+ test("idempotent: both blocks already correct triggers no write", () => {
110
+ const dir = freshDir();
111
+ try {
112
+ const settingsPath = join(dir, "settings.json");
113
+ writeFileSync(settingsPath, JSON.stringify({
114
+ model: "opus",
115
+ permissions: { defaultMode: "bypassPermissions", allow: [], deny: [] },
116
+ autoMode: autoModeFixture(),
117
+ }, null, 2));
65
118
  const before = statSync(settingsPath).mtimeMs;
66
119
  // Small busy-wait so a same-millisecond write would be detectable on
67
120
  // filesystems with millisecond mtime granularity.
@@ -69,6 +122,7 @@ test("idempotent: already-correct block triggers no write", () => {
69
122
  while (Date.now() < target) { /* spin */ }
70
123
  const result = seedBypassPermissionsSettings(dir);
71
124
  assert.equal(result.action, "noop");
125
+ assert.equal(result.autoMode, "present");
72
126
  const after = statSync(settingsPath).mtimeMs;
73
127
  assert.equal(after, before, "mtime must not change on noop");
74
128
  }
@@ -76,13 +130,16 @@ test("idempotent: already-correct block triggers no write", () => {
76
130
  rmSync(dir, { recursive: true, force: true });
77
131
  }
78
132
  });
79
- test("malformed JSON: overwrites with the seeded block", () => {
133
+ test("malformed JSON: overwrites with both seeded blocks", () => {
80
134
  const dir = freshDir();
81
135
  try {
82
136
  const settingsPath = join(dir, "settings.json");
83
137
  writeFileSync(settingsPath, "{ not json");
84
138
  const result = seedBypassPermissionsSettings(dir);
85
139
  assert.equal(result.action, "seeded");
140
+ assert.equal(result.autoMode, "seeded");
141
+ const parsed = JSON.parse(readFileSync(settingsPath, "utf-8"));
142
+ assert.deepEqual(parsed.autoMode, autoModeFixture());
86
143
  assert.equal(assertBypassPermissionsSeed(dir).status, "ok");
87
144
  }
88
145
  finally {
@@ -108,12 +165,32 @@ test("assert: partial block (no wildcard) reports missing", () => {
108
165
  rmSync(dir, { recursive: true, force: true });
109
166
  }
110
167
  });
168
+ test("assert: permissions correct but autoMode absent reports missing", () => {
169
+ const dir = freshDir();
170
+ try {
171
+ writeFileSync(join(dir, "settings.json"), JSON.stringify({ permissions: { defaultMode: "bypassPermissions", allow: [], deny: [] } }));
172
+ assert.equal(assertBypassPermissionsSeed(dir).status, "missing");
173
+ }
174
+ finally {
175
+ rmSync(dir, { recursive: true, force: true });
176
+ }
177
+ });
111
178
  test("BYPASS_PERMISSIONS_BLOCK frozen literal matches expected shape", () => {
112
179
  assert.equal(BYPASS_PERMISSIONS_BLOCK.defaultMode, "bypassPermissions");
113
180
  assert.deepEqual([...BYPASS_PERMISSIONS_BLOCK.allow], []);
114
181
  assert.deepEqual([...BYPASS_PERMISSIONS_BLOCK.deny], []);
115
182
  assert.ok(Object.isFrozen(BYPASS_PERMISSIONS_BLOCK));
116
183
  });
184
+ test("AUTO_MODE_BLOCK frozen literal leads each array with $defaults and names Cloudflare", () => {
185
+ assert.ok(Object.isFrozen(AUTO_MODE_BLOCK));
186
+ assert.equal(AUTO_MODE_BLOCK.environment[0], "$defaults");
187
+ assert.equal(AUTO_MODE_BLOCK.allow[0], "$defaults");
188
+ // The provisioning and persistence clauses are the sanctioned-flow surface.
189
+ assert.ok(AUTO_MODE_BLOCK.allow.some((e) => /Cloudflare token provisioning/.test(e)));
190
+ assert.ok(AUTO_MODE_BLOCK.allow.some((e) => /Cloudflare token persistence/.test(e)));
191
+ // No machine-specific absolute path is baked in.
192
+ assert.ok(!AUTO_MODE_BLOCK.environment.some((e) => e.includes("/home/")));
193
+ });
117
194
  test("assert: wildcard allow reports missing (forces rewrite on upgrade)", () => {
118
195
  const dir = freshDir();
119
196
  try {
@@ -124,7 +201,7 @@ test("assert: wildcard allow reports missing (forces rewrite on upgrade)", () =>
124
201
  rmSync(dir, { recursive: true, force: true });
125
202
  }
126
203
  });
127
- test("seeding over a wildcard-bearing file rewrites allow to [] preserving other keys", () => {
204
+ test("seeding over a wildcard-bearing file rewrites allow to [] and adds autoMode preserving other keys", () => {
128
205
  const dir = freshDir();
129
206
  try {
130
207
  const p = join(dir, "settings.json");
@@ -134,6 +211,7 @@ test("seeding over a wildcard-bearing file rewrites allow to [] preserving other
134
211
  const parsed = JSON.parse(readFileSync(p, "utf-8"));
135
212
  assert.equal(parsed.model, "opus");
136
213
  assert.deepEqual(parsed.permissions, { defaultMode: "bypassPermissions", allow: [], deny: [] });
214
+ assert.deepEqual(parsed.autoMode, autoModeFixture());
137
215
  assert.equal(assertBypassPermissionsSeed(dir).status, "ok");
138
216
  }
139
217
  finally {
package/dist/index.js CHANGED
@@ -995,7 +995,7 @@ function installClaudeCode() {
995
995
  // through to the Anthropic remote auto-classifier (which routes Agent
996
996
  // dispatch to `ask` and surfaces a prompt no unattended operator clicks).
997
997
  const permissionsSeed = seedBypassPermissionsSettings(CLAUDE_CONFIG_DIR);
998
- logFile(`[install-permissions] action=${permissionsSeed.action} path=${permissionsSeed.path}`);
998
+ logFile(`[install-permissions] action=${permissionsSeed.action} autoMode=${permissionsSeed.autoMode} path=${permissionsSeed.path}`);
999
999
  const marketplaceList = spawnSync("claude", ["plugin", "marketplace", "list"], { stdio: "pipe", encoding: "utf-8", env: claudePluginEnv() });
1000
1000
  if (marketplaceList.stderr)
1001
1001
  process.stderr.write(marketplaceList.stderr);
@@ -13,9 +13,24 @@
13
13
  // stdout bind banner never prints and every rc-spawn times out (Task 664).
14
14
  // The allow array is therefore empty and MUST stay "*"-free.
15
15
  //
16
+ // Task 739 — this module also seeds an `autoMode` block. When a session is
17
+ // toggled to `auto` permission mode at runtime (a shift-tab in claude.ai/code,
18
+ // leaving bypass), the harness auto-classifier wakes up and soft-denies the
19
+ // doctrine-sanctioned Cloudflare token mint/persist as a credential
20
+ // escalation, because the install's own Cloudflare infrastructure is not in
21
+ // the classifier's trusted environment out of the box. The autoMode block
22
+ // declares that infrastructure trusted and the scoped token mint/persist
23
+ // sanctioned, with `"$defaults"` retained first in both arrays so the
24
+ // built-in exfiltration / force-push / prod-deploy guards stay intact. It is
25
+ // belt-and-suspenders: bypassPermissions skips the classifier entirely, so
26
+ // this block only takes effect when a session has left bypass for auto. The
27
+ // classifier reads `autoMode` only from user-scope settings (this file),
28
+ // never from a shared project settings.json, so this is the only seed surface.
29
+ //
16
30
  // This module is the single writer for the brand-scoped settings.json
17
- // permissions block. Account-scoped settings.json is written by
18
- // platform/scripts/setup-account.sh in lockstep with the same shape.
31
+ // permissions and autoMode blocks. Account-scoped settings.json is written by
32
+ // platform/scripts/setup-account.sh in lockstep with the permissions block
33
+ // (the classifier ignores autoMode there, so it carries no autoMode block).
19
34
  import { readFileSync, writeFileSync, renameSync, existsSync, mkdirSync } from "node:fs";
20
35
  import { join, dirname } from "node:path";
21
36
  export const BYPASS_PERMISSIONS_BLOCK = Object.freeze({
@@ -23,7 +38,29 @@ export const BYPASS_PERMISSIONS_BLOCK = Object.freeze({
23
38
  allow: [],
24
39
  deny: [],
25
40
  });
26
- function isAlreadyCorrect(permissions) {
41
+ // Task 739 — the auto-mode trust block. Each array leads with "$defaults" so
42
+ // the harness's built-in environment scope and allow rules are inherited, not
43
+ // replaced; the appended entries add this install's own Cloudflare surface on
44
+ // top. The prose is the v3 block validated on the laptop with
45
+ // `claude auto-mode config`/`critique` (2.1.169), with the one laptop-specific
46
+ // absolute path removed in favour of a relationship phrase so the block ships
47
+ // identically to every brand and machine. `<id>` stays a literal path segment:
48
+ // this brand-scoped file is shared by every account in the install, so the
49
+ // placeholder correctly denotes "the requesting account's own dir" while the
50
+ // "never cross-account" clause does the enforcement.
51
+ export const AUTO_MODE_BLOCK = Object.freeze({
52
+ environment: Object.freeze([
53
+ "$defaults",
54
+ "Organization: a Maxy Code admin agent that operates this single brand's OWN infrastructure. This install's own working tree and the per-account data dirs under data/accounts/<id>/ are its own scope.",
55
+ "api.cloudflare.com is the intended provider endpoint for Cloudflare API tokens held in this install. Sending those tokens to api.cloudflare.com is a Standard Credentials action. api.cloudflare.com is NOT a trusted destination for repo content, secrets, or bulk file uploads — the Data Exfiltration hard block applies normally to any such content (private-source files, secrets, Workers scripts or D1 dumps that embed secrets, whole-tree uploads).",
56
+ ]),
57
+ allow: Object.freeze([
58
+ "$defaults",
59
+ "Cloudflare token provisioning: minting a Cloudflare API token for THIS account's own zone/account, scoped to one of {Zone DNS edit, Pages, D1, Workers Scripts, Access apps/policies}, is sanctioned provisioning for the install's own infrastructure. Covered: POST to /accounts/{id}/tokens or /zones/{zone}/tokens where the request body's policies / permission-groups are visible in the transcript and limited to the named scopes, plus listing or revoking this account's own tokens. If the scope cannot be verified from the transcript, or is broader than the named scopes (e.g. account-wide Edit or Global API Key), treat as a Permission Grant soft block. Token creation where the agent inferred the scope from tool output rather than explicit instruction is NOT covered.",
60
+ "Cloudflare token persistence: writing this account's own narrowly-scoped Cloudflare API tokens (as minted above) to that same account's secrets directory data/accounts/<id>/secrets/ is intended local persistence, not Credential Leakage — scoped to this account's own Cloudflare service tokens, never to any .claude/ config path, and never cross-account.",
61
+ ]),
62
+ });
63
+ function isPermissionsCorrect(permissions) {
27
64
  if (!permissions || typeof permissions !== "object")
28
65
  return false;
29
66
  const p = permissions;
@@ -37,6 +74,24 @@ function isAlreadyCorrect(permissions) {
37
74
  return false;
38
75
  return true;
39
76
  }
77
+ function stringArraysEqual(a, b) {
78
+ if (!Array.isArray(a) || a.length !== b.length)
79
+ return false;
80
+ return a.every((v, i) => v === b[i]);
81
+ }
82
+ function isAutoModeCorrect(autoMode) {
83
+ if (!autoMode || typeof autoMode !== "object")
84
+ return false;
85
+ const a = autoMode;
86
+ return (stringArraysEqual(a.environment, AUTO_MODE_BLOCK.environment) &&
87
+ stringArraysEqual(a.allow, AUTO_MODE_BLOCK.allow));
88
+ }
89
+ function freshAutoModeBlock() {
90
+ return {
91
+ environment: [...AUTO_MODE_BLOCK.environment],
92
+ allow: [...AUTO_MODE_BLOCK.allow],
93
+ };
94
+ }
40
95
  export function seedBypassPermissionsSettings(configDir) {
41
96
  const settingsPath = join(configDir, "settings.json");
42
97
  let existing = {};
@@ -51,20 +106,26 @@ export function seedBypassPermissionsSettings(configDir) {
51
106
  }
52
107
  catch {
53
108
  // Malformed JSON: treat as empty and overwrite with a well-formed
54
- // file containing only the permissions block. Any keys the prior
55
- // file carried are unrecoverable from invalid JSON anyway.
109
+ // file containing only the permissions and autoMode blocks. Any keys
110
+ // the prior file carried are unrecoverable from invalid JSON anyway.
56
111
  }
57
112
  }
58
113
  }
59
- if (isAlreadyCorrect(existing.permissions)) {
60
- return { action: "noop", path: settingsPath };
114
+ const permissionsOk = isPermissionsCorrect(existing.permissions);
115
+ const autoModeOk = isAutoModeCorrect(existing.autoMode);
116
+ if (permissionsOk && autoModeOk) {
117
+ return { action: "noop", autoMode: "present", path: settingsPath };
61
118
  }
62
119
  mkdirSync(dirname(settingsPath), { recursive: true });
63
- const merged = { ...existing, permissions: { ...BYPASS_PERMISSIONS_BLOCK, allow: [...BYPASS_PERMISSIONS_BLOCK.allow], deny: [] } };
120
+ const merged = {
121
+ ...existing,
122
+ permissions: { ...BYPASS_PERMISSIONS_BLOCK, allow: [...BYPASS_PERMISSIONS_BLOCK.allow], deny: [] },
123
+ autoMode: freshAutoModeBlock(),
124
+ };
64
125
  const tmpPath = `${settingsPath}.task583.tmp`;
65
126
  writeFileSync(tmpPath, JSON.stringify(merged, null, 2) + "\n");
66
127
  renameSync(tmpPath, settingsPath);
67
- return { action: "seeded", path: settingsPath };
128
+ return { action: "seeded", autoMode: autoModeOk ? "present" : "seeded", path: settingsPath };
68
129
  }
69
130
  export function assertBypassPermissionsSeed(configDir) {
70
131
  const settingsPath = join(configDir, "settings.json");
@@ -79,8 +140,8 @@ export function assertBypassPermissionsSeed(configDir) {
79
140
  }
80
141
  if (!parsed || typeof parsed !== "object")
81
142
  return { status: "missing", path: settingsPath };
82
- const permissions = parsed.permissions;
83
- return isAlreadyCorrect(permissions)
143
+ const record = parsed;
144
+ return isPermissionsCorrect(record.permissions) && isAutoModeCorrect(record.autoMode)
84
145
  ? { status: "ok", path: settingsPath }
85
146
  : { status: "missing", path: settingsPath };
86
147
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-maxy-code",
3
- "version": "0.1.283",
3
+ "version": "0.1.285",
4
4
  "description": "Install Maxy — AI for Productive People",
5
5
  "bin": {
6
6
  "create-maxy-code": "./dist/index.js"
@@ -24,19 +24,19 @@ function seedSkill(plugin, skill, body) {
24
24
  const fm = (name, description) => `---\nname: ${name}\ndescription: ${description}\n---\n\n# ${name}\n`;
25
25
  describe("searchSkills", () => {
26
26
  it("description-only match returns the owning plugin + canonical slug path top-ranked", () => {
27
- seedSkill("quotation", "quotation", fm("quotation", "Use when an operator's own way of pricing jobs needs to become something the assistant can apply — set up my pricing, learn how I quote, re-learning their method from past quotes."));
27
+ seedSkill("siteoffice-job", "quotation", fm("quotation", "Use when an operator's own way of pricing jobs needs to become something the assistant can apply — set up my pricing, learn how I quote, re-learning their method from past quotes."));
28
28
  seedSkill("admin", "qr-code", fm("qr-code", "Generate QR codes. Trigger phrases: create a QR, make a QR code."));
29
29
  seedSkill("admin", "datetime", fm("datetime", "Timezone queries and relative-date arithmetic."));
30
30
  const hits = searchSkills(root, "pricing algorithm");
31
31
  expect(hits.length).toBeGreaterThan(0);
32
- expect(hits[0].pluginName).toBe("quotation");
32
+ expect(hits[0].pluginName).toBe("siteoffice-job");
33
33
  expect(hits[0].skillName).toBe("quotation");
34
34
  expect(hits[0].file).toBe("skills/quotation/SKILL.md");
35
35
  });
36
36
  it("a common trigger word does not let a noise skill outrank the distinctive match", () => {
37
37
  // "create" is a noise term (appears in many descriptions); "quote" is
38
38
  // distinctive. quotation must still win for "create a quote".
39
- seedSkill("quotation", "quotation", fm("quotation", "Onboarding a new business's pricing, re-learning their method from past quotes, checking the method reproduces past quotes."));
39
+ seedSkill("siteoffice-job", "quotation", fm("quotation", "Onboarding a new business's pricing, re-learning their method from past quotes, checking the method reproduces past quotes."));
40
40
  seedSkill("admin", "qr-code", fm("qr-code", "Create a QR code. Trigger: create a QR, create a code."));
41
41
  seedSkill("admin", "publish-site", fm("publish-site", "Create and publish a static site online."));
42
42
  seedSkill("admin", "deck-pages", fm("deck-pages", "Create a slide deck of pages."));
@@ -1 +1 @@
1
- {"version":3,"file":"skill-search.test.js","sourceRoot":"","sources":["../../src/__tests__/skill-search.test.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"skill-search.test.js","sourceRoot":"","sources":["../../src/__tests__/skill-search.test.ts"],"names":[],"mappings":"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"}
@@ -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:5dae80dcc0e3d36e755dcf26570c4d7c8893b6198b8cd769b3e5c6c1c79169f5
4
+ content-hash: sha256:cc48ff74e2c0782fd1d36ad2f280495df8124e1bb5e3fff9a3d777db7c48b3de
5
5
  brand: maxy-code
6
6
  product-name: Maxy
7
7
  ---
@@ -310,6 +310,8 @@ If you suspect background processes are piling up, run `grep '\[reaper\]' ~/.{br
310
310
 
311
311
  Every install seeds `permissions.allow:[]` plus `defaultMode:"bypassPermissions"` into both the brand-scoped settings file (`~/.{brand}/.claude/settings.json`) and every account-scoped one (`<install>/data/accounts/<id>/.claude/settings.json`). `bypassPermissions` alone stops Claude Code from sending tool calls to its remote auto-classifier (it skips all permission checks), which would otherwise surface a permission prompt in the chat that an unattended session never answers. The `allow` array stays empty and **must never contain `"*"`**: Claude Code ≥ 2.1.167 rejects `"*"` as an allow token and renders a blocking Settings Warning that, under `--remote-control`, stops every `/rc-spawn` from binding (Task 664). What each subagent is allowed to use is still controlled by the `tools:` line in its agent file, not by a top-level allowlist. To verify after an install: `cat ~/.{brand}/.claude/settings.json | jq '.permissions'` (expect `allow: []`). To repair an install seeded before Task 664: `bash <install>/platform/scripts/backfill-bypass-permissions.sh ~/.{brand}`.
312
312
 
313
+ **`autoMode` trust block (Task 739).** The same brand-scoped writer ([`permissions-seed.ts`](../../../packages/create-maxy-code/src/permissions-seed.ts), `AUTO_MODE_BLOCK`) also seeds the harness `autoMode` block. `bypassPermissions` skips the auto-mode classifier entirely, so the block is **belt-and-suspenders**: it only matters when a session is toggled into `auto` mode, where the classifier otherwise soft-denies the doctrine-sanctioned Cloudflare scoped-token mint ("permission/credential escalation") because the install's own Cloudflare infra is not in the classifier's default trusted environment. The block keeps `"$defaults"` first in `environment` and `allow` (so the built-in Data-Exfiltration / force-push / prod-deploy guards stay intact) and adds prose declaring `api.cloudflare.com` the install's own provider endpoint plus an allow for minting/persisting narrow per-scope tokens. It is seeded **only** into the brand/user settings (`~/.{brand}/.claude/settings.json`) — the classifier never reads `autoMode` from the shared project (account-scoped) settings, so `setup-account.sh` does not carry it. An auto-mode agent cannot seed this itself (the default `hard_deny` "Auto-Mode Bypass" blocks any agent editing `.claude/settings*.json`); installer seed is the only path. To verify after an install: `CLAUDE_CONFIG_DIR=~/.{brand}/.claude claude auto-mode config | jq '.environment, .allow'` (expect the Cloudflare entries, `$defaults` expanded) and `jq '.autoMode' ~/.{brand}/.claude/settings.json`.
314
+
313
315
  ---
314
316
  # Plugins Guide
315
317
  Source: https://docs.getmaxy.com/plugins-guide.md
@@ -2441,6 +2443,8 @@ authoritative. This section names the surfaces and what backs each.
2441
2443
  | Mode trigger | Per-`(accountId, userId)` `SpawnPreference` for `permissionMode` and `model`; persists across reload, tab, device. | `session-defaults.ts` |
2442
2444
  | Nav rows (Chat / People / Agents / Projects / Tasks / Artefacts) | People, Agents, Tasks open the artefact-pane Graph filtered to the matching label. Chat selects the active conversation. Artefacts swaps the list to editable documents. | `graph-search.ts`, `graph-subgraph.ts`, `sidebar-artefacts.ts` |
2443
2445
  | Sessions list (Active / Archived / All) | Live row store driven by SSE; manual reconcile button on the segmented control re-fetches the full id set. | `/claude-sessions/events`, `/claude-sessions` |
2446
+ | Channel reader nav rows (WhatsApp / Telegram) | One conditional nav row **per interactive channel that has ≥1 admin session** (Task 738). Each row carries its brand glyph (WhatsApp `#25D366`, Telegram `#229ED9`); a channel with no admin session shows no row, and no install ever shows an always-empty "No conversations" row. The channel list loads **once on sidebar mount** (the per-channel counts must be known before the nav renders), so a failed load shows one muted, non-clickable line instead of channel rows. Clicking a channel row lists that channel's admin conversations; each conversation row shows the **operator display name** (`Person givenName familyName`, resolved server-side from `senderId`; falls back to the raw `senderId` when unbound/unresolved), with the agent session title as the hover tooltip only. | `/whatsapp-reader/conversations` |
2447
+ | Channel transcript (`<Transcript>`) | Reads one session's JSONL over SSE (`/whatsapp-reader/stream`) and renders both sides plus tool calls — the turns claude.ai/code hides because channel inbound is stamped `isMeta`. The thread scrolls inside its grid cell (header/sidebar/footer fixed). The stream sends a 20 s heartbeat so an idle Cloudflare tunnel does not idle-drop, tags each frame with a byte-offset `id:`, and resumes from the client's `Last-Event-ID` on reconnect (no duplicate backlog); the client clears the "Stream disconnected" banner on reopen and latches it only on a terminal CLOSED state. | `/whatsapp-reader/stream` |
2444
2448
  | Conversations row hover actions | Inline rename, archive, delete, JSONL view / download per row. The historical `.conversations-modal` CSS block exists in `globals.css` but is no longer mounted from any TSX — Sidebar.tsx now owns every per-row affordance directly. | `claude-sessions.ts` |
2445
2449
  | Artefacts list | Lists every `:FileArtifact` under this account's tree (`relativePath STARTS WITH 'accounts/'`, all file types, excluding the `uploads/<id>/` subtree — Task 684) plus this account's IDENTITY / SOUL / KNOWLEDGE / specialist templates. Click downloads the row's backing file (`downloadPath` → `GET /api/admin/files/download`) so the operator opens it in their local app; rows whose file is outside `DATA_ROOT` (bundled-fallback templates) show a "can't be downloaded" pill. The in-app artefact pane is dead pending removal (Task 518). | `sidebar-artefacts.ts`, `files.ts` |
2446
2450
  | System-stats widget | CPU / RAM widget at the foot of the sidebar. | `system-stats.ts` (see above) |
@@ -18,7 +18,7 @@ It composes two patterns you must read alongside it:
18
18
  - **`cloudflare/references/d1-data-capture.md`** — the form → Pages Function → D1 → sweep mechanics, and the token-scope breakage (§ 0). Everything about minting the Pages-+-D1 token, `wrangler.toml`, and `wrangler d1 execute --remote` lives there; this skill does not repeat it. Note its rule that the D1 **query** endpoint rejects a D1-Read token — every `wrangler d1 execute` here uses a **D1-Edit** token, reads included.
19
19
  - **`business-assistant/references/invoicing.md`** — PDF generation via `browser-navigate` + `browser-pdf-save` (its step 4). The **deploy-time base render** (§ 5) uses that exact pattern. Dispatch (§ 6) does **not** — it stamps the persisted base with a PDF library, no browser and no network.
20
20
 
21
- Auth for every `wrangler`/API call is a minted narrow token per `cloudflare/references/api.md` — detect the master's type (`cfat_…` account-scoped vs `cfut_…` user-scoped) and route endpoints by prefix per that reference. Deploy mechanics are `cloudflare/references/hosting-sites.md`.
21
+ Auth for every `wrangler`/API call is the **reused per-scope narrow token** of `cloudflare/references/api.md` (one deterministic token per scope minted once if absent, persisted to the secrets file, then loaded and reused; Task 732), routed by the master's type (`cfat_…` account-scoped vs `cfut_…` user-scoped) by prefix per that reference. Deploy mechanics are `cloudflare/references/hosting-sites.md`.
22
22
 
23
23
  ---
24
24
 
@@ -91,7 +91,9 @@ CLOUDFLARE_API_TOKEN="${MINTED_ACCESS}" curl -sS \
91
91
  | jq '.result | length'
92
92
  ```
93
93
 
94
- **Token scope.** Gating needs one minted narrow token (per `api.md` § Minting a narrow token, routed by prefix with mint→verify backoff as § 8.8 notes) carrying **Access application + policy write** *and* **`Access: Identity Providers Write`**. Resolve those permission-group ids the same way `d1-data-capture.md` § 0 resolves the Pages/D1 ids — one `GET …/tokens/permission_groups`. Token *creation* stays an account-level concern; this skill names the scopes and consumes whatever token type the account supplies. `${MINTED_ACCESS}` below is that token.
94
+ **Token scope.** Gating needs the **reused per-scope Access token** (`<brand>-access` — minted once if absent, persisted, then loaded and reused per `api.md` § Provisioning and reusing a stable per-scope token; routed by prefix, with the mint→verify backoff of § 8.8 applying only on the mint-once path) carrying **Access application + policy write** *and* **`Access: Identity Providers Write`**. Resolve those permission-group ids the same way `d1-data-capture.md` § 0 resolves the Pages/D1 ids — one `GET …/tokens/permission_groups` — but **"Access: Apps and Policies Write" can return duplicate ids** from that call, and minting with the wrong variant poisons the token: it then returns `10000` on **every** call, **reads included**. So **verify the token with a read before relying on it** — a `GET …/access/apps` that returns `10000` is the wrong-id signature (distinct from § 8.8's transient post-mint `10000`, which clears within ~30s); re-resolve and mint with the other variant. Token *creation* stays an account-level concern; this skill names the scopes and consumes whatever token type the account supplies. `${MINTED_ACCESS}` below is that token.
95
+
96
+ **Access-app updates are `PUT`, not `PATCH`.** When you update an existing Access app — most often to widen its destinations — send `PUT …/access/apps/{app_id}` with the **full app body** (re-supply `name`, `destinations`, `type`, `session_duration`, …). `PATCH` returns `10405 "Method not allowed for this authentication scheme"` under API-token auth; there is no working partial-update verb (`api.md` § Access).
95
97
 
96
98
  **Create IdP → app → policy, in that order:**
97
99
 
@@ -103,7 +105,7 @@ CLOUDFLARE_API_TOKEN="${MINTED_ACCESS}" curl -sS \
103
105
  --data '{"name":"One-time PIN","type":"onetimepin"}' | jq '{ok:.success, type:.result.type}'
104
106
  ```
105
107
 
106
- 2. **Create the Access application** scoped to the deployed page's hostname **and** path (the `<project>.pages.dev` host and the signing document's path), so the gate covers exactly that document.
108
+ 2. **Create the Access application** scoped to the deployed host's **document path(s) `and` `/api/*`** the signing document's path(s) plus the capture endpoints `POST /api/accept` and `GET /api/status` (§ 4). Leave `/` and the static assets **public**. Gating only the document path is a **bypass**: with `/api/*` open, anyone can `POST /api/accept` and forge an acceptance, and `GET /api/status` is readable ungated. Leaving `/` public is deliberate and load-bearing — it is exactly what lets the neutral root portal (below) load **without a PIN**; the document path(s) and `/api/*` are the only gated destinations.
107
109
  3. **Attach a policy** whose `include` allowlist names the **signer's email** (and the business owner's). Only those addresses can authenticate and view the page.
108
110
 
109
111
  The IdP call shapes are given inline above; the Access **app** and **policy** endpoints live in `api.md`'s endpoint map, and the permission-group ids are resolved by the `api.md` method (`GET …/tokens/permission_groups`).
@@ -112,7 +114,9 @@ The IdP call shapes are given inline above; the Access **app** and **policy** en
112
114
 
113
115
  **Dispatch is unaffected (§ 6).** The sweep stamps the **persisted base** (`base.pdf`, § 1), never by fetching the gated page, so a gate never turns a dispatched "PDF" into an Access login screen — § 8.3 makes that structural. Keep it in mind whenever a gated document is swept.
114
116
 
115
- **Root landing pagea neutral portal, required for every gated site.** The § 1 project ships only the document page(s), so there is **no `/` page**. Cloudflare Access routinely lands a visitor on the **bare root `/`**: after login it does not always deep-link back to the requested path, and its logout endpoint (`/cdn-cgi/access/logout`) returns the user to `/?__cf_access_message=logged_out`. With no `/` page both land on a **404** a client-facing portal showing a raw 404 the moment a signer logs out. So a gated site **must** ship a root `index.html`. It is a **neutral portal**, not a redirect: a single host can carry **many** gated documents (each its own slug/path/`DOC_REF` § 3's D1 table is shared across a brand's documents, and § 1a scopes the Access app to host **and** path), and they all share one `/`. The root **cannot know which document** a visitor came from Access provides **no logout `returnTo`/redirect parameter**, so the root cannot route by origin — so it must name no document at all.
117
+ **Logout links carry `returnTo` point them at the public root (the primary loop-fix).** Cloudflare Access **does** honour a logout redirect: `GET /cdn-cgi/access/logout?returnTo=<https origin root>` 302s straight to that root and **strips** the stale `__cf_access_message` param. The parameter is **camelCase `returnTo` only** `return_to` and `redirect_url` are ignored. So build every signer-facing logout link as `/cdn-cgi/access/logout?returnTo=https://<host>/`, with `returnTo` pointing at the **public root**, never at the gated document path (which would just re-challenge for a PIN). A logout link built this way lands the signer cleanly on the public root with no stale paramthis is what actually breaks the "enter PIN logged-out screen" loop.
118
+
119
+ **Root landing page — a neutral portal, the belt-and-suspenders backstop.** The § 1 project ships only the document page(s), so there is **no `/` page** unless you add one. Even with `returnTo` available, Cloudflare Access still occasionally lands a visitor on the **bare root `/`** — a logout link that omitted `returnTo`, or a post-login that did not deep-link back to the requested path — and with no `/` page that is a **404** on a client-facing portal. So a gated site **must** ship a root `index.html` as the backstop for those bare-root landings. It is a **neutral portal**, not a redirect: a single host can carry **many** gated documents (each its own slug/path/`DOC_REF` — § 3's D1 table is shared across a brand's documents, and § 1a gates each document's path **and** `/api/*`), and they all share one `/`. The root names **no** document — logout links point `returnTo` at the bare root, not at any document path — so a single portal serving every document's bare-root landing must name none.
116
120
 
117
121
  ```html
118
122
  <!doctype html>
@@ -483,6 +487,8 @@ When the durable routine (§ 8.2) runs, or when the operator asks for new accept
483
487
 
484
488
  The acceptance flow is proven end-to-end on the **live** Pages deployment — the D1 row, not the POST status, is the contract. Every `wrangler d1 execute` uses the **D1-Edit** token (reads included; the query endpoint rejects D1 Read):
485
489
 
490
+ **When the document is Access-gated (§ 1a), `/api/*` is gated too** — so the `curl` POSTs to `/api/accept` and the `GET /api/status` below are themselves **Access-challenged (302 to login), not direct 200s**. Run this end-to-end verification either **before** the gate is applied, or from a context that can authenticate to Access — the same authenticated-session requirement § 5's base render already carries. The `wrangler d1 execute` rows reach D1 through the Cloudflare API, not the gated host, so they verify unchanged.
491
+
486
492
  ```bash
487
493
  # 1. POST a first acceptance to the live Function.
488
494
  curl -sS -X POST -H "Content-Type: application/json" \
@@ -587,6 +593,8 @@ A hand-built document ships the old bare "agree to terms" checkbox (no intent/co
587
593
 
588
594
  A `cfut_…` master at an account endpoint returns `9109`; a freshly-minted token returns `10000` for a few seconds. Both are explicit, reproducible API responses — `api.md` § Token type routes by prefix and § verify-with-backoff absorbs the `10000` window. Diagnostic path: the verify call's JSON `errors[].code`.
589
595
 
596
+ **A `10000` that does *not* clear is a different failure — the wrong permission-group id.** The "Access: Apps and Policies Write" group can return **duplicate ids** from `GET …/tokens/permission_groups` (§ 1a), and minting with the wrong variant poisons the Access token: it returns `10000` on **every** call, **reads included**, and never clears. The transient post-mint `10000` clears within ~30s of backoff; a `10000` that **persists on a `GET …/access/apps` read** is the poisoned-token signature — re-resolve the permission-group id and mint with the other variant. This is why § 1a verifies a freshly-minted Access token with a read before relying on it.
597
+
590
598
  ### 8.9 Access gate active with zero enabled IdPs (no-event lockout)
591
599
 
592
600
  Only relevant when the document is Access-gated (§ 1a). An Access app is active on the page while the org has **no** identity provider, so the login screen offers no method and everyone — operator included — is locked out. It is a **no-event** failure: the gate emits no log and does not reproduce until someone tries to log in, by which point they are already locked out. Detection is a **standing audit reconciling active Access apps against enabled login methods**:
@@ -629,3 +637,19 @@ Two distinct regressions of the § 1a neutral portal, both caught by the § 8.10
629
637
  - **Sticky-param interstitial.** The root **branches on `__cf_access_message=logged_out`** — showing a "signed out" interstitial when the param is present. Because Cloudflare preserves that param through the **entire re-login redirect**, a re-authenticated signer is landed back on `/` with the **stale** param and shown a logged-out screen *while logged in*. Signature: any reference to `__cf_access_message` in the root. The fix is the neutral portal — it makes no auth-state claim, so it is correct in every state.
630
638
 
631
639
  The neutral portal must therefore contain **no** document-specific path **and no** auth-state claim; it shows the same content unconditionally.
640
+
641
+ ### 8.12 Per-deployment preview URLs ungated (confidential-document leak, no-event)
642
+
643
+ Only relevant when the document is Access-gated (§ 1a). A host-scoped Access app covers the **canonical host** but **not** Cloudflare's per-deployment hostnames: every Pages deploy also serves at `https://<hash>.<project>.pages.dev/…`, and that hostname is **not** behind the host-scoped gate. Confirmed on a live gated build: `https://<hash>.<project>.pages.dev/<doc-path>` served **200, ungated**, while the canonical host was gated — so a confidential document leaks via any deployment-hash URL. It is a **no-event** failure: the leaked read logs nothing and does not reproduce until someone holds a preview URL. Remedy — one of:
644
+
645
+ - **Disable preview deployments** for the project, so only the production host exists; or
646
+ - **Gate `*.pages.dev`** with an Access app, so the per-deployment hostnames are challenged too.
647
+
648
+ Signal — a deploy-time / standing check tied to gating: for every gated project, assert preview deployments are disabled (or `*.pages.dev` is gated), then confirm by fetching one live `<hash>.<project>.pages.dev/<doc-path>` and asserting it is **challenged (302 to the Access login), not 200**:
649
+
650
+ ```bash
651
+ # A 200 here is the leak. Expect a 302 to the Access login.
652
+ curl -sS -o /dev/null -w '%{http_code}\n' "https://<hash>.<project>.pages.dev/<doc-path>"
653
+ ```
654
+
655
+ A project with preview deployments enabled and an Access app scoped only to the canonical host is the failure signature.
@@ -186,7 +186,7 @@ The endpoints below are the **account-scoped** (`cfat_…`) family — the commo
186
186
  - `GET /accounts/${ACC}/tokens` — list all tokens on the account (id, name, status, `expires_on`). Use it to find a per-scope token to reuse and to enumerate strays before a reconcile. User-scoped twin: `GET /user/tokens`.
187
187
  - `POST /accounts/${ACC}/tokens` — mint a per-scope token (see § Provisioning and reusing a stable per-scope token). Mint only when the list above shows the scope absent.
188
188
  - `DELETE /accounts/${ACC}/tokens/{id}` — revoke a token by id (the reconcile/cleanup verb). User-scoped twin: `DELETE /user/tokens/{id}`. Route by the master's prefix exactly as verify/mint do — `cfat_…` → `/accounts/${ACC}/tokens…`, `cfut_…` → `/user/tokens…`; the cross-type call returns `9109`.
189
- - `GET /accounts/${ACC}/tokens/permission_groups` — resolve permission-group ids for scoping.
189
+ - `GET /accounts/${ACC}/tokens/permission_groups` — resolve permission-group ids for scoping. **Caveat — some groups return duplicate ids.** A single named group (observed for **"Access: Apps and Policies Write"**) can appear **more than once** in this list under different ids. Minting with the wrong variant yields a token that returns `10000` on **every** call, **reads included** — a permanently poisoned token, not the transient post-mint `10000` window of § A freshly-minted token. **Verify a freshly-minted Access token with a read** (e.g. `GET /accounts/${ACC}/access/apps`) before relying on it: a `10000` on that read means the wrong permission-group id was used — re-resolve and mint with the other variant.
190
190
 
191
191
  ### DNS records (zone-scoped; needs **Zone · DNS · Edit**)
192
192
  - `GET /zones/${ZONE}/dns_records` — list / find a record id.
@@ -210,6 +210,7 @@ curl -sS -X POST -H "Authorization: Bearer ${TOKEN}" -H "Content-Type: applicati
210
210
 
211
211
  ### Access (Zero Trust) applications + policies
212
212
  - `POST /accounts/${ACC}/access/apps` and `POST /accounts/${ACC}/access/apps/{app_id}/policies` — author an Access application + policy programmatically. The dashboard click-path in `dashboard-guide.md` § "Author an Access policy" is the alternative for operators who prefer to click.
213
+ - `PUT /accounts/${ACC}/access/apps/{app_id}` — update an existing Access application (e.g. to widen its destinations). The body is the **full app object**, not a partial patch — re-supply `name`, `destinations`/`domain`, `type`, `session_duration`, and the rest, not just the changed fields. **`PATCH` does not work under API-token auth**: it returns `10405 "Method not allowed for this authentication scheme"`. Use `PUT` with the complete body; there is no working partial-update verb for this auth scheme.
213
214
 
214
215
  ---
215
216
 
@@ -216,6 +216,8 @@ authoritative. This section names the surfaces and what backs each.
216
216
  | Mode trigger | Per-`(accountId, userId)` `SpawnPreference` for `permissionMode` and `model`; persists across reload, tab, device. | `session-defaults.ts` |
217
217
  | Nav rows (Chat / People / Agents / Projects / Tasks / Artefacts) | People, Agents, Tasks open the artefact-pane Graph filtered to the matching label. Chat selects the active conversation. Artefacts swaps the list to editable documents. | `graph-search.ts`, `graph-subgraph.ts`, `sidebar-artefacts.ts` |
218
218
  | Sessions list (Active / Archived / All) | Live row store driven by SSE; manual reconcile button on the segmented control re-fetches the full id set. | `/claude-sessions/events`, `/claude-sessions` |
219
+ | Channel reader nav rows (WhatsApp / Telegram) | One conditional nav row **per interactive channel that has ≥1 admin session** (Task 738). Each row carries its brand glyph (WhatsApp `#25D366`, Telegram `#229ED9`); a channel with no admin session shows no row, and no install ever shows an always-empty "No conversations" row. The channel list loads **once on sidebar mount** (the per-channel counts must be known before the nav renders), so a failed load shows one muted, non-clickable line instead of channel rows. Clicking a channel row lists that channel's admin conversations; each conversation row shows the **operator display name** (`Person givenName familyName`, resolved server-side from `senderId`; falls back to the raw `senderId` when unbound/unresolved), with the agent session title as the hover tooltip only. | `/whatsapp-reader/conversations` |
220
+ | Channel transcript (`<Transcript>`) | Reads one session's JSONL over SSE (`/whatsapp-reader/stream`) and renders both sides plus tool calls — the turns claude.ai/code hides because channel inbound is stamped `isMeta`. The thread scrolls inside its grid cell (header/sidebar/footer fixed). The stream sends a 20 s heartbeat so an idle Cloudflare tunnel does not idle-drop, tags each frame with a byte-offset `id:`, and resumes from the client's `Last-Event-ID` on reconnect (no duplicate backlog); the client clears the "Stream disconnected" banner on reopen and latches it only on a terminal CLOSED state. | `/whatsapp-reader/stream` |
219
221
  | Conversations row hover actions | Inline rename, archive, delete, JSONL view / download per row. The historical `.conversations-modal` CSS block exists in `globals.css` but is no longer mounted from any TSX — Sidebar.tsx now owns every per-row affordance directly. | `claude-sessions.ts` |
220
222
  | Artefacts list | Lists every `:FileArtifact` under this account's tree (`relativePath STARTS WITH 'accounts/'`, all file types, excluding the `uploads/<id>/` subtree — Task 684) plus this account's IDENTITY / SOUL / KNOWLEDGE / specialist templates. Click downloads the row's backing file (`downloadPath` → `GET /api/admin/files/download`) so the operator opens it in their local app; rows whose file is outside `DATA_ROOT` (bundled-fallback templates) show a "can't be downloaded" pill. The in-app artefact pane is dead pending removal (Task 518). | `sidebar-artefacts.ts`, `files.ts` |
221
223
  | System-stats widget | CPU / RAM widget at the foot of the sidebar. | `system-stats.ts` (see above) |
@@ -195,3 +195,5 @@ If you suspect background processes are piling up, run `grep '\[reaper\]' ~/.{br
195
195
  ## Tool Permissions
196
196
 
197
197
  Every install seeds `permissions.allow:[]` plus `defaultMode:"bypassPermissions"` into both the brand-scoped settings file (`~/.{brand}/.claude/settings.json`) and every account-scoped one (`<install>/data/accounts/<id>/.claude/settings.json`). `bypassPermissions` alone stops Claude Code from sending tool calls to its remote auto-classifier (it skips all permission checks), which would otherwise surface a permission prompt in the chat that an unattended session never answers. The `allow` array stays empty and **must never contain `"*"`**: Claude Code ≥ 2.1.167 rejects `"*"` as an allow token and renders a blocking Settings Warning that, under `--remote-control`, stops every `/rc-spawn` from binding (Task 664). What each subagent is allowed to use is still controlled by the `tools:` line in its agent file, not by a top-level allowlist. To verify after an install: `cat ~/.{brand}/.claude/settings.json | jq '.permissions'` (expect `allow: []`). To repair an install seeded before Task 664: `bash <install>/platform/scripts/backfill-bypass-permissions.sh ~/.{brand}`.
198
+
199
+ **`autoMode` trust block (Task 739).** The same brand-scoped writer ([`permissions-seed.ts`](../../../packages/create-maxy-code/src/permissions-seed.ts), `AUTO_MODE_BLOCK`) also seeds the harness `autoMode` block. `bypassPermissions` skips the auto-mode classifier entirely, so the block is **belt-and-suspenders**: it only matters when a session is toggled into `auto` mode, where the classifier otherwise soft-denies the doctrine-sanctioned Cloudflare scoped-token mint ("permission/credential escalation") because the install's own Cloudflare infra is not in the classifier's default trusted environment. The block keeps `"$defaults"` first in `environment` and `allow` (so the built-in Data-Exfiltration / force-push / prod-deploy guards stay intact) and adds prose declaring `api.cloudflare.com` the install's own provider endpoint plus an allow for minting/persisting narrow per-scope tokens. It is seeded **only** into the brand/user settings (`~/.{brand}/.claude/settings.json`) — the classifier never reads `autoMode` from the shared project (account-scoped) settings, so `setup-account.sh` does not carry it. An auto-mode agent cannot seed this itself (the default `hard_deny` "Auto-Mode Bypass" blocks any agent editing `.claude/settings*.json`); installer seed is the only path. To verify after an install: `CLAUDE_CONFIG_DIR=~/.{brand}/.claude claude auto-mode config | jq '.environment, .allow'` (expect the Cloudflare entries, `$defaults` expanded) and `jq '.autoMode' ~/.{brand}/.claude/settings.json`.