@rubytech/create-maxy-code 0.1.458 → 0.1.460

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 (218) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/lib/storage-broker/dist/__tests__/cf-exec.test.js +298 -3
  3. package/payload/platform/lib/storage-broker/dist/__tests__/cf-exec.test.js.map +1 -1
  4. package/payload/platform/lib/storage-broker/dist/__tests__/object-limits.test.d.ts +2 -0
  5. package/payload/platform/lib/storage-broker/dist/__tests__/object-limits.test.d.ts.map +1 -0
  6. package/payload/platform/lib/storage-broker/dist/__tests__/object-limits.test.js +79 -0
  7. package/payload/platform/lib/storage-broker/dist/__tests__/object-limits.test.js.map +1 -0
  8. package/payload/platform/lib/storage-broker/dist/__tests__/resource-limits.test.d.ts +2 -0
  9. package/payload/platform/lib/storage-broker/dist/__tests__/resource-limits.test.d.ts.map +1 -0
  10. package/payload/platform/lib/storage-broker/dist/__tests__/resource-limits.test.js +41 -0
  11. package/payload/platform/lib/storage-broker/dist/__tests__/resource-limits.test.js.map +1 -0
  12. package/payload/platform/lib/storage-broker/dist/cf-exec.d.ts +13 -1
  13. package/payload/platform/lib/storage-broker/dist/cf-exec.d.ts.map +1 -1
  14. package/payload/platform/lib/storage-broker/dist/cf-exec.js +165 -0
  15. package/payload/platform/lib/storage-broker/dist/cf-exec.js.map +1 -1
  16. package/payload/platform/lib/storage-broker/dist/house-credential.d.ts.map +1 -1
  17. package/payload/platform/lib/storage-broker/dist/house-credential.js +4 -2
  18. package/payload/platform/lib/storage-broker/dist/house-credential.js.map +1 -1
  19. package/payload/platform/lib/storage-broker/dist/index.d.ts +2 -0
  20. package/payload/platform/lib/storage-broker/dist/index.d.ts.map +1 -1
  21. package/payload/platform/lib/storage-broker/dist/index.js +2 -0
  22. package/payload/platform/lib/storage-broker/dist/index.js.map +1 -1
  23. package/payload/platform/lib/storage-broker/dist/object-limits.d.ts +80 -0
  24. package/payload/platform/lib/storage-broker/dist/object-limits.d.ts.map +1 -0
  25. package/payload/platform/lib/storage-broker/dist/object-limits.js +92 -0
  26. package/payload/platform/lib/storage-broker/dist/object-limits.js.map +1 -0
  27. package/payload/platform/lib/storage-broker/dist/resource-limits.d.ts +102 -0
  28. package/payload/platform/lib/storage-broker/dist/resource-limits.d.ts.map +1 -0
  29. package/payload/platform/lib/storage-broker/dist/resource-limits.js +105 -0
  30. package/payload/platform/lib/storage-broker/dist/resource-limits.js.map +1 -0
  31. package/payload/platform/lib/storage-broker/src/__tests__/cf-exec.test.ts +334 -5
  32. package/payload/platform/lib/storage-broker/src/__tests__/object-limits.test.ts +106 -0
  33. package/payload/platform/lib/storage-broker/src/__tests__/resource-limits.test.ts +49 -0
  34. package/payload/platform/lib/storage-broker/src/cf-exec.ts +221 -6
  35. package/payload/platform/lib/storage-broker/src/house-credential.ts +4 -2
  36. package/payload/platform/lib/storage-broker/src/index.ts +2 -0
  37. package/payload/platform/lib/storage-broker/src/object-limits.ts +95 -0
  38. package/payload/platform/lib/storage-broker/src/resource-limits.ts +105 -0
  39. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +7 -6
  40. package/payload/platform/plugins/cloudflare/PLUGIN.md +3 -1
  41. package/payload/platform/plugins/cloudflare/bin/cf-token.sh +4 -3
  42. package/payload/platform/plugins/cloudflare/mcp/__tests__/auth-route.test.ts +206 -0
  43. package/payload/platform/plugins/cloudflare/mcp/__tests__/authorize.test.ts +201 -0
  44. package/payload/platform/plugins/cloudflare/mcp/__tests__/file-routes.test.ts +178 -0
  45. package/payload/platform/plugins/cloudflare/mcp/__tests__/harness.test.ts +32 -0
  46. package/payload/platform/plugins/cloudflare/mcp/__tests__/passcode.test.ts +74 -0
  47. package/payload/platform/plugins/cloudflare/mcp/__tests__/ratelimit.test.ts +77 -0
  48. package/payload/platform/plugins/cloudflare/mcp/__tests__/skill-contract.test.ts +46 -0
  49. package/payload/platform/plugins/cloudflare/mcp/__tests__/template-config.test.ts +55 -0
  50. package/payload/platform/plugins/cloudflare/mcp/__tests__/upload-route.test.ts +255 -0
  51. package/payload/platform/plugins/cloudflare/mcp/package.json +5 -2
  52. package/payload/platform/plugins/cloudflare/references/api.md +1 -1
  53. package/payload/platform/plugins/cloudflare/references/r2-object-storage.md +46 -0
  54. package/payload/platform/plugins/cloudflare/skills/data-portal/SKILL.md +123 -0
  55. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/authorize.ts +32 -0
  56. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/log.ts +12 -0
  57. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/passcode.ts +73 -0
  58. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/ratelimit.ts +55 -0
  59. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/session.ts +85 -0
  60. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/types.ts +44 -0
  61. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/auth.ts +113 -0
  62. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/delete.ts +89 -0
  63. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/download.ts +70 -0
  64. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/files.ts +41 -0
  65. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/upload.ts +151 -0
  66. package/payload/platform/plugins/cloudflare/skills/data-portal/template/public/index.html +42 -0
  67. package/payload/platform/plugins/cloudflare/skills/data-portal/template/public/portal.css +113 -0
  68. package/payload/platform/plugins/cloudflare/skills/data-portal/template/public/portal.js +155 -0
  69. package/payload/platform/plugins/cloudflare/skills/data-portal/template/schema.sql +64 -0
  70. package/payload/platform/plugins/cloudflare/skills/data-portal/template/wrangler.toml +21 -0
  71. package/payload/platform/plugins/cloudflare/skills/site-deploy/SKILL.md +8 -0
  72. package/payload/platform/plugins/docs/references/outlook-guide.md +5 -5
  73. package/payload/platform/plugins/docs/references/voice-mirror-guide.md +1 -0
  74. package/payload/platform/plugins/filesystem/PLUGIN.md +0 -1
  75. package/payload/platform/plugins/filesystem/mcp/dist/lib/scope.d.ts +14 -10
  76. package/payload/platform/plugins/filesystem/mcp/dist/lib/scope.d.ts.map +1 -1
  77. package/payload/platform/plugins/filesystem/mcp/dist/lib/scope.js +14 -10
  78. package/payload/platform/plugins/filesystem/mcp/dist/lib/scope.js.map +1 -1
  79. package/payload/platform/plugins/outlook/.claude-plugin/plugin.json +1 -1
  80. package/payload/platform/plugins/outlook/PLUGIN.md +3 -5
  81. package/payload/platform/plugins/outlook/mcp/dist/__tests__/attach.test.d.ts +2 -0
  82. package/payload/platform/plugins/outlook/mcp/dist/__tests__/attach.test.d.ts.map +1 -0
  83. package/payload/platform/plugins/outlook/mcp/dist/__tests__/attach.test.js +138 -0
  84. package/payload/platform/plugins/outlook/mcp/dist/__tests__/attach.test.js.map +1 -0
  85. package/payload/platform/plugins/outlook/mcp/dist/__tests__/detach.test.d.ts +2 -0
  86. package/payload/platform/plugins/outlook/mcp/dist/__tests__/detach.test.d.ts.map +1 -0
  87. package/payload/platform/plugins/outlook/mcp/dist/__tests__/detach.test.js +75 -0
  88. package/payload/platform/plugins/outlook/mcp/dist/__tests__/detach.test.js.map +1 -0
  89. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-attachments.test.d.ts +2 -0
  90. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-attachments.test.d.ts.map +1 -0
  91. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-attachments.test.js +121 -0
  92. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-attachments.test.js.map +1 -0
  93. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-attachments.test.d.ts +2 -0
  94. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-attachments.test.d.ts.map +1 -0
  95. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-attachments.test.js +102 -0
  96. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-attachments.test.js.map +1 -0
  97. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-removal.test.d.ts +2 -0
  98. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-removal.test.d.ts.map +1 -0
  99. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-removal.test.js +187 -0
  100. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-removal.test.js.map +1 -0
  101. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit.test.js +1 -1
  102. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit.test.js.map +1 -1
  103. package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-send-attachments.test.d.ts +2 -0
  104. package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-send-attachments.test.d.ts.map +1 -0
  105. package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-send-attachments.test.js +209 -0
  106. package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-send-attachments.test.js.map +1 -0
  107. package/payload/platform/plugins/outlook/mcp/dist/index.js +12 -8
  108. package/payload/platform/plugins/outlook/mcp/dist/index.js.map +1 -1
  109. package/payload/platform/plugins/outlook/mcp/dist/lib/attach.d.ts +37 -0
  110. package/payload/platform/plugins/outlook/mcp/dist/lib/attach.d.ts.map +1 -0
  111. package/payload/platform/plugins/outlook/mcp/dist/lib/attach.js +79 -0
  112. package/payload/platform/plugins/outlook/mcp/dist/lib/attach.js.map +1 -0
  113. package/payload/platform/plugins/outlook/mcp/dist/lib/attachment-list.d.ts +43 -0
  114. package/payload/platform/plugins/outlook/mcp/dist/lib/attachment-list.d.ts.map +1 -0
  115. package/payload/platform/plugins/outlook/mcp/dist/lib/attachment-list.js +65 -0
  116. package/payload/platform/plugins/outlook/mcp/dist/lib/attachment-list.js.map +1 -0
  117. package/payload/platform/plugins/outlook/mcp/dist/lib/detach.d.ts +65 -0
  118. package/payload/platform/plugins/outlook/mcp/dist/lib/detach.d.ts.map +1 -0
  119. package/payload/platform/plugins/outlook/mcp/dist/lib/detach.js +99 -0
  120. package/payload/platform/plugins/outlook/mcp/dist/lib/detach.js.map +1 -0
  121. package/payload/platform/plugins/outlook/mcp/dist/lib/message.d.ts +8 -0
  122. package/payload/platform/plugins/outlook/mcp/dist/lib/message.d.ts.map +1 -1
  123. package/payload/platform/plugins/outlook/mcp/dist/lib/message.js +4 -0
  124. package/payload/platform/plugins/outlook/mcp/dist/lib/message.js.map +1 -1
  125. package/payload/platform/plugins/outlook/mcp/dist/lib/outbound-attachments.d.ts +36 -8
  126. package/payload/platform/plugins/outlook/mcp/dist/lib/outbound-attachments.d.ts.map +1 -1
  127. package/payload/platform/plugins/outlook/mcp/dist/lib/outbound-attachments.js +40 -13
  128. package/payload/platform/plugins/outlook/mcp/dist/lib/outbound-attachments.js.map +1 -1
  129. package/payload/platform/plugins/outlook/mcp/dist/lib/upload-session.d.ts +9 -5
  130. package/payload/platform/plugins/outlook/mcp/dist/lib/upload-session.d.ts.map +1 -1
  131. package/payload/platform/plugins/outlook/mcp/dist/lib/upload-session.js +12 -8
  132. package/payload/platform/plugins/outlook/mcp/dist/lib/upload-session.js.map +1 -1
  133. package/payload/platform/plugins/outlook/mcp/dist/tools/draft-edit.d.ts +49 -4
  134. package/payload/platform/plugins/outlook/mcp/dist/tools/draft-edit.d.ts.map +1 -1
  135. package/payload/platform/plugins/outlook/mcp/dist/tools/draft-edit.js +82 -13
  136. package/payload/platform/plugins/outlook/mcp/dist/tools/draft-edit.js.map +1 -1
  137. package/payload/platform/plugins/outlook/mcp/dist/tools/draft.d.ts +8 -2
  138. package/payload/platform/plugins/outlook/mcp/dist/tools/draft.d.ts.map +1 -1
  139. package/payload/platform/plugins/outlook/mcp/dist/tools/draft.js +13 -4
  140. package/payload/platform/plugins/outlook/mcp/dist/tools/draft.js.map +1 -1
  141. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-attachment.d.ts +15 -16
  142. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-attachment.d.ts.map +1 -1
  143. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-attachment.js +8 -41
  144. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-attachment.js.map +1 -1
  145. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-reply.d.ts.map +1 -1
  146. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-reply.js +3 -20
  147. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-reply.js.map +1 -1
  148. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-send.d.ts +27 -3
  149. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-send.d.ts.map +1 -1
  150. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-send.js +53 -8
  151. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-send.js.map +1 -1
  152. package/payload/platform/plugins/outlook/references/auth.md +1 -1
  153. package/payload/platform/plugins/outlook/references/graph-surfaces.md +33 -6
  154. package/payload/platform/plugins/outlook/skills/outlook/SKILL.md +22 -5
  155. package/payload/platform/plugins/storage-broker/PLUGIN.md +41 -2
  156. package/payload/platform/plugins/storage-broker/mcp/dist/__tests__/confine-path.test.d.ts +2 -0
  157. package/payload/platform/plugins/storage-broker/mcp/dist/__tests__/confine-path.test.d.ts.map +1 -0
  158. package/payload/platform/plugins/storage-broker/mcp/dist/__tests__/confine-path.test.js +85 -0
  159. package/payload/platform/plugins/storage-broker/mcp/dist/__tests__/confine-path.test.js.map +1 -0
  160. package/payload/platform/plugins/storage-broker/mcp/dist/__tests__/put-source.test.d.ts +2 -0
  161. package/payload/platform/plugins/storage-broker/mcp/dist/__tests__/put-source.test.d.ts.map +1 -0
  162. package/payload/platform/plugins/storage-broker/mcp/dist/__tests__/put-source.test.js +77 -0
  163. package/payload/platform/plugins/storage-broker/mcp/dist/__tests__/put-source.test.js.map +1 -0
  164. package/payload/platform/plugins/storage-broker/mcp/dist/confine-path.d.ts +12 -0
  165. package/payload/platform/plugins/storage-broker/mcp/dist/confine-path.d.ts.map +1 -0
  166. package/payload/platform/plugins/storage-broker/mcp/dist/confine-path.js +107 -0
  167. package/payload/platform/plugins/storage-broker/mcp/dist/confine-path.js.map +1 -0
  168. package/payload/platform/plugins/storage-broker/mcp/dist/index.js +86 -0
  169. package/payload/platform/plugins/storage-broker/mcp/dist/index.js.map +1 -1
  170. package/payload/platform/plugins/storage-broker/mcp/dist/put-source.d.ts +11 -0
  171. package/payload/platform/plugins/storage-broker/mcp/dist/put-source.d.ts.map +1 -0
  172. package/payload/platform/plugins/storage-broker/mcp/dist/put-source.js +35 -0
  173. package/payload/platform/plugins/storage-broker/mcp/dist/put-source.js.map +1 -0
  174. package/payload/platform/plugins/voice-mirror/PLUGIN.md +19 -7
  175. package/payload/platform/plugins/voice-mirror/mcp/dist/index.d.ts.map +1 -1
  176. package/payload/platform/plugins/voice-mirror/mcp/dist/index.js +35 -16
  177. package/payload/platform/plugins/voice-mirror/mcp/dist/index.js.map +1 -1
  178. package/payload/platform/plugins/voice-mirror/mcp/dist/lib/admin-user.d.ts +31 -0
  179. package/payload/platform/plugins/voice-mirror/mcp/dist/lib/admin-user.d.ts.map +1 -0
  180. package/payload/platform/plugins/voice-mirror/mcp/dist/lib/admin-user.js +87 -0
  181. package/payload/platform/plugins/voice-mirror/mcp/dist/lib/admin-user.js.map +1 -0
  182. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-distil-profile.d.ts.map +1 -1
  183. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-distil-profile.js +13 -3
  184. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-distil-profile.js.map +1 -1
  185. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-ingest-session-text.d.ts.map +1 -1
  186. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-ingest-session-text.js +2 -1
  187. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-ingest-session-text.js.map +1 -1
  188. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-record-feedback.d.ts.map +1 -1
  189. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-record-feedback.js +2 -1
  190. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-record-feedback.js.map +1 -1
  191. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-tag-content.d.ts +2 -2
  192. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-tag-content.d.ts.map +1 -1
  193. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-tag-content.js +12 -3
  194. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-tag-content.js.map +1 -1
  195. package/payload/platform/plugins/voice-mirror/mcp/scripts/smoke.mjs +334 -0
  196. package/payload/platform/plugins/voice-mirror/skills/voice-mirror/SKILL.md +13 -0
  197. package/payload/platform/scripts/logs-read-jsonl.test.sh +118 -0
  198. package/payload/platform/scripts/logs-read.sh +63 -9
  199. package/payload/platform/scripts/voice-author-key-audit.sh +155 -0
  200. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.d.ts.map +1 -1
  201. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js +4 -0
  202. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js.map +1 -1
  203. package/payload/platform/services/claude-session-manager/dist/mcp-descriptor-resolve.d.ts +17 -0
  204. package/payload/platform/services/claude-session-manager/dist/mcp-descriptor-resolve.d.ts.map +1 -0
  205. package/payload/platform/services/claude-session-manager/dist/mcp-descriptor-resolve.js +88 -0
  206. package/payload/platform/services/claude-session-manager/dist/mcp-descriptor-resolve.js.map +1 -0
  207. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts +0 -1
  208. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
  209. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +34 -23
  210. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
  211. package/payload/server/{chunk-64FGYKNZ.js → chunk-JXWFVE5X.js} +148 -0
  212. package/payload/server/{chunk-F4D35LKL.js → chunk-POBXIHOF.js} +34 -0
  213. package/payload/server/maxy-edge.js +1 -1
  214. package/payload/server/server.js +412 -37
  215. package/payload/server/{src-S7C4P6TT.js → src-4F37OHLK.js} +19 -1
  216. package/payload/platform/plugins/outlook/mcp/dist/__tests__/pkce-flow.test.d.ts +0 -2
  217. package/payload/platform/plugins/outlook/mcp/dist/__tests__/pkce-flow.test.js +0 -215
  218. package/payload/platform/plugins/outlook/mcp/dist/__tests__/pkce-flow.test.js.map +0 -1
