@rubytech/create-realagent-code 0.1.605 → 0.1.607

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 (207) hide show
  1. package/dist/__tests__/claude-trust-seed.test.js +125 -0
  2. package/dist/claude-trust-seed.js +52 -0
  3. package/dist/index.js +108 -34
  4. package/package.json +1 -1
  5. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +16 -1
  6. package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +9 -0
  7. package/payload/platform/plugins/docs/references/admin-ui.md +13 -0
  8. package/payload/platform/plugins/docs/references/platform.md +2 -0
  9. package/payload/platform/services/claude-session-manager/dist/claude-trust-census.d.ts +34 -0
  10. package/payload/platform/services/claude-session-manager/dist/claude-trust-census.d.ts.map +1 -0
  11. package/payload/platform/services/claude-session-manager/dist/claude-trust-census.js +83 -0
  12. package/payload/platform/services/claude-session-manager/dist/claude-trust-census.js.map +1 -0
  13. package/payload/platform/services/claude-session-manager/dist/index.js +13 -0
  14. package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
  15. package/payload/server/public/activity.html +6 -6
  16. package/payload/server/public/agents.html +5 -5
  17. package/payload/server/public/assets/AdminLoginScreens-DuQ3ax5U.js +1 -0
  18. package/payload/server/public/assets/AdminLoginScreens-DuQ3ax5U.js.br +0 -0
  19. package/payload/server/public/assets/AdminLoginScreens-DuQ3ax5U.js.gz +0 -0
  20. package/payload/server/public/assets/AdminShell-1HyATV_t.js +3 -0
  21. package/payload/server/public/assets/AdminShell-1HyATV_t.js.br +0 -0
  22. package/payload/server/public/assets/AdminShell-1HyATV_t.js.gz +0 -0
  23. package/payload/server/public/assets/activity-C2qWlcJ5.js +1 -0
  24. package/payload/server/public/assets/activity-C2qWlcJ5.js.br +0 -0
  25. package/payload/server/public/assets/activity-C2qWlcJ5.js.gz +0 -0
  26. package/payload/server/public/assets/admin-DqpfY8-t.js +1 -0
  27. package/payload/server/public/assets/admin-DqpfY8-t.js.br +0 -0
  28. package/payload/server/public/assets/admin-DqpfY8-t.js.gz +0 -0
  29. package/payload/server/public/assets/agents-DoGVAjIT.js +1 -0
  30. package/payload/server/public/assets/agents-DoGVAjIT.js.br +0 -0
  31. package/payload/server/public/assets/agents-DoGVAjIT.js.gz +0 -0
  32. package/payload/server/public/assets/{browser-CqDBcKwU.js → browser-DjqS897k.js} +1 -1
  33. package/payload/server/public/assets/browser-DjqS897k.js.br +0 -0
  34. package/payload/server/public/assets/browser-DjqS897k.js.gz +0 -0
  35. package/payload/server/public/assets/calendar-DJeOm0WS.js +1 -0
  36. package/payload/server/public/assets/calendar-DJeOm0WS.js.br +0 -0
  37. package/payload/server/public/assets/calendar-DJeOm0WS.js.gz +0 -0
  38. package/payload/server/public/assets/chat-BrRE0Dck.js +1 -0
  39. package/payload/server/public/assets/chat-BrRE0Dck.js.br +0 -0
  40. package/payload/server/public/assets/chat-BrRE0Dck.js.gz +0 -0
  41. package/payload/server/public/assets/chevron-left-D9z1aK3p.js +1 -0
  42. package/payload/server/public/assets/chevron-left-D9z1aK3p.js.br +0 -0
  43. package/payload/server/public/assets/clock-BqHWMICF.js +1 -0
  44. package/payload/server/public/assets/clock-BqHWMICF.js.br +0 -0
  45. package/payload/server/public/assets/clock-BqHWMICF.js.gz +0 -0
  46. package/payload/server/public/assets/copy-p9ZM9fnn.js +1 -0
  47. package/payload/server/public/assets/copy-p9ZM9fnn.js.br +0 -0
  48. package/payload/server/public/assets/copy-p9ZM9fnn.js.gz +0 -0
  49. package/payload/server/public/assets/data-CPsqW0qW.js +1 -0
  50. package/payload/server/public/assets/data-CPsqW0qW.js.br +0 -0
  51. package/payload/server/public/assets/data-CPsqW0qW.js.gz +0 -0
  52. package/payload/server/public/assets/{field-X4OVA3vx.js → field-WRk67lvr.js} +1 -1
  53. package/payload/server/public/assets/field-WRk67lvr.js.br +0 -0
  54. package/payload/server/public/assets/field-WRk67lvr.js.gz +0 -0
  55. package/payload/server/public/assets/file-text-BJDCb334.js +1 -0
  56. package/payload/server/public/assets/file-text-BJDCb334.js.br +0 -0
  57. package/payload/server/public/assets/file-text-BJDCb334.js.gz +0 -0
  58. package/payload/server/public/assets/{graph-l7tzHqvm.js → graph-CAU2xh2Z.js} +2 -2
  59. package/payload/server/public/assets/graph-CAU2xh2Z.js.br +0 -0
  60. package/payload/server/public/assets/graph-CAU2xh2Z.js.gz +0 -0
  61. package/payload/server/public/assets/graph-labels-CEDmX7qW.js +1 -0
  62. package/payload/server/public/assets/graph-labels-CEDmX7qW.js.br +0 -0
  63. package/payload/server/public/assets/graph-labels-CEDmX7qW.js.gz +0 -0
  64. package/payload/server/public/assets/{jsx-runtime-bq0g4NlA.css → jsx-runtime-hPFGo4rC.css} +1 -1
  65. package/payload/server/public/assets/jsx-runtime-hPFGo4rC.css.br +0 -0
  66. package/payload/server/public/assets/{jsx-runtime-bq0g4NlA.css.gz → jsx-runtime-hPFGo4rC.css.gz} +0 -0
  67. package/payload/server/public/assets/{operator-Dg-dwf7a.js → operator-1WFztEQA.js} +1 -1
  68. package/payload/server/public/assets/operator-1WFztEQA.js.br +0 -0
  69. package/payload/server/public/assets/operator-1WFztEQA.js.gz +0 -0
  70. package/payload/server/public/assets/page-B72XigcU.js +32 -0
  71. package/payload/server/public/assets/page-B72XigcU.js.br +0 -0
  72. package/payload/server/public/assets/page-B72XigcU.js.gz +0 -0
  73. package/payload/server/public/assets/page-nT8l_cX1.js +1 -0
  74. package/payload/server/public/assets/page-nT8l_cX1.js.br +0 -0
  75. package/payload/server/public/assets/page-nT8l_cX1.js.gz +0 -0
  76. package/payload/server/public/assets/play-CYqOCqm6.js +1 -0
  77. package/payload/server/public/assets/play-CYqOCqm6.js.br +0 -0
  78. package/payload/server/public/assets/play-CYqOCqm6.js.gz +0 -0
  79. package/payload/server/public/assets/public-DAea6w7w.js +1 -0
  80. package/payload/server/public/assets/public-DAea6w7w.js.br +0 -0
  81. package/payload/server/public/assets/public-DAea6w7w.js.gz +0 -0
  82. package/payload/server/public/assets/rotate-ccw-CqlRlGsY.js +1 -0
  83. package/payload/server/public/assets/rotate-ccw-CqlRlGsY.js.br +0 -0
  84. package/payload/server/public/assets/rotate-ccw-CqlRlGsY.js.gz +0 -0
  85. package/payload/server/public/assets/{routines-6fxZNacQ.js → routines-vy5xsyqx.js} +2 -2
  86. package/payload/server/public/assets/routines-vy5xsyqx.js.br +0 -0
  87. package/payload/server/public/assets/routines-vy5xsyqx.js.gz +0 -0
  88. package/payload/server/public/assets/{skills-51mqx-s5.js → skills-X7FTilVy.js} +1 -1
  89. package/payload/server/public/assets/skills-X7FTilVy.js.br +0 -0
  90. package/payload/server/public/assets/skills-X7FTilVy.js.gz +0 -0
  91. package/payload/server/public/assets/tasks-BXx_jk_p.js +3 -0
  92. package/payload/server/public/assets/tasks-BXx_jk_p.js.br +0 -0
  93. package/payload/server/public/assets/tasks-BXx_jk_p.js.gz +0 -0
  94. package/payload/server/public/assets/triangle-alert-CuWcGaRo.js +1 -0
  95. package/payload/server/public/assets/triangle-alert-CuWcGaRo.js.br +0 -0
  96. package/payload/server/public/assets/triangle-alert-CuWcGaRo.js.gz +0 -0
  97. package/payload/server/public/assets/{useCopyFeedback-BG7h37Rx.js → useCopyFeedback-DLSKs6r2.js} +1 -1
  98. package/payload/server/public/assets/useCopyFeedback-DLSKs6r2.js.br +0 -0
  99. package/payload/server/public/assets/useCopyFeedback-DLSKs6r2.js.gz +0 -0
  100. package/payload/server/public/assets/useMediaQuery-C_1CV_iW.js +6 -0
  101. package/payload/server/public/assets/useMediaQuery-C_1CV_iW.js.br +0 -0
  102. package/payload/server/public/assets/useMediaQuery-C_1CV_iW.js.gz +0 -0
  103. package/payload/server/public/assets/useVoiceRecorder-DBzM_HIx.js +2 -0
  104. package/payload/server/public/assets/useVoiceRecorder-DBzM_HIx.js.br +0 -0
  105. package/payload/server/public/assets/useVoiceRecorder-DBzM_HIx.js.gz +0 -0
  106. package/payload/server/public/assets/wrench-nDMQ6x-6.js +1 -0
  107. package/payload/server/public/assets/wrench-nDMQ6x-6.js.br +0 -0
  108. package/payload/server/public/assets/wrench-nDMQ6x-6.js.gz +0 -0
  109. package/payload/server/public/browser.html +5 -5
  110. package/payload/server/public/calendar.html +8 -8
  111. package/payload/server/public/chat.html +14 -14
  112. package/payload/server/public/data.html +12 -12
  113. package/payload/server/public/field.html +3 -3
  114. package/payload/server/public/graph.html +10 -10
  115. package/payload/server/public/index.html +14 -14
  116. package/payload/server/public/operator.html +15 -15
  117. package/payload/server/public/public.html +14 -14
  118. package/payload/server/public/routines.html +8 -8
  119. package/payload/server/public/skills.html +6 -6
  120. package/payload/server/public/tasks.html +7 -7
  121. package/payload/server/server.js +1 -1
  122. package/payload/server/public/assets/AdminLoginScreens-DI5bNlX8.js +0 -1
  123. package/payload/server/public/assets/AdminLoginScreens-DI5bNlX8.js.br +0 -0
  124. package/payload/server/public/assets/AdminLoginScreens-DI5bNlX8.js.gz +0 -0
  125. package/payload/server/public/assets/AdminShell-B-iX9ahJ.js +0 -3
  126. package/payload/server/public/assets/AdminShell-B-iX9ahJ.js.br +0 -0
  127. package/payload/server/public/assets/AdminShell-B-iX9ahJ.js.gz +0 -0
  128. package/payload/server/public/assets/activity-CqgFwANo.js +0 -1
  129. package/payload/server/public/assets/activity-CqgFwANo.js.br +0 -0
  130. package/payload/server/public/assets/activity-CqgFwANo.js.gz +0 -0
  131. package/payload/server/public/assets/admin-K63n6ybW.js +0 -1
  132. package/payload/server/public/assets/admin-K63n6ybW.js.br +0 -0
  133. package/payload/server/public/assets/admin-K63n6ybW.js.gz +0 -0
  134. package/payload/server/public/assets/agents-cue5Zf8X.js +0 -1
  135. package/payload/server/public/assets/agents-cue5Zf8X.js.br +0 -0
  136. package/payload/server/public/assets/agents-cue5Zf8X.js.gz +0 -0
  137. package/payload/server/public/assets/browser-CqDBcKwU.js.br +0 -0
  138. package/payload/server/public/assets/browser-CqDBcKwU.js.gz +0 -0
  139. package/payload/server/public/assets/calendar-BgzuW5Rt.js +0 -1
  140. package/payload/server/public/assets/calendar-BgzuW5Rt.js.br +0 -0
  141. package/payload/server/public/assets/calendar-BgzuW5Rt.js.gz +0 -0
  142. package/payload/server/public/assets/chat-HYZViu2u.js +0 -1
  143. package/payload/server/public/assets/chat-HYZViu2u.js.br +0 -3
  144. package/payload/server/public/assets/chat-HYZViu2u.js.gz +0 -0
  145. package/payload/server/public/assets/chevron-left-C5FXH9r_.js +0 -1
  146. package/payload/server/public/assets/chevron-left-C5FXH9r_.js.br +0 -0
  147. package/payload/server/public/assets/clock-DxPtyejC.js +0 -1
  148. package/payload/server/public/assets/clock-DxPtyejC.js.br +0 -0
  149. package/payload/server/public/assets/clock-DxPtyejC.js.gz +0 -0
  150. package/payload/server/public/assets/copy-BNhZ0flH.js +0 -1
  151. package/payload/server/public/assets/copy-BNhZ0flH.js.br +0 -0
  152. package/payload/server/public/assets/copy-BNhZ0flH.js.gz +0 -0
  153. package/payload/server/public/assets/data-kCBB8Z4Z.js +0 -1
  154. package/payload/server/public/assets/data-kCBB8Z4Z.js.br +0 -0
  155. package/payload/server/public/assets/data-kCBB8Z4Z.js.gz +0 -0
  156. package/payload/server/public/assets/field-X4OVA3vx.js.br +0 -0
  157. package/payload/server/public/assets/field-X4OVA3vx.js.gz +0 -0
  158. package/payload/server/public/assets/file-text-BddO_2EI.js +0 -1
  159. package/payload/server/public/assets/file-text-BddO_2EI.js.br +0 -0
  160. package/payload/server/public/assets/file-text-BddO_2EI.js.gz +0 -0
  161. package/payload/server/public/assets/graph-l7tzHqvm.js.br +0 -0
  162. package/payload/server/public/assets/graph-l7tzHqvm.js.gz +0 -0
  163. package/payload/server/public/assets/graph-labels-CtFFq5W2.js +0 -1
  164. package/payload/server/public/assets/graph-labels-CtFFq5W2.js.br +0 -0
  165. package/payload/server/public/assets/graph-labels-CtFFq5W2.js.gz +0 -0
  166. package/payload/server/public/assets/jsx-runtime-bq0g4NlA.css.br +0 -0
  167. package/payload/server/public/assets/operator-Dg-dwf7a.js.br +0 -7
  168. package/payload/server/public/assets/operator-Dg-dwf7a.js.gz +0 -0
  169. package/payload/server/public/assets/page-Ci1bfIbs.js +0 -1
  170. package/payload/server/public/assets/page-Ci1bfIbs.js.br +0 -0
  171. package/payload/server/public/assets/page-Ci1bfIbs.js.gz +0 -0
  172. package/payload/server/public/assets/page-Y9JmFq0Q.js +0 -32
  173. package/payload/server/public/assets/page-Y9JmFq0Q.js.br +0 -0
  174. package/payload/server/public/assets/page-Y9JmFq0Q.js.gz +0 -0
  175. package/payload/server/public/assets/play-BSZVGpQT.js +0 -1
  176. package/payload/server/public/assets/play-BSZVGpQT.js.br +0 -0
  177. package/payload/server/public/assets/play-BSZVGpQT.js.gz +0 -0
  178. package/payload/server/public/assets/public-m66FAza_.js +0 -1
  179. package/payload/server/public/assets/public-m66FAza_.js.br +0 -0
  180. package/payload/server/public/assets/public-m66FAza_.js.gz +0 -0
  181. package/payload/server/public/assets/rotate-ccw-DTOJmOxB.js +0 -1
  182. package/payload/server/public/assets/rotate-ccw-DTOJmOxB.js.br +0 -0
  183. package/payload/server/public/assets/rotate-ccw-DTOJmOxB.js.gz +0 -0
  184. package/payload/server/public/assets/routines-6fxZNacQ.js.br +0 -0
  185. package/payload/server/public/assets/routines-6fxZNacQ.js.gz +0 -0
  186. package/payload/server/public/assets/skills-51mqx-s5.js.br +0 -0
  187. package/payload/server/public/assets/skills-51mqx-s5.js.gz +0 -0
  188. package/payload/server/public/assets/tasks-D4y-Cwa3.js +0 -3
  189. package/payload/server/public/assets/tasks-D4y-Cwa3.js.br +0 -0
  190. package/payload/server/public/assets/tasks-D4y-Cwa3.js.gz +0 -0
  191. package/payload/server/public/assets/triangle-alert-BzCjDFts.js +0 -1
  192. package/payload/server/public/assets/triangle-alert-BzCjDFts.js.br +0 -0
  193. package/payload/server/public/assets/triangle-alert-BzCjDFts.js.gz +0 -0
  194. package/payload/server/public/assets/useCopyFeedback-BG7h37Rx.js.br +0 -2
  195. package/payload/server/public/assets/useCopyFeedback-BG7h37Rx.js.gz +0 -0
  196. package/payload/server/public/assets/useMediaQuery-CqwktQr_.js +0 -6
  197. package/payload/server/public/assets/useMediaQuery-CqwktQr_.js.br +0 -0
  198. package/payload/server/public/assets/useMediaQuery-CqwktQr_.js.gz +0 -0
  199. package/payload/server/public/assets/useVoiceRecorder-CAZcUc1s.js +0 -2
  200. package/payload/server/public/assets/useVoiceRecorder-CAZcUc1s.js.br +0 -0
  201. package/payload/server/public/assets/useVoiceRecorder-CAZcUc1s.js.gz +0 -0
  202. package/payload/server/public/assets/wrench-B2WKLLQG.js +0 -1
  203. package/payload/server/public/assets/wrench-B2WKLLQG.js.br +0 -0
  204. package/payload/server/public/assets/wrench-B2WKLLQG.js.gz +0 -0
  205. /package/payload/server/public/assets/{jsx-runtime-UK6hgBWO.js → jsx-runtime-CvVXOszc.js} +0 -0
  206. /package/payload/server/public/assets/{jsx-runtime-UK6hgBWO.js.br → jsx-runtime-CvVXOszc.js.br} +0 -0
  207. /package/payload/server/public/assets/{jsx-runtime-UK6hgBWO.js.gz → jsx-runtime-CvVXOszc.js.gz} +0 -0
