matrx-files 0.1.0__py3-none-any.whl

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 (205) hide show
  1. matrx_files/FEATURE.md +230 -0
  2. matrx_files/MODULE_README.md +242 -0
  3. matrx_files/__init__.py +395 -0
  4. matrx_files/_config.py +89 -0
  5. matrx_files/analysis/__init__.py +104 -0
  6. matrx_files/analysis/context.py +52 -0
  7. matrx_files/analysis/dispatcher.py +547 -0
  8. matrx_files/analysis/label_catalog.py +249 -0
  9. matrx_files/analysis/ocr_router.py +89 -0
  10. matrx_files/analysis/persistence.py +293 -0
  11. matrx_files/analysis/preferences.py +153 -0
  12. matrx_files/analysis/protocol.py +165 -0
  13. matrx_files/analysis/registry.py +129 -0
  14. matrx_files/analysis/text_sanitize.py +87 -0
  15. matrx_files/api/__init__.py +23 -0
  16. matrx_files/api/router.py +24 -0
  17. matrx_files/api/router_assets.py +344 -0
  18. matrx_files/api/router_files.py +497 -0
  19. matrx_files/api/router_share.py +71 -0
  20. matrx_files/asset_builder.py +239 -0
  21. matrx_files/asset_envelope.py +462 -0
  22. matrx_files/backends/__init__.py +33 -0
  23. matrx_files/backends/base_backend.py +192 -0
  24. matrx_files/backends/llm_helpers.py +189 -0
  25. matrx_files/backends/router.py +638 -0
  26. matrx_files/backends/s3_backend.py +625 -0
  27. matrx_files/backends/server_backend.py +358 -0
  28. matrx_files/backends/supabase_backend.py +438 -0
  29. matrx_files/backends/url_parser.py +215 -0
  30. matrx_files/base_handler.py +21 -0
  31. matrx_files/batch_handler.py +89 -0
  32. matrx_files/cloud_mixin.py +202 -0
  33. matrx_files/cloud_sync/__init__.py +117 -0
  34. matrx_files/cloud_sync/_media_resolution.py +606 -0
  35. matrx_files/cloud_sync/backfill/__init__.py +17 -0
  36. matrx_files/cloud_sync/backfill/rekey.py +267 -0
  37. matrx_files/cloud_sync/backfill/restamp_headers.py +235 -0
  38. matrx_files/cloud_sync/backfill/thumbnails.py +261 -0
  39. matrx_files/cloud_sync/byte_cache.py +158 -0
  40. matrx_files/cloud_sync/cdn.py +184 -0
  41. matrx_files/cloud_sync/config.py +125 -0
  42. matrx_files/cloud_sync/db.py +974 -0
  43. matrx_files/cloud_sync/download_stream.py +251 -0
  44. matrx_files/cloud_sync/events/__init__.py +51 -0
  45. matrx_files/cloud_sync/events/default_logger.py +137 -0
  46. matrx_files/cloud_sync/idempotency.py +150 -0
  47. matrx_files/cloud_sync/media_ref.py +328 -0
  48. matrx_files/cloud_sync/migrations.py +165 -0
  49. matrx_files/cloud_sync/models.py +247 -0
  50. matrx_files/cloud_sync/permissions.py +489 -0
  51. matrx_files/cloud_sync/processing/__init__.py +8 -0
  52. matrx_files/cloud_sync/processing/thumbnails.py +214 -0
  53. matrx_files/cloud_sync/sql/001_initial_schema.sql +595 -0
  54. matrx_files/cloud_sync/sql/002_guest_support.sql +96 -0
  55. matrx_files/cloud_sync/sql/003_security_correctness_quotas.sql +1058 -0
  56. matrx_files/cloud_sync/sql/004_lineage_thumbnail_webhooks.sql +200 -0
  57. matrx_files/cloud_sync/sql/005_tus_uploads_realtime.sql +144 -0
  58. matrx_files/cloud_sync/sql/006_canonical_storage_uri.sql +55 -0
  59. matrx_files/cloud_sync/sql/007_drop_legacy_storage_uri.sql +163 -0
  60. matrx_files/cloud_sync/sql/008_idempotency.sql +81 -0
  61. matrx_files/cloud_sync/sql/009_variant_catalog_index.sql +27 -0
  62. matrx_files/cloud_sync/sql/010_canonical_media_columns.sql +267 -0
  63. matrx_files/cloud_sync/sql/011_drop_legacy_thumbnail_columns.sql +32 -0
  64. matrx_files/cloud_sync/sql/012_exclude_system_files_from_user_tree.sql +204 -0
  65. matrx_files/cloud_sync/sql/013_cleanup_cascade_variants.sql +116 -0
  66. matrx_files/cloud_sync/sql/014_user_tree_privacy_and_usability.sql +195 -0
  67. matrx_files/cloud_sync/sql/015_hide_system_namespace_from_user_tree.sql +169 -0
  68. matrx_files/cloud_sync/sql/015_legacy_storage_uri_nullable.sql +33 -0
  69. matrx_files/cloud_sync/sql/016_relocate_legacy_system_output_and_drop_scars.sql +117 -0
  70. matrx_files/cloud_sync/sync_engine.py +1969 -0
  71. matrx_files/cloud_sync/thumbnail_resolver.py +117 -0
  72. matrx_files/cloud_sync/transports/__init__.py +58 -0
  73. matrx_files/cloud_sync/transports/presigned.py +309 -0
  74. matrx_files/cloud_sync/transports/tus.py +756 -0
  75. matrx_files/cloud_sync/transports/tus_cleanup.py +206 -0
  76. matrx_files/cloud_sync/url_resolver.py +368 -0
  77. matrx_files/cloud_sync/variants.py +268 -0
  78. matrx_files/cloud_sync/variants_service.py +370 -0
  79. matrx_files/cloud_sync/versioning.py +422 -0
  80. matrx_files/content_headers.py +135 -0
  81. matrx_files/db/__init__.py +170 -0
  82. matrx_files/db/db_requirements.py +18 -0
  83. matrx_files/db/helpers/auto_config_files.py +62 -0
  84. matrx_files/db/managers/files/__init__.py +22 -0
  85. matrx_files/db/managers/files/account_tiers.py +205 -0
  86. matrx_files/db/managers/files/analysis.py +217 -0
  87. matrx_files/db/managers/files/analysis_result.py +235 -0
  88. matrx_files/db/managers/files/entities.py +229 -0
  89. matrx_files/db/managers/files/file_rag_jobs.py +223 -0
  90. matrx_files/db/managers/files/file_versions.py +223 -0
  91. matrx_files/db/managers/files/files.py +319 -0
  92. matrx_files/db/managers/files/folders.py +235 -0
  93. matrx_files/db/managers/files/idempotency.py +205 -0
  94. matrx_files/db/managers/files/overrides.py +235 -0
  95. matrx_files/db/managers/files/page_annotations.py +265 -0
  96. matrx_files/db/managers/files/pages.py +247 -0
  97. matrx_files/db/managers/files/rate_limit_buckets.py +199 -0
  98. matrx_files/db/managers/files/share_links.py +211 -0
  99. matrx_files/db/managers/files/structure.py +211 -0
  100. matrx_files/db/managers/files/uploads_inflight.py +217 -0
  101. matrx_files/db/managers/files/user_account.py +217 -0
  102. matrx_files/db/managers/files/user_storage_usage.py +205 -0
  103. matrx_files/db/managers/files/webhook_deliveries.py +217 -0
  104. matrx_files/db/managers/files/webhook_dispatch_state.py +199 -0
  105. matrx_files/db/managers/files/webhooks.py +229 -0
  106. matrx_files/db/migrations/.gitkeep +0 -0
  107. matrx_files/db/models_files.py +934 -0
  108. matrx_files/db/models_iam.py +76 -0
  109. matrx_files/dedup.py +389 -0
  110. matrx_files/file_handler.py +298 -0
  111. matrx_files/file_manager.py +869 -0
  112. matrx_files/local_files.py +129 -0
  113. matrx_files/media_sniff.py +303 -0
  114. matrx_files/service.py +1499 -0
  115. matrx_files/specific_handlers/MODULE_README.md +245 -0
  116. matrx_files/specific_handlers/__init__.py +0 -0
  117. matrx_files/specific_handlers/code_handler.py +106 -0
  118. matrx_files/specific_handlers/html_handler.py +45 -0
  119. matrx_files/specific_handlers/image_handler.py +1216 -0
  120. matrx_files/specific_handlers/image_ops/__init__.py +171 -0
  121. matrx_files/specific_handlers/image_ops/adjust.py +246 -0
  122. matrx_files/specific_handlers/image_ops/base.py +323 -0
  123. matrx_files/specific_handlers/image_ops/bg_remove.py +154 -0
  124. matrx_files/specific_handlers/image_ops/color.py +192 -0
  125. matrx_files/specific_handlers/image_ops/composite.py +305 -0
  126. matrx_files/specific_handlers/image_ops/detect.py +214 -0
  127. matrx_files/specific_handlers/image_ops/filter.py +251 -0
  128. matrx_files/specific_handlers/image_ops/geometry.py +316 -0
  129. matrx_files/specific_handlers/image_ops/inpaint.py +96 -0
  130. matrx_files/specific_handlers/image_ops/mask_ops.py +266 -0
  131. matrx_files/specific_handlers/image_ops/tone.py +175 -0
  132. matrx_files/specific_handlers/image_studio_presets.py +207 -0
  133. matrx_files/specific_handlers/json_handler.py +130 -0
  134. matrx_files/specific_handlers/markdown_handler.py +130 -0
  135. matrx_files/specific_handlers/pdf/__init__.py +380 -0
  136. matrx_files/specific_handlers/pdf/analyze/__init__.py +86 -0
  137. matrx_files/specific_handlers/pdf/analyze/duplicate_pages.py +407 -0
  138. matrx_files/specific_handlers/pdf/analyze/embedded_images.py +104 -0
  139. matrx_files/specific_handlers/pdf/analyze/layout.py +245 -0
  140. matrx_files/specific_handlers/pdf/analyze/metadata.py +147 -0
  141. matrx_files/specific_handlers/pdf/analyze/models.py +128 -0
  142. matrx_files/specific_handlers/pdf/analyze/outline.py +111 -0
  143. matrx_files/specific_handlers/pdf/analyze/page_classification.py +66 -0
  144. matrx_files/specific_handlers/pdf/analyze/pattern_candidates.py +255 -0
  145. matrx_files/specific_handlers/pdf/analyze/reading_order.py +156 -0
  146. matrx_files/specific_handlers/pdf/analyze/repeated_regions.py +519 -0
  147. matrx_files/specific_handlers/pdf/analyze/repeated_regions_detector.py +62 -0
  148. matrx_files/specific_handlers/pdf/analyze/tables_detector.py +51 -0
  149. matrx_files/specific_handlers/pdf/analyze/text_extraction.py +317 -0
  150. matrx_files/specific_handlers/pdf/cloud_ocr/__init__.py +5 -0
  151. matrx_files/specific_handlers/pdf/concurrency.py +122 -0
  152. matrx_files/specific_handlers/pdf/extract/__init__.py +63 -0
  153. matrx_files/specific_handlers/pdf/extract/bbox.py +345 -0
  154. matrx_files/specific_handlers/pdf/extract/chunking.py +162 -0
  155. matrx_files/specific_handlers/pdf/extract/ocr.py +41 -0
  156. matrx_files/specific_handlers/pdf/extract/region.py +132 -0
  157. matrx_files/specific_handlers/pdf/extract/search.py +297 -0
  158. matrx_files/specific_handlers/pdf/extract/tables.py +356 -0
  159. matrx_files/specific_handlers/pdf/extract/text.py +307 -0
  160. matrx_files/specific_handlers/pdf/generate/__init__.py +8 -0
  161. matrx_files/specific_handlers/pdf/handler.py +1393 -0
  162. matrx_files/specific_handlers/pdf/images_to_pdf.py +172 -0
  163. matrx_files/specific_handlers/pdf/internal.py +118 -0
  164. matrx_files/specific_handlers/pdf/ml/__init__.py +5 -0
  165. matrx_files/specific_handlers/pdf/models.py +188 -0
  166. matrx_files/specific_handlers/pdf/ops/__init__.py +86 -0
  167. matrx_files/specific_handlers/pdf/ops/compose.py +114 -0
  168. matrx_files/specific_handlers/pdf/ops/compress.py +396 -0
  169. matrx_files/specific_handlers/pdf/ops/pages.py +354 -0
  170. matrx_files/specific_handlers/pdf/ops/render.py +444 -0
  171. matrx_files/specific_handlers/pdf/pipelines/__init__.py +6 -0
  172. matrx_files/specific_handlers/pdf/pipelines/ai.py +59 -0
  173. matrx_files/specific_handlers/pdf/redact/SCHEMA.sql +66 -0
  174. matrx_files/specific_handlers/pdf/redact/__init__.py +135 -0
  175. matrx_files/specific_handlers/pdf/redact/engine.py +326 -0
  176. matrx_files/specific_handlers/pdf/redact/from_regions.py +101 -0
  177. matrx_files/specific_handlers/pdf/redact/mapping_store.py +116 -0
  178. matrx_files/specific_handlers/pdf/redact/models.py +117 -0
  179. matrx_files/specific_handlers/pdf/redact/patterns.py +501 -0
  180. matrx_files/specific_handlers/pdf/redact/reversible.py +309 -0
  181. matrx_files/specific_handlers/pdf/redact/scrub.py +381 -0
  182. matrx_files/specific_handlers/pdf/redact/substitutes.py +139 -0
  183. matrx_files/specific_handlers/pdf/redact/validators.py +138 -0
  184. matrx_files/specific_handlers/pdf/render/__init__.py +18 -0
  185. matrx_files/specific_handlers/pdf/render/overlay.py +182 -0
  186. matrx_files/specific_handlers/pdf/stream_bridge.py +56 -0
  187. matrx_files/specific_handlers/pdf/studio/__init__.py +34 -0
  188. matrx_files/specific_handlers/pdf/studio/presets.py +373 -0
  189. matrx_files/specific_handlers/pdf/studio/render_spec.py +167 -0
  190. matrx_files/specific_handlers/pdf/tmp.py +97 -0
  191. matrx_files/specific_handlers/pdf/wire.py +73 -0
  192. matrx_files/specific_handlers/pdf_handler.py +178 -0
  193. matrx_files/specific_handlers/ppt_handler.py +101 -0
  194. matrx_files/specific_handlers/svg_rasterizer.py +175 -0
  195. matrx_files/specific_handlers/text_handler.py +45 -0
  196. matrx_files/specific_handlers/thumbnail_source.py +632 -0
  197. matrx_files/specific_handlers/video_compose.py +316 -0
  198. matrx_files/specific_handlers/video_handler.py +230 -0
  199. matrx_files/standalone/__init__.py +0 -0
  200. matrx_files/standalone/app.py +154 -0
  201. matrx_files/system_paths.py +300 -0
  202. matrx_files/zip_stream.py +73 -0
  203. matrx_files-0.1.0.dist-info/METADATA +74 -0
  204. matrx_files-0.1.0.dist-info/RECORD +205 -0
  205. matrx_files-0.1.0.dist-info/WHEEL +4 -0