@@ -14,14 +14,32 @@ export type RunFn = (
14
14
  env: Record<string, string>,
15
15
  ) => Promise<{ stdout: string }>;
16
16
 
17
- // The HTTP seam for the Cloudflare API. Injectable so r2List and d1Create are
17
+ // The HTTP seam for the Cloudflare API. Injectable so the API-backed methods are
18
18
  // unit-testable without a network. Minimal subset of the global fetch: callers
19
19
  // read the body once via text() and branch on ok/status. `body` is set for the
20
- // POST paths (d1Create); GET paths (r2List) omit it.
20
+ // POST/PUT paths (d1Create, r2ObjectPut); GET/DELETE paths omit it.
21
+ //
22
+ // Task 1691 widened both halves: `body` carries Uint8Array for the object-put
23
+ // path, and `arrayBuffer` reads the raw object body for r2ObjectGet, which is
24
+ // the one method whose response is not a JSON envelope. arrayBuffer is required
25
+ // rather than optional: an optional member would force a fake-only fallback
26
+ // branch in r2ObjectGet that never runs against a real fetch.
21
27
  export type FetchFn = (
22
28
  url: string,
23
- init: { method: string; headers: Record<string, string>; body?: string },
24
- ) => Promise<{ ok: boolean; status: number; text(): Promise<string> }>;
29
+ init: { method: string; headers: Record<string, string>; body?: string | Uint8Array },
30
+ ) => Promise<{
31
+ ok: boolean;
32
+ status: number;
33
+ text(): Promise<string>;
34
+ arrayBuffer(): Promise<ArrayBuffer>;
35
+ }>;
36
+
37
+ export interface R2Object {
38
+ key: string;
39
+ size: number;
40
+ etag: string;
41
+ lastModified: string;
42
+ }
25
43
 