@@ -0,0 +1,125 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { seedClaudeTrust, formatTrustSeedLine } from "../claude-trust-seed.js";
4
+ const ROOT = "/home/neo/maxy-code/data/accounts";
5
+ const A = `${ROOT}/aaa`;
6
+ const B = `${ROOT}/bbb`;
7
+ test("seeds every account directory that exists, alongside $HOME", () => {
8
+ const r = seedClaudeTrust({
9
+ state: {},
10
+ homeWorkspace: "/home/neo",
11
+ accountsRoot: ROOT,
12
+ accountDirs: [A, B],
13
+ });
14
+ const projects = r.state.projects;
15
+ assert.equal(projects["/home/neo"].hasTrustDialogAccepted, true);
16
+ assert.equal(projects[A].hasTrustDialogAccepted, true);
17
+ assert.equal(projects[B].hasTrustDialogAccepted, true);
18
+ assert.equal(r.state.hasCompletedOnboarding, true);
19
+ assert.deepEqual({ accounts: r.accounts, seeded: r.seeded, alreadyTrusted: r.alreadyTrusted, stale: r.stale }, { accounts: 2, seeded: 2, alreadyTrusted: 0, stale: 0 });
20
+ });
21
+ test("preserves other keys on an account entry and counts an existing true as already-set", () => {
22
+ const r = seedClaudeTrust({
23
+ state: {
24
+ numStartups: 519,
25
+ projects: {
26
+ [A]: { hasTrustDialogAccepted: true, exampleFilesGenerated: true },
27
+ [B]: { hasTrustDialogAccepted: false, lastCost: 1.5 },
28
+ },
29
+ },
30
+ homeWorkspace: "/home/neo",
31
+ accountsRoot: ROOT,
32
+ accountDirs: [A, B],
33
+ });
34
+ const projects = r.state.projects;
35
+ assert.equal(r.state.numStartups, 519);
36
+ assert.equal(projects[A].exampleFilesGenerated, true);
37
+ assert.equal(projects[B].lastCost, 1.5);
38
+ assert.equal(projects[B].hasTrustDialogAccepted, true);
39
+ assert.equal(r.alreadyTrusted, 1);
40
+ assert.equal(r.seeded, 1);
41
+ });
42
+ test("an entry whose directory is gone keeps its value and counts as stale", () => {
43
+ const gone = `${ROOT}/deleted`;
44
+ const r = seedClaudeTrust({
45
+ state: { projects: { [gone]: { hasTrustDialogAccepted: false } } },
46
+ homeWorkspace: "/home/neo",
47
+ accountsRoot: ROOT,
48
+ accountDirs: [A],
49
+ });
50
+ const projects = r.state.projects;
51
+ assert.equal(projects[gone].hasTrustDialogAccepted, false);
52
+ assert.equal(r.stale, 1);
53
+ assert.equal(r.accounts, 1);
54
+ });
55
+ // The CLI records an entry for any directory a session ran in, so an account's
56
+ // own subdirectories sit under the accounts root too. Counting them as stale
57
+ // accounts would inflate the field for every operator who worked inside one.
58
+ test("a subdirectory of a live account is not counted as a stale account", () => {
59
+ const r = seedClaudeTrust({
60
+ state: {
61
+ projects: {
62
+ [A]: { hasTrustDialogAccepted: true },
63
+ [`${A}/sites/portal`]: { hasTrustDialogAccepted: true },
64
+ },
65
+ },
66
+ homeWorkspace: "/home/neo",
67
+ accountsRoot: ROOT,
68
+ accountDirs: [A],
69
+ });
70
+ assert.equal(r.stale, 0);
71
+ });
72
+ test("no account directories is accounts=0, not a throw", () => {
73
+ const r = seedClaudeTrust({
74
+ state: {},
75
+ homeWorkspace: "/home/neo",
76
+ accountsRoot: ROOT,
77
+ accountDirs: [],
78
+ });
79
+ assert.equal(r.accounts, 0);
80
+ assert.equal(r.state.projects["/home/neo"] !== undefined, true);
81
+ });
82
+ // deployPayload() runs before setupAccount(), so on a FRESH install the first
83
+ // pass sees no accounts directory at all and the account that setupAccount
84
+ // then creates would never be trusted. The installer therefore primes twice;
85
+ // this pins the shape that second pass depends on.
86
+ test("fresh-install shape: an empty first pass then a second pass seeds the new account", () => {
87
+ const first = seedClaudeTrust({
88
+ state: {},
89
+ homeWorkspace: "/home/neo",
90
+ accountsRoot: ROOT,
91
+ accountDirs: [],
92
+ });
93
+ assert.equal(first.accounts, 0);
94
+ assert.equal(first.seeded, 0);
95
+ const second = seedClaudeTrust({
96
+ state: first.state,
97
+ homeWorkspace: "/home/neo",
98
+ accountsRoot: ROOT,
99
+ accountDirs: [A],
100
+ });
101
+ const projects = second.state.projects;
102
+ assert.equal(projects[A].hasTrustDialogAccepted, true);
103
+ assert.equal(second.accounts, 1);
104
+ assert.equal(second.seeded, 1);
105
+ assert.equal(projects["/home/neo"].hasTrustDialogAccepted, true);
106
+ });
107
+ test("the install line names which pass ran", () => {
108
+ const r = seedClaudeTrust({
109
+ state: {},
110
+ homeWorkspace: "/home/neo",
111
+ accountsRoot: ROOT,
112
+ accountDirs: [A],
113
+ });
114
+ const line = formatTrustSeedLine("/cfg/.claude.json", "/home/neo", r, "none", "post-account");
115
+ assert.equal(line, " [install] op=claude-trust-seed phase=post-account file=/cfg/.claude.json home=/home/neo" +
116
+ " accounts=1 seeded=1 alreadyTrusted=0 stale=0 error=none");
117
+ });
118
+ test("second run over the first run's output is byte-identical", () => {
119
+ const input = { state: {}, homeWorkspace: "/home/neo", accountsRoot: ROOT, accountDirs: [A, B] };
120
+ const first = seedClaudeTrust(input);
121
+ const second = seedClaudeTrust({ ...input, state: first.state });
122
+ assert.equal(JSON.stringify(second.state), JSON.stringify(first.state));
123
+ assert.equal(second.seeded, 0);
124
+ assert.equal(second.alreadyTrusted, 2);
125
+ });
@@ -0,0 +1,52 @@
1
+ // Task 2720 — Claude Code scopes folder trust to the git repository root, and
2
+ // provision-account-dir.sh gives every account directory its own repo, so
3
+ // trusting $HOME does not reach them. An untrusted workspace halts the
4
+ // PTY-spawned `claude --remote-control` on the folder-trust dialog before it
5
+ // binds, which reads downstream as `op=bind-signal-absent … pidFileSeen=false`
6
+ // and, because /rc-spawn answers 200, reaches the operator as silence.
7
+ //
8
+ // Pure so the counts and the preservation invariant are testable without a
9
+ // filesystem; the caller owns the read and the write.
10
+ export function seedClaudeTrust(input) {
11
+ const state = input.state;
12
+ state.hasCompletedOnboarding = true;
13
+ state.theme = state.theme ?? "auto";
14
+ const projects = (state.projects ?? {});
15
+ const homeEntry = projects[input.homeWorkspace] ?? {};
16
+ homeEntry.hasTrustDialogAccepted = true;
17
+ projects[input.homeWorkspace] = homeEntry;
18
+ let seeded = 0;
19
+ let alreadyTrusted = 0;
20
+ for (const dir of input.accountDirs) {
21
+ const entry = projects[dir] ?? {};
22
+ if (entry.hasTrustDialogAccepted === true) {
23
+ alreadyTrusted += 1;
24
+ }
25
+ else {
26
+ entry.hasTrustDialogAccepted = true;
27
+ seeded += 1;
28
+ }
29
+ projects[dir] = entry;
30
+ }
31
+ // A deleted account cannot be spawned into, so its entry is left exactly as
32
+ // the CLI wrote it. Counting it separately keeps the install line's
33
+ // `seeded + alreadyTrusted === accounts` invariant meaningful.
34
+ //
35
+ // Direct children only. The CLI records an entry for ANY directory a session
36
+ // ran in, so an account's own subdirectory (a `sites/<project>` tree, say)
37
+ // also sits under this prefix; counting those as stale accounts would inflate
38
+ // the field every time somebody worked inside an account.
39
+ const onDisk = new Set(input.accountDirs);
40
+ const prefix = `${input.accountsRoot}/`;
41
+ const stale = Object.keys(projects).filter((k) => k.startsWith(prefix) && !k.slice(prefix.length).includes("/") && !onDisk.has(k)).length;
42
+ state.projects = projects;
43
+ return { state, accounts: input.accountDirs.length, seeded, alreadyTrusted, stale };
44
+ }
45
+ /** `phase` distinguishes the two passes: `deploy` seeds an upgrade's existing
46
+ * accounts, `post-account` seeds the directory a fresh install just created.
47
+ * Without it the two lines are byte-identical and the operator cannot tell
48
+ * which pass did the work. */
49
+ export function formatTrustSeedLine(file, home, r, error, phase) {
50
+ return (` [install] op=claude-trust-seed phase=${phase} file=${file} home=${home} accounts=${r.accounts}` +
51
+ ` seeded=${r.seeded} alreadyTrusted=${r.alreadyTrusted} stale=${r.stale} error=${error}`);
52
+ }
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import { execFileSync, spawn, spawnSync } from "node:child_process";
3
3
  import { existsSync, mkdirSync, writeFileSync, cpSync, readFileSync, rmSync, readdirSync, appendFileSync, openSync, closeSync, chmodSync, statSync, realpathSync, accessSync, constants as fsConstants } from "node:fs";
