@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.
- package/dist/__tests__/installer-settings-permissions.test.js +86 -8
- package/dist/index.js +1 -1
- package/dist/permissions-seed.js +72 -11
- package/package.json +1 -1
- package/payload/platform/plugins/admin/mcp/dist/__tests__/skill-search.test.js +3 -3
- package/payload/platform/plugins/admin/mcp/dist/__tests__/skill-search.test.js.map +1 -1
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +5 -1
- package/payload/platform/plugins/business-assistant/skills/e-sign/SKILL.md +28 -4
- package/payload/platform/plugins/cloudflare/references/api.md +2 -1
- package/payload/platform/plugins/docs/references/admin-ui.md +2 -0
- package/payload/platform/plugins/docs/references/platform.md +2 -0
- package/payload/server/public/assets/AdminShell-DwPVr9K1.js +1 -0
- package/payload/server/public/assets/{Checkbox-syHoU9nY.js → Checkbox-BdDmGlvK.js} +1 -1
- package/payload/server/public/assets/{admin-DyuZ4NT5.js → admin-BYtGQJ0d.js} +1 -1
- package/payload/server/public/assets/{architectureDiagram-Q4EWVU46-DsionCio.js → architectureDiagram-Q4EWVU46-CBx_GoUY.js} +1 -1
- package/payload/server/public/assets/{blockDiagram-DXYQGD6D-NOG5yVZk.js → blockDiagram-DXYQGD6D-DvWR4fpv.js} +1 -1
- package/payload/server/public/assets/{browser-XrVAUQdG.js → browser-2PXsTraZ.js} +1 -1
- package/payload/server/public/assets/{c4Diagram-AHTNJAMY-DwFV1k9I.js → c4Diagram-AHTNJAMY-BbSOxeeU.js} +1 -1
- package/payload/server/public/assets/channel-C4NqYS8u.js +1 -0
- package/payload/server/public/assets/{chunk-336JU56O-iJ4ISPCi.js → chunk-336JU56O-DJzmXTk5.js} +2 -2
- package/payload/server/public/assets/{chunk-426QAEUC-sWe0Iexz.js → chunk-426QAEUC-BSwzskM5.js} +1 -1
- package/payload/server/public/assets/{chunk-4TB4RGXK-kKNYi2_i.js → chunk-4TB4RGXK-C-fH8BTZ.js} +1 -1
- package/payload/server/public/assets/{chunk-5FUZZQ4R-Car3r0he.js → chunk-5FUZZQ4R-DW-MaiGX.js} +1 -1
- package/payload/server/public/assets/{chunk-5PVQY5BW-DR9EfLvj.js → chunk-5PVQY5BW-DlKbwC1X.js} +1 -1
- package/payload/server/public/assets/{chunk-EDXVE4YY-DLUWVRZV.js → chunk-EDXVE4YY-DnkdxwAt.js} +1 -1
- package/payload/server/public/assets/{chunk-ENJZ2VHE-B4bNJo6C.js → chunk-ENJZ2VHE-BTfezWsj.js} +1 -1
- package/payload/server/public/assets/{chunk-ICPOFSXX-Qzl8Ki0l.js → chunk-ICPOFSXX-BGg8uTtu.js} +1 -1
- package/payload/server/public/assets/{chunk-OYMX7WX6-BenBO6O_.js → chunk-OYMX7WX6-CYJInav8.js} +1 -1
- package/payload/server/public/assets/{chunk-U2HBQHQK-DewM8R0D.js → chunk-U2HBQHQK-B6kD6ZHU.js} +1 -1
- package/payload/server/public/assets/{chunk-X2U36JSP-C22Q6Ea4.js → chunk-X2U36JSP-7tvWkjym.js} +1 -1
- package/payload/server/public/assets/{chunk-YZCP3GAM-DfUUNTnA.js → chunk-YZCP3GAM-D6NjOb5l.js} +1 -1
- package/payload/server/public/assets/{chunk-ZZ45TVLE-CUT6zma2.js → chunk-ZZ45TVLE-Dqz69eJy.js} +1 -1
- package/payload/server/public/assets/classDiagram-6PBFFD2Q-CI3_kz04.js +1 -0
- package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-UP3RcKH9.js +1 -0
- package/payload/server/public/assets/clone-CvAPweMB.js +1 -0
- package/payload/server/public/assets/{dagre-KV5264BT-D4EGb60h.js → dagre-KV5264BT-DswETLwm.js} +1 -1
- package/payload/server/public/assets/{dagre-CYXRIdv0.js → dagre-di-KHpsq.js} +1 -1
- package/payload/server/public/assets/{data-tcu3MXmK.js → data-B6vpe0Zj.js} +1 -1
- package/payload/server/public/assets/{diagram-5BDNPKRD-BzieCEfT.js → diagram-5BDNPKRD-DWmq_Zkl.js} +1 -1
- package/payload/server/public/assets/{diagram-G4DWMVQ6-DDbO71XD.js → diagram-G4DWMVQ6-BVi6yjLL.js} +1 -1
- package/payload/server/public/assets/{diagram-MMDJMWI5-DqQ_UXDA.js → diagram-MMDJMWI5-D6XKyUAT.js} +1 -1
- package/payload/server/public/assets/{diagram-TYMM5635-CFyxA1aZ.js → diagram-TYMM5635-sU1d7P6W.js} +1 -1
- package/payload/server/public/assets/{erDiagram-SMLLAGMA-BMaEBbOK.js → erDiagram-SMLLAGMA-CY6qPZvu.js} +1 -1
- package/payload/server/public/assets/{flowDiagram-DWJPFMVM-De5ChZQP.js → flowDiagram-DWJPFMVM-CE9hd869.js} +1 -1
- package/payload/server/public/assets/{ganttDiagram-T4ZO3ILL-CXEMPVaK.js → ganttDiagram-T4ZO3ILL-Cbb885gt.js} +1 -1
- package/payload/server/public/assets/{gitGraphDiagram-UUTBAWPF-OZedr_6y.js → gitGraphDiagram-UUTBAWPF-BNzO3jBu.js} +1 -1
- package/payload/server/public/assets/{graph-I4GyC2MW.js → graph-DfegJdnT.js} +1 -1
- package/payload/server/public/assets/{graph-labels-DSDTSBfM.js → graph-labels-BG4aqzs6.js} +1 -1
- package/payload/server/public/assets/{graphlib-CN4nhD2U.js → graphlib-CIzKPoW_.js} +1 -1
- package/payload/server/public/assets/{infoDiagram-42DDH7IO-CSqg4nRL.js → infoDiagram-42DDH7IO-DWGtofAT.js} +1 -1
- package/payload/server/public/assets/{ishikawaDiagram-UXIWVN3A-ChlzrZyx.js → ishikawaDiagram-UXIWVN3A-UG1rus6G.js} +1 -1
- package/payload/server/public/assets/{journeyDiagram-VCZTEJTY-DsrK55ZO.js → journeyDiagram-VCZTEJTY-CYhHYXmj.js} +1 -1
- package/payload/server/public/assets/{kanban-definition-6JOO6SKY-DYF-_Rym.js → kanban-definition-6JOO6SKY-CWHj9DHX.js} +1 -1
- package/payload/server/public/assets/{line-3zmwI8XK.js → line-vMN9CmXx.js} +1 -1
- package/payload/server/public/assets/{mermaid-parser.core-BrcX8-cl.js → mermaid-parser.core-5X7WWCZe.js} +1 -1
- package/payload/server/public/assets/{mermaid.core-CBdz3b1N.js → mermaid.core-B0zUZsT6.js} +3 -3
- package/payload/server/public/assets/{mindmap-definition-QFDTVHPH-BqCqlYZb.js → mindmap-definition-QFDTVHPH-BGr68ynR.js} +1 -1
- package/payload/server/public/assets/{pieDiagram-DEJITSTG-D5LiILuM.js → pieDiagram-DEJITSTG-Bo3zU_w2.js} +1 -1
- package/payload/server/public/assets/{public-udi_Z7la.js → public-DCZ92LR9.js} +3 -3
- package/payload/server/public/assets/{quadrantDiagram-34T5L4WZ-C3iuI5l_.js → quadrantDiagram-34T5L4WZ-o-dLMRj-.js} +1 -1
- package/payload/server/public/assets/{requirementDiagram-MS252O5E-CTPOw5qQ.js → requirementDiagram-MS252O5E-D_O_ZclX.js} +1 -1
- package/payload/server/public/assets/{sankeyDiagram-XADWPNL6-D_5aVlYp.js → sankeyDiagram-XADWPNL6-Doz7uWIn.js} +1 -1
- package/payload/server/public/assets/{sequenceDiagram-FGHM5R23-D7h5vocM.js → sequenceDiagram-FGHM5R23-r1VW_jgc.js} +1 -1
- package/payload/server/public/assets/{stateDiagram-FHFEXIEX-DyZjuwPj.js → stateDiagram-FHFEXIEX-B7O06aua.js} +1 -1
- package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-Bo1g66pA.js +1 -0
- package/payload/server/public/assets/{timeline-definition-GMOUNBTQ-BqpVYJz_.js → timeline-definition-GMOUNBTQ-WS2AL0yL.js} +1 -1
- package/payload/server/public/assets/{useSelectionMode-BnRcJeg2.css → useSelectionMode-lFraIQzT.css} +1 -1
- package/payload/server/public/assets/{vennDiagram-DHZGUBPP-Czflml0e.js → vennDiagram-DHZGUBPP-_-FLcVAh.js} +1 -1
- package/payload/server/public/assets/{wardleyDiagram-NUSXRM2D-Cqz3pSTK.js → wardleyDiagram-NUSXRM2D-nZ3oO0ih.js} +1 -1
- package/payload/server/public/assets/{xychartDiagram-5P7HB3ND-CS5dEuUO.js → xychartDiagram-5P7HB3ND-CqTdRM37.js} +1 -1
- package/payload/server/public/browser.html +4 -4
- package/payload/server/public/data.html +5 -5
- package/payload/server/public/graph.html +6 -6
- package/payload/server/public/index.html +5 -5
- package/payload/server/public/public.html +4 -4
- package/payload/server/server.js +236 -155
- package/payload/server/public/assets/AdminShell-8Y7-maNi.js +0 -1
- package/payload/server/public/assets/channel-D8Go_zJk.js +0 -1
- package/payload/server/public/assets/classDiagram-6PBFFD2Q-BLb1mzrT.js +0 -1
- package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-Ctj5qYTS.js +0 -1
- package/payload/server/public/assets/clone-DGZ2vydA.js +0 -1
- package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-BiW9pGXy.js +0 -1
- /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
|
-
|
|
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, {
|
|
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("
|
|
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
|
-
|
|
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
|
|
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);
|
package/dist/permissions-seed.js
CHANGED
|
@@ -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
|
|
18
|
-
// platform/scripts/setup-account.sh in lockstep with the
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
60
|
-
|
|
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 = {
|
|
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
|
|
83
|
-
return
|
|
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
|
@@ -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("
|
|
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("
|
|
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("
|
|
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":"AAAA,gEAAgE;AAChE,EAAE;AACF,6EAA6E;AAC7E,4EAA4E;AAC5E,6EAA6E;AAC7E,8EAA8E;AAC9E,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AACrE,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACxE,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,YAAY,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAC;AAE7E,IAAI,IAAY,CAAC;AAEjB,UAAU,CAAC,GAAG,EAAE;IACd,IAAI,GAAG,WAAW,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,eAAe,CAAC,CAAC,CAAC;AACtD,CAAC,CAAC,CAAC;AAEH,SAAS,CAAC,GAAG,EAAE;IACb,MAAM,CAAC,IAAI,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;AAClD,CAAC,CAAC,CAAC;AAEH,SAAS,SAAS,CAAC,MAAc,EAAE,KAAa,EAAE,IAAY;IAC5D,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAC;IAC3D,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACpC,aAAa,CAAC,IAAI,CAAC,GAAG,EAAE,UAAU,CAAC,EAAE,IAAI,CAAC,CAAC;AAC7C,CAAC;AAED,MAAM,EAAE,GAAG,CAAC,IAAY,EAAE,WAAmB,EAAE,EAAE,CAAC,cAAc,IAAI,kBAAkB,WAAW,cAAc,IAAI,IAAI,CAAC;AAExH,QAAQ,CAAC,cAAc,EAAE,GAAG,EAAE;IAC5B,EAAE,CAAC,mFAAmF,EAAE,GAAG,EAAE;QAC3F,SAAS,CACP,
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
-
**
|
|
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 param — this 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`.
|