26
44
  export interface CfExec {
27
45
  d1List(): Promise<{ name: string; uuid: string }[]>;
@@ -29,9 +47,34 @@ export interface CfExec {
29
47
  d1Query(name: string, sql: string): Promise<unknown>;
30
48
  r2List(): Promise<{ name: string }[]>;
31
49
  r2Create(name: string): Promise<void>;
50
+ r2ObjectList(bucket: string, prefix?: string): Promise<R2Object[]>;
51
+ r2ObjectFind(bucket: string, key: string): Promise<R2Object | null>;
52
+ r2ObjectGet(bucket: string, key: string): Promise<Uint8Array>;
53
+ r2ObjectPut(bucket: string, key: string, body: Uint8Array): Promise<void>;
54
+ r2ObjectDelete(bucket: string, key: string): Promise<void>;
55
+ }
56
+
57
+ // An object key carries `/` as a real path separator (the data portal's
58
+ // per-person prefixes), so it is encoded segment-wise rather than whole:
59
+ // separators stay separators, and spaces and specials are escaped. This
60
+ // encoding is not doc-confirmed; it is chosen so that a wrong guess surfaces as
61
+ // a 404 carrying its body, never as a silently misfiled object.
62
+ function encodeObjectKey(key: string): string {
63
+ return key.split("/").map(encodeURIComponent).join("/");
32
64
  }
33
65
 
34
- const defaultFetch: FetchFn = (url, init) => fetch(url, init);
66
+ // The single-object endpoint, shared by get/put/delete so the three cannot drift
67
+ // apart on encoding.
68
+ function objectUrl(accountId: string, bucket: string, key: string): string {
69
+ return `https://api.cloudflare.com/client/v4/accounts/${accountId}/r2/buckets/${encodeURIComponent(bucket)}/objects/${encodeObjectKey(key)}`;
70
+ }
71
+
72
+ // The cast is confined to this adapter. A Uint8Array is a valid fetch body at
73
+ // runtime, but the DOM lib's BodyInit does not admit TypeScript's generic
74
+ // Uint8Array<ArrayBufferLike>, so consumers compiled against DOM typings (the
75
+ // ui server) reject the call the Node typings accept. Casting here keeps the
76
+ // seam's own type honest for every caller and test.
77
+ const defaultFetch: FetchFn = (url, init) => fetch(url, init as RequestInit);
35
78
 
36
79
  const defaultRun: RunFn = (cmd, args, env) =>
37
80
  new Promise((resolve, reject) => {
@@ -62,7 +105,7 @@ function parseJson(raw: string, context: string): unknown {
62
105
  async function cfApiJson(
63
106
  fetchFn: FetchFn,
64
107
  url: string,
65
- init: { method: string; headers: Record<string, string>; body?: string },
108
+ init: { method: string; headers: Record<string, string>; body?: string | Uint8Array },
66
109
  context: string,
67
110
  ): Promise<{ parsed: unknown; body: string }> {
68
111
  const res = await fetchFn(url, init);
@@ -80,6 +123,81 @@ async function cfApiJson(
80
123
  return { parsed, body };
81
124
  }
82
125
 
126
+ // The one pagination loop for the object listing, shared by r2ObjectList and
127
+ // r2ObjectFind so the two cannot drift apart on cursor discipline — the same
128
+ // reason objectUrl is shared by get/put/delete.
129
+ //
130
+ // 1665's rules live here and nowhere else: follow the cursor to exhaustion,
131
+ // is_truncated is authoritative when present, a truncated page naming no cursor
132
+ // is an anomaly to surface rather than a partial list, and a repeated cursor is
133
+ // a cycle rather than progress. per_page is deliberately never sent — its
134
+ // default is unconfirmed, and omitting it takes whatever page size Cloudflare
135
+ // serves rather than guessing a value.
136
+ //
137
+ // It yields pages rather than returning a list, which is what lets r2ObjectFind
138
+ // stop early: an async generator is pull-based, so a consumer that stops
139
+ // iterating never triggers the next request.
140
+ async function* r2ObjectPages(
141
+ fetchFn: FetchFn,
142
+ cred: HouseCredential,
143
+ bucket: string,
144
+ prefix?: string,
145
+ ): AsyncGenerator<R2Object[]> {
146
+ const base = `https://api.cloudflare.com/client/v4/accounts/${cred.accountId}/r2/buckets/${encodeURIComponent(bucket)}/objects`;
147
+ const seen = new Set<string>();
148
+ let cursor: string | undefined;
149
+ for (;;) {
150
+ const params = new URLSearchParams();
151
+ if (prefix !== undefined) params.set("prefix", prefix);
152
+ if (cursor !== undefined) params.set("cursor", cursor);
153
+ const qs = params.toString();
154
+ const { parsed, body } = await cfApiJson(
155
+ fetchFn,
156
+ qs ? `${base}?${qs}` : base,
157
+ { method: "GET", headers: { Authorization: `Bearer ${cred.apiToken}` } },
158
+ "r2 object list",
159
+ );
160
+ const page = parsed as {
161
+ result?: Array<{ key: string; size: number; etag: string; last_modified: string }>;
162
+ result_info?: { cursor?: string | null; is_truncated?: boolean };
163
+ };
164
+ const objects: R2Object[] = [];
165
+ for (const o of page.result ?? []) {
166
+ objects.push({ key: o.key, size: o.size, etag: o.etag, lastModified: o.last_modified });
167
+ }
168
+ yield objects;
169
+ const truncated = page.result_info?.is_truncated;
170
+ const next = page.result_info?.cursor;
171
+ // is_truncated is authoritative *when present*: a last page may still carry
172
+ // a cursor, and following it would issue a pointless request or trip the
173
+ // cycle guard below on a healthy response.
174
+ if (truncated === false) return;
175
+ // The API says there is more but names no cursor: the response is not the
176
+ // shape we understand. Surface it rather than stop, which would silently
177
+ // truncate the audit's listing — the failure this loop exists to prevent.
178
+ if (truncated === true && !next) {
179
+ throw new Error(
180
+ `storage-broker: r2 object list reported truncated but gave no pagination cursor: ${body}`,
181
+ );
182
+ }
183
+ // is_truncated absent: it is itself an unmeasured fact, so it must not be
184
+ // what decides correctness. Fall back to cursor presence — the rule r2List
185
+ // uses on the same envelope — so an envelope without the flag paginates
186
+ // rather than truncating silently.
187
+ if (!next) return;
188
+ // A cursor seen before means the API is cycling rather than advancing: it
189
+ // would loop forever, re-yielding the same objects each pass. Tracking every
190
+ // cursor (not just the last) also catches a cycle of period >= 2.
191
+ if (seen.has(next)) {
192
+ throw new Error(
193
+ `storage-broker: r2 object list returned a repeated pagination cursor: ${next}`,
194
+ );
195
+ }
196
+ seen.add(next);
197
+ cursor = next;
198
+ }
199
+ }
200
+
83
201
  export function makeCfExec(
84
202
  cred: HouseCredential,
85
203
  run: RunFn = defaultRun,
@@ -182,6 +300,103 @@ export function makeCfExec(
182
300
  async r2Create(name) {
183
301
  await run("npx", ["wrangler", "r2", "bucket", "create", name], env);
184
302
  },
303
+
304
+ // --- R2 objects (Task 1691) -------------------------------------------
305
+ //
306
+ // The v4 REST API exposes the object surface under
307
+ // /accounts/{id}/r2/buckets/{bucket}/objects and accepts the same Bearer
308
+ // token as the bucket endpoints (account-level Workers R2 Storage Write).
309
+ // The r2/api/tokens/ note that "Object Read & Write" is S3-only constrains
310
+ // the bucket-item-scoped *permission types*, not these endpoints.
311
+ //
312
+ // The endpoint's existence and token class are doc-confirmed. Its behaviour
313
+ // was unmeasured until Task 1697, which probed the live v4 API on 2026-07-16
314
+ // from the laptop's own maxy-code install (no house Cloudflare credential
315
+ // exists on the dev Mac, which is why the tests fake the fetch seam). What
316
+ // that probe established, so nobody re-guesses it:
317
+ //
318
+ // - The listing is NOT in ascending UTF-8 binary order. `prefix=a/photo.jpg`
319
+ // returns `a/photo.jpg.bak` before `a/photo.jpg`, so the exact key is not
320
+ // the first element and `per_page=1` returns a neighbour. S3's
321
+ // ListObjectsV2 ordering guarantee does not transfer to this endpoint.
322
+ // - HEAD is routed only for single-segment keys: any key containing "/"
323
+ // returns 405 while GET on the identical URL returns 404.
324
+ // - A ranged GET is honoured at 2 MB and ignored at 12 B, so it cannot size
325
+ // an object without an undocumented size threshold deciding correctness.
326
+ // - per_page is bounded 0..1000 (error 10029), and per_page=0 returns zero
327
+ // objects with is_truncated:true — a loop trap. It is still never sent.
328
+ //
329
+ // The design keeps every unmeasured fact unable to affect correctness:
330
+ // per_page is never sent, order is never assumed, and nothing fails
331
+ // silently. Evidence:
332
+ // docs/superpowers/specs/2026-07-16-storage-broker-get-sizing-early-exit-design.md
333
+ async r2ObjectList(bucket, prefix) {
334
+ // The standing audit needs the complete set: a truncated object listing
335
+ // reads as "those objects are absent", producing false phantom rows and
336
+ // hiding real orphans — the same bug r2List had, one endpoint over. So
337
+ // this drains every page. r2ObjectFind is the caller that needs one
338
+ // record and must not pay for this.
339
+ const objects: R2Object[] = [];
340
+ for await (const page of r2ObjectPages(fetchFn, cred, bucket, prefix)) {
341
+ for (const o of page) objects.push(o);
342
+ }
343
+ return objects;
344
+ },
345
+ async r2ObjectFind(bucket, key) {
346
+ // Task 1697 — the size source for a get. Scans each page for the exact key
347
+ // and returns on the first match, so it holds one page rather than every
348
+ // object sharing the key as a prefix, and issues no request past the page
349
+ // carrying the match.
350
+ //
351
+ // Exact match, not prefix: a prefix of `a/photo.jpg` also matches
352
+ // `a/photo.jpg.bak`, and sizing the neighbour would either reject a legal
353
+ // get or admit an oversized one. Exiting on the match rather than on
354
+ // position is what makes this independent of listing order — measured
355
+ // 2026-07-16 against the live v4 API, the listing is NOT in ascending
356
+ // UTF-8 binary order, so the exact key is not the first element and
357
+ // `per_page=1` returns a neighbour.
358
+ //
359
+ // An absent key still drains every page, because proving absence requires
360
+ // seeing them all. That residual is accepted and permanent; nothing on
361
+ // this endpoint sizes one object in bounded work. See
362
+ // docs/superpowers/specs/2026-07-16-storage-broker-get-sizing-early-exit-design.md
363
+ for await (const page of r2ObjectPages(fetchFn, cred, bucket, key)) {
364
+ const hit = page.find((o) => o.key === key);
365
+ if (hit) return hit;
366
+ }
367
+ return null;
368
+ },
369
+ async r2ObjectGet(bucket, key) {
370
+ // The one method cfApiJson cannot wrap: get returns the raw object body,
371
+ // not a success envelope, so there is nothing to parse or assert
372
+ // success:true on. The error body is still retained on a non-2xx.
373
+ const res = await fetchFn(objectUrl(cred.accountId, bucket, key), {
374
+ method: "GET",
375
+ headers: { Authorization: `Bearer ${cred.apiToken}` },
376
+ });
377
+ if (!res.ok) {
378
+ throw new Error(
379
+ `storage-broker: r2 object get failed: ${res.status}\n${await res.text()}`,
380
+ );
381
+ }
382
+ return new Uint8Array(await res.arrayBuffer());
383
+ },
384
+ async r2ObjectPut(bucket, key, body) {
385
+ await cfApiJson(
386
+ fetchFn,
387
+ objectUrl(cred.accountId, bucket, key),
388
+ { method: "PUT", headers: { Authorization: `Bearer ${cred.apiToken}` }, body },
389
+ "r2 object put",
390
+ );
391
+ },
392
+ async r2ObjectDelete(bucket, key) {
393
+ await cfApiJson(
394
+ fetchFn,
395
+ objectUrl(cred.accountId, bucket, key),
396
+ { method: "DELETE", headers: { Authorization: `Bearer ${cred.apiToken}` } },
397
+ "r2 object delete",
398
+ );
399
+ },
185
400
  };
186
401
  }
187
402
 
@@ -2,8 +2,10 @@
2
2
  // config directory. This file is the one place the account-wide capability
3
3
  // lives. House-only is a location convention plus the cf-token.sh caller gate,
4
4
  // NOT an OS permission: every account's session runs as the same unix user, so
5
- // a sub-account agent holding Bash can read this path directly. Task 1690
6
- // tracks the OS user separation that would make this a real boundary. Parses
5
+ // a sub-account agent holding Bash can read this path directly. That is a
6
+ // permanent property, not a pending gap: OS user separation was costed and
7
+ // declined as disproportionate (Task 1690, archived undone), so nothing tracks
8
+ // it. Do not cite this file's location or mode as a boundary. Parses
7
9
  // KEY=value lines without polluting process.env (same discipline as
8
10
  // reconcile-bookings' readEnvFile).
9
11
 
@@ -7,7 +7,9 @@
7
7
  // isolation and the isolation logic auditable in one place.
8
8
 
9
9
  export * from "./authorize.js";
10
+ export * from "./object-limits.js";
10
11
  export * from "./registry.js";
12
+ export * from "./resource-limits.js";
11
13
  export * from "./house-credential.js";
12
14
  export * from "./cf-exec.js";
13
15
  export * from "./house-scoped-token.js";
@@ -0,0 +1,95 @@
1
+ /**
2
+ * The byte cap on R2 object transfer, and the arithmetic the routes need to
3
+ * enforce it before they buffer anything.
4
+ *
5
+ * One number, one home (Task 1695). The storage-broker MCP tools and the house
6
+ * ui routes both read the cap from here, so the process that rejects a too-large
7
+ * file and the process that would otherwise buffer it cannot drift apart.
8
+ *
9
+ * Deliberately dependency-free: the MCP imports this module directly, without
10
+ * the rest of the lib (cf-exec, registry, audit, remediate), which it has no use
11
+ * for. The ui route reads it through the barrel.
12
+ */
13
+
14
+ /**
15
+ * The maximum size, in bytes, of an object moved through storage-r2-object-get
16
+ * or storage-r2-object-put.
17
+ *
18
+ * Set independently of the email plugin's OUTBOUND_ATTACHMENT_MAX_BYTES (25 MiB)
19
+ * even though the payload class is the same. The two exist for different reasons
20
+ * and must be free to move for different reasons: email's tracks an SMTP-side
21
+ * rejection threshold, this one tracks house-process memory. Do not couple them.
22
+ *
23
+ * Not bounded by R2. A single-part upload may be up to 5 GiB (doc-confirmed:
24
+ * developers.cloudflare.com/r2/platform/limits/), roughly fifty times this, and
25
+ * the v4 REST API states no separate object-size limit. This cap is the only
26
+ * bound on either transfer.
27
+ *
28
+ * The bound is not small: at the cap a get still leaves the house process
29
+ * holding the object and its base64 form, roughly 2.4x the object. That cost was
30
+ * weighed and accepted when the number was chosen.
31
+ */
32
+ export const R2_OBJECT_MAX_BYTES = 100 * 1024 * 1024;
33
+
34
+ /** base64 emits 4 characters per 3 input bytes, padded up to the next quantum. */
35
+ export function base64Ceiling(bytes: number): number {
36
+ return 4 * Math.ceil(bytes / 3);
37
+ }
38
+
39
+ /**
40
+ * The largest JSON body an object route's metadata envelope can legitimately
41
+ * carry: a bucket name, one key or prefix, and JSON punctuation. No payload term.
42
+ *
43
+ * Used twice, because it answers one question — how large can an envelope
44
+ * legitimately get. `/r2/object/list`, `get` and `delete` carry an envelope and
45
+ * nothing else, so this is their whole body limit (Task 1698). `put` carries the
46
+ * same envelope plus base64 object bytes, so it adds this to the base64 ceiling
47
+ * below, which is the allowance that keeps a legal at-cap put from being rejected
48
+ * by its own key.
49
+ *
50
+ * Deliberately one constant rather than two that happen to match. This is the
51
+ * opposite ruling to the email cap above, for the opposite reason: that pair must
52
+ * stay independent because the numbers track different constraints and would drift
53
+ * for different reasons. Here the constraint is identical, so a change to either
54
+ * use must move both.
55
+ *
56
+ * Sized from confirmed R2 limits rather than assumed. An object key is at most
57
+ * 1,024 bytes (developers.cloudflare.com/r2/platform/limits/) and a bucket name is
58
+ * 3-63 characters (developers.cloudflare.com/r2/buckets/create-buckets/), so a
59
+ * real envelope is about 1.1 KB, or roughly 6 KiB if every key byte takes a
60
+ * six-character \uXXXX escape. This clears that by about ten times and sits about
61
+ * 2,100 times below put's body limit: far above anything a real caller sends, far
62
+ * below anything that threatens house memory. Nothing here depends on either
63
+ * figure being exact.
64
+ */
65
+ export const R2_OBJECT_ENVELOPE_MAX_BODY_BYTES = 64 * 1024;
66
+
67
+ /**
68
+ * The largest JSON body /r2/object/put can legitimately carry.
69
+ *
70
+ * This bounds house memory; it does not adjudicate the last kilobyte. The exact
71
+ * decoded-byte rule lives in the MCP, which knows the source's size exactly.
72
+ */
73
+ export const R2_OBJECT_PUT_MAX_BODY_BYTES =
74
+ base64Ceiling(R2_OBJECT_MAX_BYTES) + R2_OBJECT_ENVELOPE_MAX_BODY_BYTES;
75
+
76
+ function formatMiB(bytes: number): string {
77
+ return (bytes / 1024 / 1024).toFixed(1);
78
+ }
79
+
80
+ /**
81
+ * The over-cap message, worded once so the MCP and the routes agree. Names the
82
+ * offending object and the limit, which is what makes the failure actionable
83
+ * rather than an opaque out-of-memory kill.
84
+ *
85
+ * Exact bytes accompany the MiB figures because MiB alone rounds: an object one
86
+ * byte over the cap reads "100.0 MiB, which exceeds the 100.0 MiB limit", which
87
+ * is a message that appears to contradict itself and invites the reader to treat
88
+ * the rejection as a bug. The bytes are what make it legible; the MiB is what
89
+ * makes it quick.
90
+ */
91
+ export function tooLargeMessage(what: string, actualBytes: number): string {
92
+ return `${what} is ${formatMiB(actualBytes)} MiB (${actualBytes} bytes), which exceeds the ${formatMiB(
93
+ R2_OBJECT_MAX_BYTES,
94
+ )} MiB (${R2_OBJECT_MAX_BYTES} bytes) R2 object limit.`;
95
+ }
@@ -0,0 +1,105 @@
1
+ /**
2
+ * The bounds on the request bodies the resource-level routes will buffer:
3
+ * /d1/create, /d1/query and /r2/create (Task 1702).
4
+ *
5
+ * Parallel to object-limits.ts, which bounds the object routes. Kept separate
6
+ * because the facts differ: an object envelope is dominated by R2's 1,024-byte
7
+ * object key limit, and these routes carry no key at all. Coupling them would tie
8
+ * a D1 database name to R2's key limit, so a revision to either would move a bound
9
+ * that has no reason to move.
10
+ *
11
+ * These are bounds on house memory. They do not adjudicate the last byte of a name
12
+ * or a statement — the same stance object-limits.ts takes, for the same reason. The
13
+ * house process holds the account-wide credential, so an out-of-memory kill there
14
+ * takes the brand server down, and on the target hardware that is a hard freeze
15
+ * that emits nothing.
16
+ *
17
+ * Deliberately dependency-free, like object-limits.ts: the ui route reads these
18
+ * through the barrel, and nothing here needs the rest of the lib.
19
+ */
20
+
21
+ /**
22
+ * Cloudflare's documented maximum SQL statement length.
23
+ *
24
+ * "Maximum SQL statement length | 100,000 bytes (100 KB)"
25
+ * https://developers.cloudflare.com/d1/platform/limits/
26
+ *
27
+ * Recorded because it is what governs a legitimate `sql`, and because
28
+ * D1_QUERY_MAX_BODY_BYTES below is only knowable as safe by way of it.
29
+ *
30
+ * A second, independent bound applies to the same string on the way out, and is
31
+ * recorded here so nobody rediscovers it as a bug: cf-exec's d1Query shells
32
+ * `execFile("npx", ["wrangler", "d1", "execute", ..., "--command", sql])`, so the
33
+ * decoded statement travels as a single argv element. Linux caps one such element
34
+ * at MAX_ARG_STRLEN = 131,072 bytes and fails execve with E2BIG above it
35
+ * (https://man7.org/linux/man-pages/man2/execve.2.html). That is a property of the
36
+ * spawn path, not of D1. It does not bind first: 100,000 < 131,072, so a statement
37
+ * D1 would accept always fits the spawn.
38
+ */
39
+ export const D1_MAX_SQL_STATEMENT_BYTES = 100_000;
40
+
41
+ /**
42
+ * The worst-case expansion of a byte when it is serialized into a JSON string.
43
+ *
44
+ * A single ASCII control byte becomes the six characters \uXXXX. Multi-byte
45
+ * characters are cheaper per byte — a 3-byte UTF-8 character also becomes six
46
+ * characters (2x), and a non-BMP character becomes a 12-character surrogate pair
47
+ * for 4 bytes (3x) — so 6 is a true ceiling over bytes, reached by ASCII.
48
+ *
49
+ * This is why a body limit is not simply the statement limit: the cap bounds the
50
+ * wire body, and escaping moves the two apart.
51
+ */
52
+ export const JSON_STRING_WORST_CASE_EXPANSION = 6;
53
+
54
+ /**
55
+ * The largest JSON body /d1/create or /r2/create can legitimately carry: a single
56
+ * resource name and JSON punctuation. No payload term, and no key term.
57
+ *
58
+ * Sized on headroom rather than a tight derivation, because only one of the two
59
+ * names it bounds has a documented hard limit:
60
+ *
61
+ * - R2 bucket name: 3-63 characters, lowercase letters, digits and hyphens.
62
+ * https://developers.cloudflare.com/r2/buckets/create-buckets/
63
+ * - D1 database name: no documented limit. The API reference states only
64
+ * `name: string`, "D1 database name" — no maxLength, pattern or charset.
65
+ * https://developers.cloudflare.com/api/resources/d1/subresources/database/
66
+ * The get-started guide gives guidance, not a limit: names "should use a
67
+ * combination of ASCII characters, shorter than 32 characters".
68
+ *
69
+ * So a real body is under ~100 bytes and 64 KiB clears it by roughly 650x, while
70
+ * sitting ~2,100x below put's body limit: far above anything a real caller sends,
71
+ * far below anything that threatens house memory. Nothing here depends on either
72
+ * figure being exact. An undocumented D1 name limit cannot plausibly approach 64
73
+ * KiB, and a request body is the wrong place to discover that it had.
74
+ *
75
+ * Deliberately not R2_OBJECT_ENVELOPE_MAX_BODY_BYTES, which happens to hold the
76
+ * same value. That constant is governed by R2's object key limit; this one is not,
77
+ * and the two must be free to drift. Couple what is one fact; separate what merely
78
+ * matches today.
79
+ */
80
+ export const STORAGE_RESOURCE_NAME_MAX_BODY_BYTES = 64 * 1024;
81
+
82
+ /**
83
+ * The largest JSON body /d1/query can legitimately carry: a database name and a
84
+ * SQL statement.
85
+ *
86
+ * The envelope reasoning behind the object routes' bound does not govern this. A
87
+ * large but entirely legal migration or batch statement is not a 1,024-byte object
88
+ * key, so applying that constant here would reject correct queries.
89
+ *
90
+ * The bound is on the wire body, not the decoded statement, and JSON escaping moves
91
+ * those apart. The largest legal statement (D1_MAX_SQL_STATEMENT_BYTES) can present
92
+ * a wire body of up to
93
+ *
94
+ * JSON_STRING_WORST_CASE_EXPANSION * 100,000 + 65,536 = 665,536 bytes
95
+ *
96
+ * so capping at 100,000 would 413 a statement D1 would have accepted. 1 MiB clears
97
+ * the worst case by about 1.6x and sits ~133x below put's body limit.
98
+ *
99
+ * It is a flat number rather than that expression, for legibility at the call site.
100
+ * The arithmetic is not decoration: it is the only thing that makes 1 MiB knowable
101
+ * as safe, so resource-limits.test.ts asserts the relationship. If D1 ever raises
102
+ * its statement limit above 163,840 bytes, that test fails rather than this route
103
+ * quietly beginning to refuse legal SQL.
104
+ */
105
+ export const D1_QUERY_MAX_BODY_BYTES = 1024 * 1024;
@@ -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:d31a178be753e371f5773e362173eb0090115ababe81563d2b5f38890098202c
4
+ content-hash: sha256:c7d0d9f383f8d9ee8ed50b8869ce43477e3cb5ebf62305a39041c072e34bfeb1
5
5
  brand: maxy-code
6
6
  product-name: Maxy
7
7
  ---
@@ -2057,7 +2057,7 @@ The `outlook` plugin gives the admin agent access to Microsoft 365 / Outlook.com
2057
2057
  | `outlook-mail-list` | Mail newest-first. Default top=25, folder=Inbox. `folder` takes a well-known name or alias (case/space-insensitive): Inbox, Sent Items/sent, Drafts, Deleted Items/trash, Junk Email/junk/spam, Archive, Outbox — or a raw folder id. Custom folder names are not resolved. `since`/`before` (ISO date-times) narrow by receipt date. The 250-per-page figure is not a ceiling: the reply carries `nextCursor` when older mail remains — pass it back as `cursor` to page further back. Filter by sender/subject via `outlook-mail-search`. |
2058
2058
  | `outlook-mail-search` | Microsoft Graph `$search` (KQL) over the mailbox. Preview-only, like `outlook-mail-list`, and pages the same way via `nextCursor`/`cursor`. Put sender/subject/recipient/date filters in the query: `from:`, `to:`, `subject:`, `received>=`. Graph forbids `$search`+`$filter`, so a strict date window is on `outlook-mail-list`. |
2059
2059
  | `outlook-mail-fetch-body` | Complete body of one message by id (`GET /me/messages/{id}?$select=body`). HTML decoded to text, no preview cap. The full-read path — list/search return Microsoft's ~255-char preview only. Returns the envelope plus the whole body. |
2060
- | `outlook-mail-attachment` | List or download a message's inbound attachments. With `messageId` alone, LISTS them (`id`, `name`, `contentType`, `size`, `isInline`, and a `kind` of file/item/reference) via `GET /me/messages/{id}/attachments` and `$select` (no bytes). With `messageId`+`attachmentId`, DOWNLOADS one file attachment: its bytes are written to `{accountDir}/uploads/outlook/<messageHash>/<bytesHash>-<name>` at `0o600` and the saved path is returned, ready for the Read tool, `SendUserFile`, or an `outlook-mail-reply` attachment. Only file attachments download — item/reference kinds, empty payloads, and anything over 25 MB are refused with nothing written. `outlook-mail-list` / `outlook-mail-search` / `outlook-mail-fetch-body` report `hasAttachments` so the agent knows when to call this. Needs `Mail.Read`. |
2060
+ | `outlook-mail-attachment` | List or download any message's attachments — received or draft. With `messageId` alone, LISTS them (`id`, `name`, `contentType`, `size`, `isInline`, and a `kind` of file/item/reference) via `GET /me/messages/{id}/attachments` and `$select` (no bytes). A draft is a message in the same collection, so passing a draftId lists what that draft carries; the returned `name`/`id` is what `outlook-draft-edit`'s `removeAttachments` takes. With `messageId`+`attachmentId`, DOWNLOADS one file attachment: its bytes are written to `{accountDir}/uploads/outlook/<messageHash>/<bytesHash>-<name>` at `0o600` and the saved path is returned, ready for the Read tool, `SendUserFile`, or an `outlook-mail-reply` attachment. Only file attachments download — item/reference kinds, empty payloads, and anything over 25 MB are refused with nothing written. `outlook-mail-list` / `outlook-mail-search` / `outlook-mail-fetch-body` report `hasAttachments` so the agent knows when to call this. Needs `Mail.Read`. |
2061
2061
  | `outlook-mail-reply` | Reply in-thread (`createReply` / `createReplyAll` + send). Threads natively via conversationId. `replyAll`, added cc/bcc, and account-scoped file attachments (up to 25 MB per file; files over Graph's ~3 MB inline limit use a chunked upload session). Body is plain text, prepended above the quote. Needs `Mail.ReadWrite` + `Mail.Send`. |
2062
2062
  | `outlook-mail-delete` | Move messages to Deleted Items (`POST /me/messages/{id}/move`). Recoverable — never hard-deletes. An id already gone is counted not-moved. Needs `Mail.ReadWrite`. |
2063
2063
  | `outlook-mail-otp-extract` | Poll the inbox for a one-time code from a sender (domain). Reads full bodies to extract it; optional subject regex and timeout (default 60 s). |
@@ -2071,9 +2071,9 @@ The `outlook` plugin gives the admin agent access to Microsoft 365 / Outlook.com
2071
2071
  | `outlook-contacts-list` | Top contacts. Default top=50. |
2072
2072
  | `outlook-mailbox-info` | Health probe for one mailbox — auth state, refresh-window, folder count. Name the mailbox with `mailbox` when several are attached. |
2073
2073
  | `outlook-mailbox-list` | Every connected mailbox with its email, scopes, and token health. Local only — no network — so it also flags a mailbox whose stored credentials cannot be read. |
2074
- | `outlook-mail-send` | Send a message (`POST /me/sendMail`). to/cc/bcc arrays, subject, body, isHtml. Success asserted on Graph 202. Needs `Mail.Send`. |
2075
- | `outlook-draft` | Create a draft in Drafts (`POST /me/messages`). Same body model. Returns the draft id. Needs `Mail.ReadWrite`. |
2076
- | `outlook-draft-edit` | Update an existing draft in place (`PATCH /me/messages/{draftId}`). Graph drafts are mutable — pass draftId plus the fields to change (to/cc/bcc/subject/body/isHtml). Sent messages cannot be edited. Needs `Mail.ReadWrite`. |
2074
+ | `outlook-mail-send` | Send a message. No attachments → `POST /me/sendMail`; with attachments → draft, attach, send (`/me/sendMail` carries attachments inline and cannot fit a large file). to/cc/bcc arrays, subject, body, isHtml, attachments (account-dir paths, 25 MB/file). Success asserted on Graph 202; no message id on either route. Needs `Mail.Send` (+ `Mail.ReadWrite` to attach). |
2075
+ | `outlook-draft` | Create a draft in Drafts (`POST /me/messages`). Same body model, including attachments — files are attached before the id is returned, so the draft is ready to send. Returns the draft id. Needs `Mail.ReadWrite`. |
2076
+ | `outlook-draft-edit` | Update an existing draft in place (`PATCH /me/messages/{draftId}`) and/or add and remove attachments. Graph drafts are mutable — pass draftId plus the fields to change (to/cc/bcc/subject/body/isHtml/attachments/removeAttachments). `attachments` adds; `removeAttachments` takes files off by name or attachment id; both together replace a file in one call. Sent messages cannot be edited. Needs `Mail.ReadWrite`. |
2077
2077
  | `outlook-draft-send` | Send an existing draft by id (`POST /me/messages/{id}/send`). Graph consumes the draft, returns 202. Needs `Mail.Send`. |
2078
2078
 
2079
2079
  ## Observability
@@ -2148,7 +2148,7 @@ Latency triage: `mail-list count=0 elapsedMs<200` consistent → permissions iss
2148
2148
 
2149
2149
  ## Out of scope
2150
2150
 
2151
- Mail move to arbitrary folders / flag, hard (permanent) delete, ingesting reviewed mail into the business graph (the `email-ingest` analogue), contacts write, recurring-series occurrence editing, the `scheduling` Neo4j calendar and any auto-sync with it, OneDrive / Files, push notifications, on-premises Exchange, M365 admin scopes (`User.Read.All`, `AuditLog.Read.All`), public-agent exposure, multi-tenant federation. Mail read, full-body read, inbound attachment list/download, reply, delete (to Deleted Items), and compose are supported (`outlook-mail-fetch-body`, `outlook-mail-attachment`, `outlook-mail-reply`, `outlook-mail-delete`, `outlook-mail-otp-extract`, `outlook-mail-send`, `outlook-draft`, `outlook-draft-edit`, `outlook-draft-send`), and the calendar is read plus control. See `platform/plugins/outlook/PLUGIN.md` for the full out-of-scope list.
2151
+ Mail move to arbitrary folders / flag, hard (permanent) delete, removing an attachment from a **sent** message (Graph forbids it), ingesting reviewed mail into the business graph (the `email-ingest` analogue), contacts write, recurring-series occurrence editing, the `scheduling` Neo4j calendar and any auto-sync with it, OneDrive / Files, push notifications, on-premises Exchange, M365 admin scopes (`User.Read.All`, `AuditLog.Read.All`), public-agent exposure, multi-tenant federation. Mail read, full-body read, inbound attachment list/download, reply, delete (to Deleted Items), and compose — all four outbound paths carrying attachments, with a draft's attachments editable in place (add, remove, replace) — are supported (`outlook-mail-fetch-body`, `outlook-mail-attachment`, `outlook-mail-reply`, `outlook-mail-delete`, `outlook-mail-otp-extract`, `outlook-mail-send`, `outlook-draft`, `outlook-draft-edit`, `outlook-draft-send`), and the calendar is read plus control. See `platform/plugins/outlook/PLUGIN.md` for the full out-of-scope list.
2152
2152
 
2153
2153
  ---
2154
2154
  # QuickBooks Online
@@ -2715,6 +2715,7 @@ Voice mirror is on by default for every drafting skill. To opt out for one, set
2715
2715
  - **Copy public figures** — voice mirror only learns from your own writing.
2716
2716
  - **Clone audio** — text only, no speech synthesis.
2717
2717
  - **Guess** — historical content stays `unknown` until you mark it. Maxy never auto-classifies your writing. (Automatic ingestion applies only to your live PTY sessions where authorship is certain.)
2718
+ - **Invent an identity** — a voice belongs to a real admin user on the account. Attribution is refused outright if it names anyone else, and nothing is written. A person's name is not an identity, so tagging someone's writing on a shared account needs their actual admin id, not their name.
2718
2719
 
2719
2720
  ## Status
2720
2721
 
@@ -32,7 +32,9 @@ The plugin registers no agent-facing MCP tools. Every Cloudflare operation is th
32
32
  | Skill | Purpose |
33
33
  |---|---|
34
34
  | [cloudflare/SKILL.md](skills/cloudflare/SKILL.md) | The entry point for every Cloudflare operation. Routes each operation class to its reference, names the outcome contracts (tunnel external HTTP 200; hosting deployed URL; D1 round-trip), and states the tool-discipline + secret-redaction rules that bind the agent. |
35
- | [site-deploy/SKILL.md](skills/site-deploy/SKILL.md) | Assemble one canonical content tree into a live Pages deployment on its custom domain that survives the install device being offline. Names the single-source-tree convention, the reused per-scope token, the form-to-D1 trigger, Open Graph correctness, and the cache-busted live-`200` done-gate; defers the command blocks to `hosting-sites.md` and `d1-data-capture.md`. Admin-run; a content specialist hands the deploy off to it. |
35
+ | [site-deploy/SKILL.md](skills/site-deploy/SKILL.md) | Assemble one canonical content tree into a live Pages deployment on its custom domain that survives the install device being offline. Names the single-source-tree convention, the reused per-scope token, the form-to-D1 trigger, the R2 binding trigger, Open Graph correctness, and the cache-busted live-`200` done-gate; defers the command blocks to `hosting-sites.md`, `d1-data-capture.md` and `r2-object-storage.md`. Admin-run; a content specialist hands the deploy off to it. |
36
+ | [calendar-site/SKILL.md](skills/calendar-site/SKILL.md) | Assemble and deploy one account's public booking page — a static page plus edge Functions over a D1 `bookings` table — on its own custom domain, so a submission survives the install device being offline. Admin-run; defers the deploy and the live done-gate to `site-deploy`. |
37
+ | [data-portal/SKILL.md](skills/data-portal/SKILL.md) | Stand up one account's gated file-drop portal on its own custom domain: each enrolled person signs in with their own passcode and can upload, list, download and delete only their own files, into one R2 bucket under a per-person prefix with a D1 manifest row per upload. The bucket is created by the owning account's own session — the broker registers ownership to the creator and never reassigns it — while the admin assembles and deploys. Admin-run; defers the deploy and the live done-gate to `site-deploy`. |
36
38
 
37
39
  ### References
38
40
 
@@ -46,9 +46,10 @@ case "$scope" in pages|d1|storage|calendar-d1|dns|access) ;; *) die "unknown sco
46
46
  # specialist. Every account's session runs as the SAME OS user (no uid/gid is
47
47
  # dropped at spawn) and the house env path is deterministic, so an agent holding
48
48
  # Bash can read that file directly or export a false ACCOUNT_ID. Real enforcement
49
- # needs OS user separation; until that lands this is a boundary of intent, not of
50
- # permission. Do not cite this gate as proof a sub-account cannot reach the
51
- # master.
49
+ # needs OS user separation, which was costed and declined as disproportionate
50
+ # (Task 1690, archived undone). This is therefore permanently a boundary of
51
+ # intent, not of permission -- no follow-up tracks closing it. Do not cite this
52
+ # gate as proof a sub-account cannot reach the master.
52
53
  #
53
54
  # "House" is an exact shell mirror of resolveAccountDir + isHouseAccount
54
55
  # (platform/services/claude-session-manager/src/config.ts:238-323) -- the same