4
4
  import { clearBrandAgentRoot } from "./brand-agent-root.js";
5
5
  import { seedBypassPermissionsSettings, assertBypassPermissionsSeed } from "./permissions-seed.js";
6
- import { resolve, join, dirname } from "node:path";
6
+ import { resolve, join, dirname, basename } from "node:path";
7
7
  import { randomBytes } from "node:crypto";
8
8
  import { resolveInstallPortFromFs, buildMaxyUnitFile, buildClaudeSessionManagerUnitFile, buildEmailSamplerUnitFile, buildClaudePtysSliceUnitFile, buildCloudflaredSliceUnitFile, buildDarwinServiceEnvPath } from "./port-resolution.js";
9
9
  import { buildBrandCronBlock, mergeBrandCronBlock } from "./cron-registration.js";
@@ -27,6 +27,7 @@ import { parseSwVers, isSupportedMacosVersion } from "./macos-version.js";
27
27
  import { decideChromiumAction, isSnapConfinedPath } from "./snap-chromium.js";
28
28
  import { classifyPortHolder } from "./preflight-port-classifier.js";
29
29
  import { memoryControllerMissing, cmdlineAlreadyPatched, patchCmdline } from "./cgroup-memory-controller.js";
30
+ import { seedClaudeTrust, formatTrustSeedLine } from "./claude-trust-seed.js";
30
31
  import { parsePluginList, computeInstallActions, parseExternalPlugins, findUnregisteredResyncs, discoverMarketplaceCandidateDirs, } from "./lib/plugin-install.js";