matrx_files/FEATURE.md ADDED
@@ -0,0 +1,230 @@
1
+ # FEATURE — Client-facing file references (the OUT contract)
2
+
3
+ **The native storage location never reaches a client.** `storage_uri` /
4
+ `file_uri` (`s3://bucket/owner/key`) is server-only — a client can do nothing
5
+ with it and exposing one is info-disclosure. The lean client reference is
6
+ **`FileRef`**. Verified against code 2026-07-04.
7
+
8
+ ## Reading a file — the ONE access gate (the IN contract)
9
+
10
+ 🔒 **The server bypasses Postgres RLS** — cloud_sync connects with the
11
+ service-role key (`db.py`), so `db.get_file*` returns ANY user's row. The ONLY
12
+ thing between one user and another user's file is a **code-level access check**.
13
+ Therefore: **every read of a file's bytes / lookup of a record for a REQUEST
14
+ must go through the authorized gate**, which enforces owner / public / shared
15
+ (`SyncEngine.get_authorized_record_async` → `PermissionsManager`).
16
+
17
+ - **Media (chat attachments, tool/agent media, references, preview sources):**
18
+ `FileManager.resolve_media[_async]` — the ACCESS GATE lives inside it
19
+ (`cloud_sync/_media_resolution.py::_authorized_or_error_{sync,async}`). It reads
20
+ the request user from the ambient context (`matrx_utils.ctx.get_active_user_id`,
21
+ wired to `AppContext`), enforces access, **trusts a valid share token**, and
22
+ **refuses a raw `s3://` path from a user**. A denial stamps
23
+ `resolver_error="access_denied"`; aidream's `media_normalizer` turns that into a
24
+ **403** (`_raise_if_access_denied`). Test: `tests/test_media_access_gate.py`.
25
+ - **Record-first byte reads (preview, pdf-compress, and any request handler that
26
+ turns a client `file_id` into bytes):** `FileService.read_record_for_active_user`
27
+ (enforces for the request user, trusts internal jobs) — NOT raw `db.get_file*`.
28
+ - **Internal trust is EXPLICIT and fail-CLOSED (2026-07-09).** With NO request
29
+ context at all, the gate allows the read ONLY when the caller declared itself
30
+ with `matrx_utils.ctx.system_file_access("<reason>")` (a scoped context
31
+ manager; logged at debug). Unmarked no-context resolution DENIES with a loud
32
+ self-identifying red banner (`matrx_validation_gate` style) and stamps
33
+ `resolver_error="access_denied_no_actor"` (also a 403 via
34
+ `_raise_if_access_denied`). A request context that carries NO user (anonymous
35
+ HTTP) is denied outright — the marker does not apply; and a request USER
36
+ always authorizes as that user, marker or not. Stamped internal callers:
37
+ matrx-rag ingest/stages/table-derivation/image-caption, the podcast pipeline,
38
+ the docproc / assets.upload / image-pipeline workflow nodes, and
39
+ `scripts/backfill_audio_video_mime.py`. Never wrap a USER-supplied ref in the
40
+ marker on a request path.
41
+
42
+ **Guard — `scripts/check_file_access_gate.py`** (loud in `release.sh`, ratchets
43
+ vs `file_access_gate_baseline.json`): screams if any code adds a NEW raw
44
+ `router.read/write` / `db.get_file*` / `afiles_table(...)` door OUTSIDE the gate
45
+ module — and (second layer, 2026-07-09) ratchets every `resolve_media[_async]`
46
+ caller (`media_caller` kind), so each NEW caller must be classified:
47
+ request-scoped (user rides AppContext) or wrapped in `system_file_access(...)`.
48
+ The count only goes down — route a grandfathered door through the gate,
49
+ then `--update-baseline`. This is what makes a second unguarded door impossible.
50
+ Context: the download path (`/files/{id}`) was always gated; a later "cld makeover"
51
+ refactor added preview/chat-media/etc. paths that bypassed it — the gate + guard
52
+ close that class. (2026-07-07)
53
+
54
+ ## The two shapes
55
+
56
+ - **IN — `MediaRef`** ([cloud_sync/media_ref.py](cloud_sync/media_ref.py)):
57
+ a client *references* a file to send us. Exactly one of `file_id | url |
58
+ file_uri`. `file_uri` is a legitimate **input** identifier here (an
59
+ `s3://`/`gs://` URI the caller owns) — this is the ONLY place `file_uri` is
60
+ allowed on a client-facing shape, because it's inbound, not outbound.
61
+ - **OUT — `FileRef`** ([asset_envelope.py](asset_envelope.py)): what a client
62
+ *receives* to point at a stored file. `file_id` + the URL contract + display
63
+ metadata, and **nothing else**. Round-trips: the `file_id` a client gets
64
+ back in a `FileRef` is exactly what it sends in a `MediaRef`.
65
+
66
+ `FileRef` fields: `file_id, visibility, file_name, mime_type, size_bytes, url,
67
+ cdn_url, signed_url, signed_url_expires_at, download_url, thumbnail_url`.
68
+ Populate the URL flavours via `SyncEngine.build_urls_for_record_async` — **never
69
+ hand-mint URLs.**
70
+
71
+ ## The rule, stated absolutely
72
+
73
+ - **No `storage_uri` / `file_uri` on ANY OUT shape** — response model, stream
74
+ event, or embedded reference. Render/download via `url` / `cdn_url` /
75
+ `signed_url` (+ `signed_url_expires_at`) / `download_url`; identify by
76
+ `file_id`.
77
+ - **`file_path` (virtual folder path) is NOT the storage location** — it's the
78
+ user-facing tree key. It stays on the file-*browser* shape (`FileRecord` in
79
+ aidream); the lean `FileRef` omits it.
80
+ - **Internal result/entity models keep `storage_uri`** — `SyncResult`,
81
+ `CloudFile`, `CloudFileVersion` are server-side and read it to mint URLs /
82
+ read bytes. They are NOT client-facing. Do not copy their `storage_uri` onto
83
+ a wire shape.
84
+
85
+ ## The four leak channels (all closed)
86
+
87
+ A client can receive a file description through four structurally distinct
88
+ channels. Every one must stay clean:
89
+
90
+ 1. **REST response models** (aidream) — `FileRecord`, `FileUploadResponse`,
91
+ `PresignedUploadResponse`, `AssetVariant`, `VisionMediaUploadResponse`,
92
+ `ResearchUploadResponse`, `DocumentDetail`, `LibraryDocDetail`,
93
+ `LibraryDocOut`. All strip the field; the version endpoints return a
94
+ projected `FileVersionRecord` (never a raw `select("*")` row).
95
+ 2. **Stream events** — cx-chat `UnifiedMediaBlock` + persisted media parts.
96
+ ✅ **CLOSED**. `file_uri` is REMOVED from the wire type entirely (not just
97
+ nulled): the `MediaBlockShared` subclasses (matrx-connect) and the
98
+ `_StoredMediaPartBase` subclasses (matrx-ai `db/message_parts.py`) no longer
99
+ declare it, so it's gone from `generated/stream-events.ts` (count: 0). Both
100
+ carry a `mode="before"` validator that drops a stale `file_uri` from a
101
+ historical `cx_message.content[]` row (past `extra="forbid"`), and the part
102
+ rescues a genuine external URI into `url`. The 9 content builders that set
103
+ `file_uri=envelope.storage_uri` (matrx-ai `config/media_config.py` ×3,
104
+ `providers/base_media.py`, groq/xai/openai/elevenlabs/openai_image) now set
105
+ only `file_id` + `url`. `file_uri` stays valid ONLY as an INPUT identifier on
106
+ `MediaRef` / `PreviewSource` (still in `api-types.ts`, correctly) and on the
107
+ internal provider content classes (Gemini `gs://` / OpenAI `input_file`) —
108
+ never on an OUT shape. Regenerate after any change: `python
109
+ scripts/generate_types.py`. Test:
110
+ `packages/matrx-connect/tests/test_media_block_no_storage_uri.py`.
111
+ 3. **Direct client access** — the frontend reads `files.files` as `authenticated`
112
+ (and could write it). ✅ **CLOSED** at the DB for read AND write: table-level
113
+ SELECT/UPDATE/INSERT dropped, re-granted as explicit column lists that omit
114
+ `storage_uri` (a column REVOKE alone is a no-op against a table grant — see
115
+ 0147 for SELECT, 0149 for UPDATE/INSERT). The write half matters: an owner
116
+ could otherwise repoint their row's `storage_uri` at another user's S3 object
117
+ (write-side IDOR).
118
+ 4. **Supabase Realtime WAL** — `files.files` was published full-row. ✅ **CLOSED**:
119
+ `REPLICA IDENTITY DEFAULT` + a column-list publication that omits
120
+ `storage_uri`.
121
+
122
+ Channels 3+4 are DB-level, **applied to Matrx Main 2026-07-06**:
123
+ [0146](../../../db/migrations/0146_files_storage_uri_isolation.sql) (publication
124
+ + replica identity) + [0147](../../../db/migrations/0147_files_storage_uri_column_grant.sql)
125
+ (SELECT column-grant) + [0149](../../../db/migrations/0149_files_storage_uri_write_lockdown.sql)
126
+ (UPDATE/INSERT column-grant). Deploy order was FE-first (frontend stopped
127
+ selecting `storage_uri`) → then the migrations.
128
+
129
+ > ⚠️ Still OPEN — the **read-side media IDOR**: `resolve_media_async` trusts a
130
+ > client `file_id`/`file_uri` with no owner check. That's the resolution *auth
131
+ > model*, a separate broad decision — see [FOUND_DEFECTS.md](../../../FOUND_DEFECTS.md).
132
+ > The DB grants above stop the FE from *reading/writing* the column; they don't
133
+ > make resolution authorize the source ref.
134
+
135
+ ## The guards (two independent layers, each screams)
136
+
137
+ 1. **Static** — [scripts/audit_api_types.py](../../../scripts/audit_api_types.py)
138
+ `FORBIDDEN_WIRE_FIELDS`: any `storage_uri`/`file_uri` on a **response**
139
+ model fails the CI ratchet. Plus the exhaustive per-shape test
140
+ [aidream/api/tests/test_no_storage_uri_on_wire.py](../../../aidream/api/tests/test_no_storage_uri_on_wire.py)
141
+ (covers nested + matrx-utils shapes CI's default scope skips), and the media-
142
+ block test above. **Add a new client-facing file shape → add it to the test's
143
+ `WIRE_SHAPES`.**
144
+ 2. **Live-DB** —
145
+ [scripts/validate_storage_uri_isolation.py](../../../scripts/validate_storage_uri_isolation.py)
146
+ (loud, non-blocking, in `release.sh`): screams if `authenticated`/`anon`
147
+ regain `SELECT` **or** `UPDATE`/`INSERT` on `storage_uri`, or the realtime
148
+ publication re-includes it. Green as of 2026-07-06.
149
+
150
+ ## Hard-delete — the ONE purge primitive (no orphaned S3 objects)
151
+
152
+ A cloud file's bytes live in S3; its row + versions + permissions + share-links
153
+ live in Postgres. Hard-delete must remove **both**. The Postgres RPC
154
+ `hard_delete_file(p_file_id)` cascades the DB side and **returns the storage
155
+ URIs still to be purged** (`{main, versions}`, handed only to the service-role
156
+ backend caller — a JWT caller never sees them). The DB wrapper
157
+ `SyncEngine.db.hard_delete_file[_async]` does the DB half **and nothing else** —
158
+ so any code that calls it directly and forgets to delete the returned objects
159
+ strands them in S3 **forever** (the row is gone; nothing ever reconciles them).
160
+ This is the **C10 orphan-data leak**.
161
+
162
+ **The rule:** every hard-delete routes through the ONE primitive —
163
+ `SyncEngine.hard_delete_and_purge[_async](file_id, storage_uri)` — which
164
+ cascades the DB rows, purges the main object + every version + `purge["main"]`
165
+ (deduped), and invalidates the byte cache. **Never call
166
+ `db.hard_delete_file[_async]` directly.** Callers: `managed_delete[_async]`,
167
+ `FileService.hard_delete`, the matrx-ai `cloud_file` tool, and the pdf orphan-
168
+ cleanup all go through it.
169
+
170
+ **Guard (loud, non-blocking, in `release.sh`):**
171
+ [scripts/audit_hard_delete_purge.py](../../../scripts/audit_hard_delete_purge.py)
172
+ AST-scans every `.hard_delete_file[_async](...)` call and fails on any outside
173
+ the primitive (allowlist: `sync_engine.py` + the wrapper's own `db.py`). This is
174
+ the second, screaming layer that keeps the leak extinct.
175
+
176
+ ## Storage ⟷ DB reconciliation (the standing detector)
177
+
178
+ [scripts/reconcile_orphan_objects.py](../../../scripts/reconcile_orphan_objects.py)
179
+ diffs the buckets against the DB in **both** directions. A healthy system trends
180
+ to zero on both; a non-zero number is a bug, not a chore.
181
+
182
+ - **ORPHAN** — an object no row claims. Costs money forever; nothing will ever
183
+ reconcile it (the row that pointed at it is gone). This is what a hard-delete
184
+ leak produces.
185
+ - **MISSING** — a row pointing at a **non-existent** object: a BROKEN FILE the
186
+ user sees but cannot open. The worse direction.
187
+
188
+ **The rule that makes it safe: over-claim, always.** Under-claiming DELETES USER
189
+ DATA; over-claiming merely leaves an orphan behind. The claim set is derived
190
+ **empirically from the live DB** (every text/jsonb column scanned for bucket
191
+ references), never from memory — a new table that stores a storage reference and
192
+ isn't registered will get its objects called orphans.
193
+
194
+ **Two invariants that are easy to get catastrophically wrong:**
195
+ 1. **Soft-deleted rows still own their bytes** — a trashed file is restorable.
196
+ Filtering `deleted_at` out of the claim set purges the trash.
197
+ 2. **Row-less-by-design prefixes exist** (`UNMANAGED_PREFIXES`). Image Studio
198
+ (`staged-variants/`) writes renders with **no DB row** and finds them by S3
199
+ **prefix listing**; ephemeral previews and legal-opinion blobs (derived keys,
200
+ rows in a *separate* DB) do the same. Inside these prefixes "no row ⇒ dead"
201
+ is FALSE, so the reconciler never touches them. This is the *correct* pattern
202
+ for throwaway scratch (a rejected preview should NOT get a `files.files` row)
203
+ — but it demands its OWN cleanup: an **S3 lifecycle rule** expires the scratch
204
+ the user never commits. Those rules are provisioned idempotently by
205
+ [`scripts/ensure_s3_lifecycle.py`](../../../scripts/ensure_s3_lifecycle.py)
206
+ (`staged-variants/` → expire 7d, applied 2026-07-12). A row-less writer with
207
+ **no** lifecycle rule leaks forever — that gap is the real defect, not the
208
+ missing DB row. `system-files/previews/` still lacks one (it lives under an
209
+ `<owner>/` segment, so a root-prefix rule can't target it — needs object
210
+ tagging); tracked in `FOUND_DEFECTS.md`.
211
+
212
+ Public/CDN objects are **held back by default** (`--include-public`): a public URL
213
+ is permanent and works with no row behind it, so it may be referenced from the CMS
214
+ (a separate database), the frontend, or an external site the script cannot see.
215
+ Private objects have no permanent URL — access requires a signed URL minted *from
216
+ the row* — so "no row" genuinely means unreachable.
217
+
218
+ ## Change Log
219
+ - **2026-07-10** — C10 orphan-leak eradicated. `FileService.hard_delete` (and the
220
+ matrx-ai `cloud_file` tool) called the raw DB fn and never purged S3 — orphaning
221
+ the main object + every version. Collapsed all 4 hard-delete paths onto the ONE
222
+ `SyncEngine.hard_delete_and_purge[_async]` primitive; added the boundary guard
223
+ `scripts/audit_hard_delete_purge.py` + regression test. Live RPC contract
224
+ verified unchanged (returns `{main, versions}` to service-role).
225
+ - **2026-07-06** — All 4 channels CLOSED. matrx-ai stream media-block leak fixed
226
+ (9 sources + builder + validator + test). Migrations 0146+0147 applied to
227
+ Matrx Main (FE verified clean first); isolation validator green.
228
+ - **2026-07-04** — Created. Introduced `FileRef`; stripped `storage_uri`/
229
+ `file_uri` from every OUT shape (incl. 3 RAG/document models the static
230
+ guard surfaced); added both guards; wrote staged migration 0146.
@@ -0,0 +1,242 @@
1
+ # `src.matrx_files` — Module Overview
2
+
3
+ > This document is partially auto-generated. Sections tagged `<!-- AUTO:id -->` are refreshed by the generator.
4
+ > Everything else is yours to edit freely and will never be overwritten.
5
+
6
+ <!-- AUTO:meta -->
7
+ ## About This Document
8
+
9
+ This file is **partially auto-generated**. Sections wrapped in `<!-- AUTO:id -->` tags
10
+ are overwritten each time the generator runs. Everything else is yours to edit freely.
11
+
12
+ | Field | Value |
13
+ |-------|-------|
14
+ | Module | `src/matrx_files` |
15
+ | Last generated | 2026-02-28 14:46 |
16
+ | Output file | `src/matrx_files/MODULE_README.md` |
17
+ | Signature mode | `signatures` |
18
+
19
+
20
+ **Child READMEs detected** (signatures collapsed — see links for detail):
21
+
22
+ | README | |
23
+ |--------|---|
24
+ | [`src/matrx_files/specific_handlers/MODULE_README.md`](src/matrx_files/specific_handlers/MODULE_README.md) | last generated 2026-02-28 14:46 |
25
+ **To refresh auto-sections:**
26
+ ```bash
27
+ python utils/code_context/generate_module_readme.py src/matrx_files --mode signatures
28
+ ```
29
+
30
+ **To add permanent notes:** Write anywhere outside the `<!-- AUTO:... -->` blocks.
31
+ <!-- /AUTO:meta -->
32
+
33
+ <!-- HUMAN-EDITABLE: This section is yours. Agents & Humans can edit this section freely — it will not be overwritten. -->
34
+
35
+ ## Architecture
36
+
37
+ > **Fill this in.** Describe the execution flow and layer map for this module.
38
+ > See `utils/code_context/MODULE_README_SPEC.md` for the recommended format.
39
+ >
40
+ > Suggested structure:
41
+ >
42
+ > ### Layers
43
+ > | File | Role |
44
+ > |------|------|
45
+ > | `entry.py` | Public entry point — receives requests, returns results |
46
+ > | `engine.py` | Core dispatch logic |
47
+ > | `models.py` | Shared data types |
48
+ >
49
+ > ### Call Flow (happy path)
50
+ > ```
51
+ > entry_function() → engine.dispatch() → implementation()
52
+ > ```
53
+
54
+
55
+ <!-- AUTO:tree -->
56
+ ## Directory Tree
57
+
58
+ > Auto-generated. 15 files across 2 directories.
59
+
60
+ ```
61
+ src/matrx_files/
62
+ ├── MODULE_README.md
63
+ ├── __init__.py
64
+ ├── base_handler.py
65
+ ├── batch_handler.py
66
+ ├── file_handler.py
67
+ ├── file_manager.py
68
+ ├── local_files.py
69
+ ├── specific_handlers/
70
+ │ ├── MODULE_README.md
71
+ │ ├── __init__.py
72
+ │ ├── code_handler.py
73
+ │ ├── html_handler.py
74
+ │ ├── image_handler.py
75
+ │ ├── json_handler.py
76
+ │ ├── markdown_handler.py
77
+ │ ├── text_handler.py
78
+ # excluded: 2 .md
79
+ ```
80
+ <!-- /AUTO:tree -->
81
+
82
+ <!-- AUTO:signatures -->
83
+ ## API Signatures
84
+
85
+ > Auto-generated via `output_mode="{mode}"`. ~5-10% token cost vs full source.
86
+ > For full source, open the individual files directly.
87
+ > Submodules with their own `MODULE_README.md` are collapsed to a single stub line.
88
+
89
+ ```
90
+ ---
91
+ Filepath: src/matrx_files/__init__.py [python]
92
+
93
+
94
+
95
+
96
+ ---
97
+ Filepath: src/matrx_files/file_handler.py [python]
98
+
99
+ class FileHandler(BaseHandler):
100
+ def __init__(self, app_name, new_instance = False, batch_print = False, print_errors = True, batch_handler = None)
101
+ def get_instance(cls, app_name, new_instance = False, batch_print = False, print_errors = True, batch_handler = None)
102
+ def _get_full_path(self, root, path)
103
+ def public_get_full_path(self, root, path)
104
+ def _ensure_directory(self, path)
105
+ def _print(self, path, message = None, color = None)
106
+ def _print_link(self, path, message = None, color = None)
107
+ def print_batch(self)
108
+ def enable_batch_print(self)
109
+ def disable_batch_print(self)
110
+ def read(self, path, mode = 'r', encoding = 'utf-8')
111
+ def write(self, path, content, **kwargs)
112
+ def append(self, path, content)
113
+ def delete(self, path)
114
+ def read_from_base(self, root, path)
115
+ def write_to_base(self, root, path, content, clean = True, remove_html = False, normalize_whitespace = False)
116
+ def append_to_base(self, root, path, content)
117
+ def delete_from_base(self, root, path)
118
+ def clean(self, content, remove_html = False, normalize_whitespace = False)
119
+ def _remove_html_tags(self, content)
120
+ def _normalize_unicode(self, content)
121
+ def _filter_unwanted_characters(self, content)
122
+ def _normalize_whitespace(self, content)
123
+ def file_exists(self, root, path)
124
+ def delete_file(self, root, path)
125
+ def list_files(self, root, path = '')
126
+ def add_to_batch(self, full_path, message = None, color = None)
127
+
128
+
129
+
130
+ ---
131
+ Filepath: src/matrx_files/batch_handler.py [python]
132
+
133
+ class BatchHandler:
134
+ def __init__(self, enable_batch = False)
135
+ def get_instance(cls, enable_batch = False)
136
+ def add_print(self, path, message = None, color = None)
137
+ def _print(self, path, message = None, color = None)
138
+ def _print_link(self, path, message = None, color = None)
139
+ def print_batch(self)
140
+ def enable_batch(self)
141
+ def disable_batch(self)
142
+ def is_batch_print_enabled(self)
143
+
144
+
145
+
146
+ ---
147
+ Filepath: src/matrx_files/file_manager.py [python]
148
+
149
+ class FileManager:
150
+ def __init__(self, app_name, new_instance = False, batch_print = False, print_errors = True, batch_handler = None)
151
+ def get_instance(cls, app_name, new_instance = False, batch_print = False, print_errors = True, batch_handler = None)
152
+ def read(self, root, path, file_type = 'text')
153
+ def write(self, root, path, content, file_type = 'text', clean = True)
154
+ def file_exists(self, root, path, file_type = 'text')
155
+ def delete_file(self, root, path, file_type = 'text')
156
+ def list_files(self, root, path = '', file_type = 'text')
157
+ def print_batch(self)
158
+ def read_json(self, root, path)
159
+ def write_json(self, root, path, data, clean = True)
160
+ def append_json(self, root, path, data, clean = True)
161
+ def read_temp_json(self, path)
162
+ def write_temp_json(self, path, data, clean = True)
163
+ def get_config_json(self, path)
164
+ def read_text(self, root, path)
165
+ def write_text(self, root, path, data, clean = True)
166
+ def read_temp_text(self, path)
167
+ def write_temp_text(self, path, data, clean = True)
168
+ def read_html(self, root, path)
169
+ def write_html(self, root, path, data, clean = True)
170
+ def read_temp_html(self, path)
171
+ def write_temp_html(self, path, data, clean = True)
172
+ def read_markdown(self, root, path)
173
+ def write_markdown(self, root, path, data, clean = True)
174
+ def read_temp_markdown(self, path)
175
+ def write_temp_markdown(self, path, data, clean = True)
176
+ def read_markdown_lines(self, root, path)
177
+ def write_markdown_lines(self, root, path, data, clean = True)
178
+ def read_image(self, root, path)
179
+ def write_image(self, root, path, data)
180
+ def read_temp_image(self, path)
181
+ def write_temp_image(self, path, data, clean = True)
182
+ def generate_filename(self, extension, sub_dir = '', prefix = '', suffix = '', random = False)
183
+ def generate_directoryname(self, sub_dir = '', prefix = '', suffix = '', random = False)
184
+ def add_to_batch(self, full_path = None, message = None, color = None)
185
+ def get_full_path_from_base(self, root, path)
186
+
187
+
188
+
189
+ ---
190
+ Filepath: src/matrx_files/local_files.py [python]
191
+
192
+ def is_wsl() -> bool
193
+ def convert_windows_to_wsl_path(windows_path: str) -> str
194
+ def resolve_local_path(input_path: str | os.PathLike) -> Path
195
+ def open_any_file(source: str) -> Tuple[str, BinaryIO]
196
+
197
+
198
+
199
+ ---
200
+ Filepath: src/matrx_files/base_handler.py [python]
201
+
202
+ class BaseHandler(ABC):
203
+ def read(self, path)
204
+ def write(self, path, content)
205
+ def append(self, path, content)
206
+ def delete(self, path)
207
+
208
+
209
+
210
+ ---
211
+ Submodule: src/matrx_files/specific_handlers/ [7 files — full detail in src/matrx_files/specific_handlers/MODULE_README.md]
212
+
213
+ ```
214
+ <!-- /AUTO:signatures -->
215
+
216
+ <!-- AUTO:dependencies -->
217
+ ## Dependencies
218
+
219
+ **External packages:** PIL, matrx_utils, requests
220
+ <!-- /AUTO:dependencies -->
221
+
222
+ <!-- AUTO:config -->
223
+ ## Generation Config
224
+
225
+ > Auto-managed. Contains the exact parameters used to generate this README.
226
+ > Used by parent modules to auto-refresh this file when it is stale.
227
+ > Do not edit manually — changes will be overwritten on the next run.
228
+
229
+ ```json
230
+ {
231
+ "subdirectory": "src/matrx_files",
232
+ "mode": "signatures",
233
+ "scope": null,
234
+ "project_noise": null,
235
+ "include_call_graph": false,
236
+ "entry_points": null,
237
+ "call_graph_exclude": [
238
+ "tests"
239
+ ]
240
+ }
241
+ ```
242
+ <!-- /AUTO:config -->