31
32
  import { findPremiumMcpDirs } from "./lib/premium-mcp-discover.js";
32
33
  import { pickBindDecision, pickEdgeBindDecision, formatEdgeBindDecision, pickWebsockifyBind, formatWebsockifyBindFile, mergeSmbConf, formatSambaMarker, SAMBA_ENABLE_UNITS, } from "./samba-provision.js";
@@ -569,6 +570,7 @@ function ensureNonSnapChromium() {
569
570
  }
570
571
  RESOLVED_CHROMIUM_BIN = decision.resolvedPath;
571
572
  }
573
+ RESOLVED_CHROMIUM_BIN = preferUnwrappedChromium(RESOLVED_CHROMIUM_BIN);
572
574
  // Defensive: never persist a snap-confined path. If realpath of the resolved
573
575
  // binary still lands under /snap/ (e.g. apt landed a snap package by mistake
574
576
  // on a misconfigured device), throw before writeChromiumBinaryPathFile sees
@@ -621,6 +623,44 @@ function runChromiumPostInstallGate(chromiumBin) {
621
623
  console.log(" Chromium post-install gate passed.");
622
624
  logFile(` [snap-chromium] post-install gate ok: ${chromiumBin} exit=0`);
623
625
  }
626
+ /**
627
+ * Prefer the real Chromium ELF binary over a distro shell wrapper.
628
+ *
629
+ * Debian and Raspberry Pi OS ship `/usr/bin/chromium` as a POSIX shell script
630
+ * that appends its own flags and then `exec $LIBDIR/$APPNAME "$@"`, where
631
+ * `LIBDIR=/usr/lib/$APPNAME` — so the real binary is derivable from the
632
+ * wrapper's own layout rather than guessed.
633
+ *
634
+ * On aarch64 with a page size above 4096 that wrapper adds
635
+ * `--js-flags=--no-decommit-pooled-pages` (its workaround for Debian #1089647).
636
+ * Current Chromium's V8 has removed that flag, so V8 exits "unrecognized flag",
637
+ * the GPU process dies with it, and the installer's CDP check then correctly
638
+ * finds nothing listening and aborts the upgrade. Measured on the beacons Pi
639
+ * 2026-08-14: Chromium 148.0.7778.167, `getconf PAGESIZE` 16384, and
640
+ * `chromium --js-flags=--no-decommit-pooled-pages` reproduces it directly.
641
+ *
642
+ * Execing the binary skips the wrapper and every flag it would inject. The
643
+ * wrapper is returned unchanged whenever the shape does not match — not a
644
+ * script, no binary at the derived path, or that path is not ELF — so a distro
645
+ * laying Chromium out differently is untouched.
646
+ */
647
+ function preferUnwrappedChromium(binPath) {
648
+ const fileType = (p) => {
649
+ const r = spawnSync("file", ["-b", p], { encoding: "utf-8", stdio: "pipe", timeout: 5_000 });
650
+ return r.status === 0 ? (r.stdout ?? "").trim() : "";
651
+ };
652
+ if (!/shell script/i.test(fileType(binPath)))
653
+ return binPath;
654
+ const appName = basename(binPath);
655
+ const real = `/usr/lib/${appName}/${appName}`;
656
+ if (!existsSync(real) || !/^ELF/.test(fileType(real))) {
657
+ logFile(` [chromium-wrapper] ${binPath} is a wrapper but ${real} is not a usable binary — keeping the wrapper`);
658
+ return binPath;
659
+ }
660
+ console.log(` Chromium wrapper bypassed: ${binPath} → ${real}`);
661
+ logFile(` [chromium-wrapper] bypassed wrapper=${binPath} binary=${real}`);
662
+ return real;
663
+ }
624
664
  /**
625
665
  * write the resolved Chromium absolute path to
626
666
  * `<INSTALL_DIR>/platform/config/chromium-binary.path` so vnc.sh and
@@ -2488,6 +2528,67 @@ function runInstallInvariantCheck(usersFile, accountsDir) {
2488
2528
  }
2489
2529
  console.log(` ${TAG} check complete divergences=${divergences}`);
2490
2530
  }
2531
+ // pre-populate the brand `.claude.json` so the manager's PTY-spawned
2532
+ // `claude --verbose --remote-control` boots straight into the session URL
2533
+ // without stopping at the first-run theme picker or the workspace-trust
2534
+ // dialog. Both prompts ate the manager's 30 s url-capture window on
2535
+ // 0.1.6 / 0.1.8 installs (Pi 192.168.88.16 verified). The markers required:
2536
+ // - hasCompletedOnboarding: true (suppresses theme picker)
2537
+ // - projects[<dir>].hasTrustDialogAccepted: true, for $HOME AND for every
2538
+ // account directory (Task 2720). Claude Code scopes folder trust to the
2539
+ // git repository root, and provision-account-dir.sh gives each account
2540
+ // directory its own repo, so $HOME's trust does not reach them. Seeding
2541
+ // $HOME alone left every spawn halted on the trust dialog.
2542
+ // Existing fields (firstStartTime, oauthAccount, migrationVersion, …) are
2543
+ // preserved so a brand re-install does not clobber state written by claude
2544
+ // during the previous run.
2545
+ //
2546
+ // Called TWICE, and both calls are load-bearing. `deploy` runs inside
2547
+ // deployPayload, which is where an upgrade's already-existing account
2548
+ // directories are seeded; `post-account` runs after setupAccount(), which is
2549
+ // the only point a FRESH install has an account directory on disk at all.
2550
+ // Running deploy alone would leave every fresh install untrusted and hang its
2551
+ // first session. The transform is idempotent, so the second pass rewrites
2552
+ // nothing the first already did (`seeded=0` when there was nothing new).
2553
+ function primeClaudeState(phase) {
2554
+ const persistentDir = resolve(process.env.HOME ?? "/root", BRAND.configDir);
2555
+ const persistentClaudeDir = join(persistentDir, ".claude");
2556
+ mkdirSync(persistentClaudeDir, { recursive: true });
2557
+ const claudeStateFile = join(persistentClaudeDir, ".claude.json");
2558
+ const homeWorkspace = process.env.HOME ?? "/root";
2559
+ let claudeState = {};
2560
+ if (existsSync(claudeStateFile)) {
2561
+ try {
2562
+ claudeState = JSON.parse(readFileSync(claudeStateFile, "utf-8"));
2563
+ }
2564
+ catch {
2565
+ claudeState = {};
2566
+ }
2567
+ }
2568
+ // An absent accounts directory is a first install, not an error; a listing
2569
+ // that throws is reported on the line rather than aborting the install.
2570
+ const accountsRoot = join(INSTALL_DIR, "data/accounts");
2571
+ let accountDirs = [];
2572
+ let seedError = "none";
2573
+ try {
2574
+ if (existsSync(accountsRoot)) {
2575
+ accountDirs = readdirSync(accountsRoot, { withFileTypes: true })
2576
+ .filter((e) => e.isDirectory())
2577
+ .map((e) => join(accountsRoot, e.name));
2578
+ }
2579
+ }
2580
+ catch (err) {
2581
+ seedError = err instanceof Error ? err.message : String(err);
2582
+ }
2583
+ const seedResult = seedClaudeTrust({
2584
+ state: claudeState,
2585
+ homeWorkspace,
2586
+ accountsRoot,
2587
+ accountDirs,
2588
+ });
2589
+ writeFileSync(claudeStateFile, JSON.stringify(seedResult.state, null, 2) + "\n", { mode: 0o600 });
2590
+ console.log(formatTrustSeedLine(claudeStateFile, homeWorkspace, seedResult, seedError, phase));
2591
+ }
2491
2592
  function deployPayload() {
2492
2593
  log("8", TOTAL, `Deploying ${BRAND.productName}...`);
2493
2594
  if (!existsSync(PAYLOAD_DIR)) {
@@ -2511,39 +2612,8 @@ function deployPayload() {
2511
2612
  // `claude` SDK subprocess and admin server read from there via the
2512
2613
  // Environment=CLAUDE_CONFIG_DIR= block in port-resolution.ts. Fresh
2513
2614
  // installs prompt the operator to run `claude /login`.
2514
- const persistentClaudeDir = join(persistentDir, ".claude");
2515
- mkdirSync(persistentClaudeDir, { recursive: true });
2516
- // pre-populate the brand `.claude.json` so the manager's
2517
- // PTY-spawned `claude --verbose --remote-control` boots straight into the
2518
- // session URL without stopping at the first-run theme picker or the
2519
- // workspace-trust dialog. Both prompts ate the manager's 30 s url-capture
2520
- // window on 0.1.6 / 0.1.8 installs (Pi 192.168.88.16 verified). The two
2521
- // markers required are:
2522
- // - hasCompletedOnboarding: true (suppresses theme picker)
2523
- // - projects[$HOME].hasTrustDialogAccepted: true
2524
- // Existing fields (firstStartTime, oauthAccount, migrationVersion, …) are
2525
- // preserved so a brand re-install does not clobber state written by claude
2526
- // during the previous run.
2527
- const claudeStateFile = join(persistentClaudeDir, ".claude.json");
2528
- const homeWorkspace = process.env.HOME ?? "/root";
2529
- let claudeState = {};
2530
- if (existsSync(claudeStateFile)) {
2531
- try {
2532
- claudeState = JSON.parse(readFileSync(claudeStateFile, "utf-8"));
2533
- }
2534
- catch {
2535
- claudeState = {};
2536
- }
2537
- }
2538
- claudeState.hasCompletedOnboarding = true;
2539
- claudeState.theme = claudeState.theme ?? "auto";
2540
- const projects = (claudeState.projects ?? {});
2541
- const homeEntry = projects[homeWorkspace] ?? {};
2542
- homeEntry.hasTrustDialogAccepted = true;
2543
- projects[homeWorkspace] = homeEntry;
2544
- claudeState.projects = projects;
2545
- writeFileSync(claudeStateFile, JSON.stringify(claudeState, null, 2) + "\n", { mode: 0o600 });
2546
- console.log(` [install] claude-state primed: ${claudeStateFile} hasCompletedOnboarding=true trustedWorkspace=${homeWorkspace}`);
2615
+ // primeClaudeState creates that directory, so there is no separate mkdir here.
2616
+ primeClaudeState("deploy");
2547
2617
  // Brand isolation: installer does not read ~/.maxy/, ~/.cloudflared/, or
2548
2618
  // ~/.cloudflare/ on non-default brands. These are peer-brand or
2549
2619
  // shared-singleton paths.
@@ -5458,6 +5528,10 @@ try {
5458
5528
  registerLocalAndExternalPlugins(); // install-time plugin registration
5459
5529
  setupVncViewer();
5460
5530
  setupAccount();
5531
+ // Task 2720 — the account directory only exists from here on a fresh
5532
+ // install, and provision-account-dir.sh has just made it a git repo root,
5533
+ // so this is the pass that trusts it. Idempotent on the upgrade path.
5534
+ primeClaudeState("post-account");
5461
5535
  if (ENTITLEMENT_PAYLOAD) {
5462
5536
  writeEntitlementFromFlag(ENTITLEMENT_PAYLOAD);
5463
5537
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-realagent-code",
3
- "version": "0.1.605",
3
+ "version": "0.1.607",
4
4
  "description": "Install Real Agent — Built for agents. By agents.",
5
5
  "bin": {
6
6
  "create-realagent-code": "./dist/index.js"
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: platform-architecture
3
3
  description: Use when grounding any documented-surface claim about what Real Agent 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:3335583b9c81ee093958338fc85cc28a8388c5a6799938675a5f643393836f4b
4
+ content-hash: sha256:72ceb87984dc863b94b0bb40ef8aead338b2d093e44caf456504d6d09ea0a2c6
5
5
  brand: realagent-code
6
6
  product-name: Real Agent
7
7
  ---
@@ -325,6 +325,8 @@ If you suspect background processes are piling up, run `grep '\[reaper\]' ~/.{br
325
325
 
326
326
  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. 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 this change: `bash <install>/platform/scripts/backfill-bypass-permissions.sh ~/.{brand}`.
327
327
 
328
+ **Workspace trust.** Separate from permissions, and its own way to stop a spawn dead. Claude Code refuses to start in a directory it does not trust, and it scopes that trust to the **git repository root** of the working directory rather than to any trusted ancestor. Every account directory is its own repository (`provision-account-dir.sh` runs `git init` there so Claude Code finds the account's own `.claude/` hook set), so trusting the home directory does not reach them. Every install therefore seeds `projects[<dir>].hasTrustDialogAccepted: true` into `~/.{brand}/.claude/.claude.json` for the home directory **and for every directory under `<install>/data/accounts`**, preserving all other fields the CLI wrote. Without it the PTY-spawned session halts on "Quick safety check: Is this a project you created or one you trust?" and never binds, which surfaces as a session that starts and answers nothing, with no error in the interface. The seeding runs twice per install and both passes matter: the `deploy` pass trusts the accounts an upgrade already has, and the `post-account` pass, which runs straight after account setup, trusts the account a brand-new install has only just created. The install line is `[install] op=claude-trust-seed phase=<deploy|post-account> file=… accounts=<N> seeded=<n> alreadyTrusted=<n> stale=<n> error=<none|msg>`; `seeded + alreadyTrusted` equals `accounts`, and a re-run reads `seeded=0`. Because an account added *after* an install has an untrusted repository root that no event announces, the session manager reconciles the two every hour and logs `[claude-trust-census] accounts=<N> trusted=<n> untrusted=<n> untrustedDirs=<csv|none> homeTrusted=<bool> onboarded=<bool>` — a non-zero `untrusted` names the directories whose next session will hang. To verify after an install: `node -e 'const j=require(process.env.HOME+"/.{brand}/.claude/.claude.json");const p=j.projects||{};for(const k of Object.keys(p))if(k.includes("/data/accounts/"))console.log(k,p[k].hasTrustDialogAccepted)'` (expect `true` for every account directory that exists).
329
+
328
330
  **`autoMode` trust block.** 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`.
329
331
 
330
332
  ---
@@ -4095,6 +4097,19 @@ which fails `prebuild` if any emoji codepoint appears in a `.ts`/`.tsx` file
4095
4097
  under `app/` (tests excluded). Inline an icon at the surrounding text size and
4096
4098
  colour (`size={14}` matches body text; Lucide inherits `currentColor`).
4097
4099
 
4100
+ **A long message shows its first 8 lines.** On `/chat`, the WhatsApp and
4101
+ Telegram readers and the public visitor chat, a message body taller than 8 lines
4102
+ is cut at 8 and carries a "Show more…" control that reveals the rest; "Show
4103
+ less" puts it back. The control appears only when there is genuinely more to
4104
+ see, so a short message looks exactly as it did before. Expanding one message
4105
+ expands only that message, and scrolling back through older messages does not
4106
+ carry an expansion onto a different one. Two things are never cut: an error the
4107
+ agent reports, and the map on a shared location. The reader's search also opens
4108
+ any message whose match would otherwise sit in the hidden part, so a search
4109
+ never finds something it then hides. See
4110
+ [`.docs/message-body-clamp.md`](../../../.docs/message-body-clamp.md) for the
4111
+ render sites and the census that watches it.
4112
+
4098
4113
  ## Admin Hono routes
4099
4114
 
4100
4115
  Every admin sub-app is mounted by
@@ -9,6 +9,15 @@ Invoked by the admin agent directly.
9
9
 
10
10
  This is the platform's release timeline, newest first. Each entry shows the date it shipped and the version it shipped in, so you can tell the operator how current their install is. To compare, read the installed version from `capabilities-here` and match it against the versions below. Keep answers high level and in plain English; this is a summary, not a full commit log.
11
11
 
12
+ ## 2026-08-14 (0.1.607)
13
+
14
+ - Upgrades no longer fail on a Raspberry Pi with a 16K page size. The system's own Chromium launcher adds a setting that current Chromium has removed, so the browser exited on startup and the install stopped when it could not reach it. The installer now runs Chromium directly, skipping that launcher.
15
+
16
+ ## 2026-08-14 (0.1.606)
17
+
18
+ - Conversations start after an install again. The installer marked only your home folder as trusted, and the assistant now asks about the folder each account actually runs from, so a session would stop at a trust prompt nobody could answer.
19
+ - A long chat message is capped at eight lines with a control to show the rest, instead of filling the pane.
20
+
12
21
  ## 2026-08-14 (0.1.605)
13
22
 
14
23
  - Five more routines now check whether there is anything to do before waking the assistant. Mail triage, appointment reminders, the operational and finance reconciles, and the two health reports all previously ran on the clock and did the deciding after starting up.
@@ -37,6 +37,19 @@ which fails `prebuild` if any emoji codepoint appears in a `.ts`/`.tsx` file
37
37
  under `app/` (tests excluded). Inline an icon at the surrounding text size and
38
38
  colour (`size={14}` matches body text; Lucide inherits `currentColor`).
39
39
 
40
+ **A long message shows its first 8 lines.** On `/chat`, the WhatsApp and
41
+ Telegram readers and the public visitor chat, a message body taller than 8 lines
42
+ is cut at 8 and carries a "Show more…" control that reveals the rest; "Show
43
+ less" puts it back. The control appears only when there is genuinely more to
44
+ see, so a short message looks exactly as it did before. Expanding one message
45
+ expands only that message, and scrolling back through older messages does not
46
+ carry an expansion onto a different one. Two things are never cut: an error the
47
+ agent reports, and the map on a shared location. The reader's search also opens
48
+ any message whose match would otherwise sit in the hidden part, so a search
49
+ never finds something it then hides. See
50
+ [`.docs/message-body-clamp.md`](../../../.docs/message-body-clamp.md) for the
51
+ render sites and the census that watches it.
52
+
40
53
  ## Admin Hono routes
41
54
 
42
55
  Every admin sub-app is mounted by
@@ -198,4 +198,6 @@ If you suspect background processes are piling up, run `grep '\[reaper\]' ~/.{br
198
198
 
199
199
  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. 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 this change: `bash <install>/platform/scripts/backfill-bypass-permissions.sh ~/.{brand}`.
200
200
 
201
+ **Workspace trust.** Separate from permissions, and its own way to stop a spawn dead. Claude Code refuses to start in a directory it does not trust, and it scopes that trust to the **git repository root** of the working directory rather than to any trusted ancestor. Every account directory is its own repository (`provision-account-dir.sh` runs `git init` there so Claude Code finds the account's own `.claude/` hook set), so trusting the home directory does not reach them. Every install therefore seeds `projects[<dir>].hasTrustDialogAccepted: true` into `~/.{brand}/.claude/.claude.json` for the home directory **and for every directory under `<install>/data/accounts`**, preserving all other fields the CLI wrote. Without it the PTY-spawned session halts on "Quick safety check: Is this a project you created or one you trust?" and never binds, which surfaces as a session that starts and answers nothing, with no error in the interface. The seeding runs twice per install and both passes matter: the `deploy` pass trusts the accounts an upgrade already has, and the `post-account` pass, which runs straight after account setup, trusts the account a brand-new install has only just created. The install line is `[install] op=claude-trust-seed phase=<deploy|post-account> file=… accounts=<N> seeded=<n> alreadyTrusted=<n> stale=<n> error=<none|msg>`; `seeded + alreadyTrusted` equals `accounts`, and a re-run reads `seeded=0`. Because an account added *after* an install has an untrusted repository root that no event announces, the session manager reconciles the two every hour and logs `[claude-trust-census] accounts=<N> trusted=<n> untrusted=<n> untrustedDirs=<csv|none> homeTrusted=<bool> onboarded=<bool>` — a non-zero `untrusted` names the directories whose next session will hang. To verify after an install: `node -e 'const j=require(process.env.HOME+"/.{brand}/.claude/.claude.json");const p=j.projects||{};for(const k of Object.keys(p))if(k.includes("/data/accounts/"))console.log(k,p[k].hasTrustDialogAccepted)'` (expect `true` for every account directory that exists).
202
+
201
203
  **`autoMode` trust block.** 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`.
@@ -0,0 +1,34 @@
1
+ export type Logger = (line: string) => void;
2
+ export interface TrustFacts {
3
+ accounts: number;
4
+ trusted: number;
5
+ untrusted: number;
6
+ untrustedDirs: string[];
7
+ homeTrusted: boolean;
8
+ onboarded: boolean;
9
+ }
10
+ /** Pure. An account directory with no entry counts as untrusted, not absent. */
11
+ export declare function collectTrustFacts(input: {
12
+ state: Record<string, unknown>;
13
+ homeWorkspace: string;
14
+ accountDirs: string[];
15
+ }): TrustFacts;
16
+ /** Pure. */
17
+ export declare function formatTrustCensus(f: TrustFacts): string;
18
+ export interface TrustCensusDeps {
19
+ claudeJsonPath: string;
20
+ accountsDir: string;
21
+ homeWorkspace: string;
22
+ logger: Logger;
23
+ intervalMs: number;
24
+ /** Injected in tests so the collector runs without a filesystem. */
25
+ readState?: () => Record<string, unknown>;
26
+ listAccountDirs?: () => string[];
27
+ }
28
+ export interface TrustCensus {
29
+ tickOnce(): void;
30
+ start(): void;
31
+ stop(): void;
32
+ }
33
+ export declare function createClaudeTrustCensus(deps: TrustCensusDeps): TrustCensus;
34
+ //# sourceMappingURL=claude-trust-census.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"claude-trust-census.d.ts","sourceRoot":"","sources":["../src/claude-trust-census.ts"],"names":[],"mappings":"AAuBA,MAAM,MAAM,MAAM,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;AAE3C,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,MAAM,CAAA;IACjB,aAAa,EAAE,MAAM,EAAE,CAAA;IACvB,WAAW,EAAE,OAAO,CAAA;IACpB,SAAS,EAAE,OAAO,CAAA;CACnB;AAED,gFAAgF;AAChF,wBAAgB,iBAAiB,CAAC,KAAK,EAAE;IACvC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC9B,aAAa,EAAE,MAAM,CAAA;IACrB,WAAW,EAAE,MAAM,EAAE,CAAA;CACtB,GAAG,UAAU,CAWb;AAED,YAAY;AACZ,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,UAAU,GAAG,MAAM,CAMvD;AAED,MAAM,WAAW,eAAe;IAC9B,cAAc,EAAE,MAAM,CAAA;IACtB,WAAW,EAAE,MAAM,CAAA;IACnB,aAAa,EAAE,MAAM,CAAA;IACrB,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,EAAE,MAAM,CAAA;IAClB,oEAAoE;IACpE,SAAS,CAAC,EAAE,MAAM,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACzC,eAAe,CAAC,EAAE,MAAM,MAAM,EAAE,CAAA;CACjC;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,IAAI,IAAI,CAAA;IAChB,KAAK,IAAI,IAAI,CAAA;IACb,IAAI,IAAI,IAAI,CAAA;CACb;AAED,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,eAAe,GAAG,WAAW,CA6C1E"}
@@ -0,0 +1,83 @@
1
+ // Task 2720 — standing reconciliation of account directories against the
2
+ // workspace-trust entries in the brand's `.claude.json`.
3
+ //
4
+ // Claude Code scopes folder trust to the git repository root, and
5
+ // provision-account-dir.sh gives every account directory its own repo, so a
6
+ // directory with no `hasTrustDialogAccepted: true` entry halts the spawned
7
+ // child on the folder-trust dialog before it binds.
8
+ //
9
+ // Two of that defect's failure modes emit no event and do not reproduce on
10
+ // demand: an installer enumeration that skipped a directory, and an account
11
+ // created after the install (a fresh repo root with no entry). Neither
12
+ // produces a log line until somebody tries to spawn a session there, and by
13
+ // then the symptom is a 60 s bind timeout that `/rc-spawn`'s 200 turns into
14
+ // silence. Only a periodic reconcile of expected against actual surfaces them.
15
+ //
16
+ // Shape mirrors the other standing censuses in this service (a pure collector,
17
+ // a pure formatter, and a start/stop factory whose setInterval is unref'd):
18
+ // see `role-census.ts`.
19
+ import { readFileSync, readdirSync } from 'node:fs';
20
+ import { join } from 'node:path';
21
+ const TAG = '[claude-trust-census]';
22
+ /** Pure. An account directory with no entry counts as untrusted, not absent. */
23
+ export function collectTrustFacts(input) {
24
+ const projects = (input.state.projects ?? {});
25
+ const untrustedDirs = input.accountDirs.filter((d) => projects[d]?.hasTrustDialogAccepted !== true);
26
+ return {
27
+ accounts: input.accountDirs.length,
28
+ trusted: input.accountDirs.length - untrustedDirs.length,
29
+ untrusted: untrustedDirs.length,
30
+ untrustedDirs,
31
+ homeTrusted: projects[input.homeWorkspace]?.hasTrustDialogAccepted === true,
32
+ onboarded: input.state.hasCompletedOnboarding === true,
33
+ };
34
+ }
35
+ /** Pure. */
36
+ export function formatTrustCensus(f) {
37
+ const dirs = f.untrustedDirs.length === 0 ? 'none' : f.untrustedDirs.join(',');
38
+ return (`${TAG} accounts=${f.accounts} trusted=${f.trusted} untrusted=${f.untrusted}` +
39
+ ` untrustedDirs=${dirs} homeTrusted=${f.homeTrusted} onboarded=${f.onboarded}`);
40
+ }
41
+ export function createClaudeTrustCensus(deps) {
42
+ let timer = null;
43
+ const readState = deps.readState ??
44
+ (() => JSON.parse(readFileSync(deps.claudeJsonPath, 'utf-8')));
45
+ const listAccountDirs = deps.listAccountDirs ??
46
+ (() => readdirSync(deps.accountsDir, { withFileTypes: true })
47
+ .filter((e) => e.isDirectory())
48
+ .map((e) => join(deps.accountsDir, e.name)));
49
+ function tickOnce() {
50
+ try {
51
+ const facts = collectTrustFacts({
52
+ state: readState(),
53
+ homeWorkspace: deps.homeWorkspace,
54
+ accountDirs: listAccountDirs(),
55
+ });
56
+ // Emitted on a clean result too. A quiet interval is itself the signal
57
+ // that the census is alive, so a missing line means the audit stopped
58
+ // rather than that every directory is trusted.
59
+ deps.logger(formatTrustCensus(facts));
60
+ }
61
+ catch (err) {
62
+ deps.logger(`${TAG} op=census-failed message=${err instanceof Error ? err.message : String(err)}`);
63
+ }
64
+ }
65
+ return {
66
+ tickOnce,
67
+ start() {
68
+ if (timer)
69
+ return;
70
+ timer = setInterval(tickOnce, deps.intervalMs);
71
+ if (typeof timer.unref === 'function')
72
+ timer.unref();
73
+ deps.logger(`${TAG} op=census-started intervalMs=${deps.intervalMs}`);
74
+ },
75
+ stop() {
76
+ if (timer) {
77
+ clearInterval(timer);
78
+ timer = null;
79
+ }
80
+ },
81
+ };
82
+ }
83
+ //# sourceMappingURL=claude-trust-census.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"claude-trust-census.js","sourceRoot":"","sources":["../src/claude-trust-census.ts"],"names":[],"mappings":"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"}
@@ -36,6 +36,7 @@ import { createPermissionModeTail } from './permission-mode-tail.js';
36
36
  import { createReaper } from './reaper.js';
37
37
  import { createReachabilityAudit } from './public-agent-reachability.js';
38
38
  import { createRoleCensus } from './role-census.js';
39
+ import { createClaudeTrustCensus } from './claude-trust-census.js';
39
40
  import { createPublicPromptSurfaceCensus } from './public-prompt-surface.js';
40
41
  import { createPublicKnowledgeAudit, readPublicAgentKnowledge } from './public-knowledge-audit.js';
41
42
  import { createPtyCensus } from './pty-census.js';
@@ -1264,6 +1265,18 @@ async function main() {
1264
1265
  intervalMs: 900_000,
1265
1266
  });
1266
1267
  quoteReconcile.start();
1268
+ // Task 2720 — hourly reconcile of account directories against their
1269
+ // workspace-trust entries in the brand's `.claude.json`. An account created
1270
+ // after the install is a fresh git repo root with no trust entry, and
1271
+ // nothing announces it: the first spawn there halts on the folder-trust
1272
+ // dialog and /rc-spawn's 200 hides the failure. Purely filesystem.
1273
+ createClaudeTrustCensus({
1274
+ claudeJsonPath: join(claudeConfigDirResolved, '.claude.json'),
1275
+ accountsDir: dirname(config.accountDir),
1276
+ homeWorkspace: process.env.HOME ?? '/root',
1277
+ logger: log,
1278
+ intervalMs: 3_600_000,
1279
+ }).start();
1267
1280
  // Task 183 — recorder JSONL tail. Posts [recorder-session-init] and
1268
1281
  // [recorder-turn] lines for every database-operator specialist spawn,
1269
1282
  // routed through the platform UI's /api/admin/log-ingest loopback