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.
- matrx_files/FEATURE.md +230 -0
- matrx_files/MODULE_README.md +242 -0
- matrx_files/__init__.py +395 -0
- matrx_files/_config.py +89 -0
- matrx_files/analysis/__init__.py +104 -0
- matrx_files/analysis/context.py +52 -0
- matrx_files/analysis/dispatcher.py +547 -0
- matrx_files/analysis/label_catalog.py +249 -0
- matrx_files/analysis/ocr_router.py +89 -0
- matrx_files/analysis/persistence.py +293 -0
- matrx_files/analysis/preferences.py +153 -0
- matrx_files/analysis/protocol.py +165 -0
- matrx_files/analysis/registry.py +129 -0
- matrx_files/analysis/text_sanitize.py +87 -0
- matrx_files/api/__init__.py +23 -0
- matrx_files/api/router.py +24 -0
- matrx_files/api/router_assets.py +344 -0
- matrx_files/api/router_files.py +497 -0
- matrx_files/api/router_share.py +71 -0
- matrx_files/asset_builder.py +239 -0
- matrx_files/asset_envelope.py +462 -0
- matrx_files/backends/__init__.py +33 -0
- matrx_files/backends/base_backend.py +192 -0
- matrx_files/backends/llm_helpers.py +189 -0
- matrx_files/backends/router.py +638 -0
- matrx_files/backends/s3_backend.py +625 -0
- matrx_files/backends/server_backend.py +358 -0
- matrx_files/backends/supabase_backend.py +438 -0
- matrx_files/backends/url_parser.py +215 -0
- matrx_files/base_handler.py +21 -0
- matrx_files/batch_handler.py +89 -0
- matrx_files/cloud_mixin.py +202 -0
- matrx_files/cloud_sync/__init__.py +117 -0
- matrx_files/cloud_sync/_media_resolution.py +606 -0
- matrx_files/cloud_sync/backfill/__init__.py +17 -0
- matrx_files/cloud_sync/backfill/rekey.py +267 -0
- matrx_files/cloud_sync/backfill/restamp_headers.py +235 -0
- matrx_files/cloud_sync/backfill/thumbnails.py +261 -0
- matrx_files/cloud_sync/byte_cache.py +158 -0
- matrx_files/cloud_sync/cdn.py +184 -0
- matrx_files/cloud_sync/config.py +125 -0
- matrx_files/cloud_sync/db.py +974 -0
- matrx_files/cloud_sync/download_stream.py +251 -0
- matrx_files/cloud_sync/events/__init__.py +51 -0
- matrx_files/cloud_sync/events/default_logger.py +137 -0
- matrx_files/cloud_sync/idempotency.py +150 -0
- matrx_files/cloud_sync/media_ref.py +328 -0
- matrx_files/cloud_sync/migrations.py +165 -0
- matrx_files/cloud_sync/models.py +247 -0
- matrx_files/cloud_sync/permissions.py +489 -0
- matrx_files/cloud_sync/processing/__init__.py +8 -0
- matrx_files/cloud_sync/processing/thumbnails.py +214 -0
- matrx_files/cloud_sync/sql/001_initial_schema.sql +595 -0
- matrx_files/cloud_sync/sql/002_guest_support.sql +96 -0
- matrx_files/cloud_sync/sql/003_security_correctness_quotas.sql +1058 -0
- matrx_files/cloud_sync/sql/004_lineage_thumbnail_webhooks.sql +200 -0
- matrx_files/cloud_sync/sql/005_tus_uploads_realtime.sql +144 -0
- matrx_files/cloud_sync/sql/006_canonical_storage_uri.sql +55 -0
- matrx_files/cloud_sync/sql/007_drop_legacy_storage_uri.sql +163 -0
- matrx_files/cloud_sync/sql/008_idempotency.sql +81 -0
- matrx_files/cloud_sync/sql/009_variant_catalog_index.sql +27 -0
- matrx_files/cloud_sync/sql/010_canonical_media_columns.sql +267 -0
- matrx_files/cloud_sync/sql/011_drop_legacy_thumbnail_columns.sql +32 -0
- matrx_files/cloud_sync/sql/012_exclude_system_files_from_user_tree.sql +204 -0
- matrx_files/cloud_sync/sql/013_cleanup_cascade_variants.sql +116 -0
- matrx_files/cloud_sync/sql/014_user_tree_privacy_and_usability.sql +195 -0
- matrx_files/cloud_sync/sql/015_hide_system_namespace_from_user_tree.sql +169 -0
- matrx_files/cloud_sync/sql/015_legacy_storage_uri_nullable.sql +33 -0
- matrx_files/cloud_sync/sql/016_relocate_legacy_system_output_and_drop_scars.sql +117 -0
- matrx_files/cloud_sync/sync_engine.py +1969 -0
- matrx_files/cloud_sync/thumbnail_resolver.py +117 -0
- matrx_files/cloud_sync/transports/__init__.py +58 -0
- matrx_files/cloud_sync/transports/presigned.py +309 -0
- matrx_files/cloud_sync/transports/tus.py +756 -0
- matrx_files/cloud_sync/transports/tus_cleanup.py +206 -0
- matrx_files/cloud_sync/url_resolver.py +368 -0
- matrx_files/cloud_sync/variants.py +268 -0
- matrx_files/cloud_sync/variants_service.py +370 -0
- matrx_files/cloud_sync/versioning.py +422 -0
- matrx_files/content_headers.py +135 -0
- matrx_files/db/__init__.py +170 -0
- matrx_files/db/db_requirements.py +18 -0
- matrx_files/db/helpers/auto_config_files.py +62 -0
- matrx_files/db/managers/files/__init__.py +22 -0
- matrx_files/db/managers/files/account_tiers.py +205 -0
- matrx_files/db/managers/files/analysis.py +217 -0
- matrx_files/db/managers/files/analysis_result.py +235 -0
- matrx_files/db/managers/files/entities.py +229 -0
- matrx_files/db/managers/files/file_rag_jobs.py +223 -0
- matrx_files/db/managers/files/file_versions.py +223 -0
- matrx_files/db/managers/files/files.py +319 -0
- matrx_files/db/managers/files/folders.py +235 -0
- matrx_files/db/managers/files/idempotency.py +205 -0
- matrx_files/db/managers/files/overrides.py +235 -0
- matrx_files/db/managers/files/page_annotations.py +265 -0
- matrx_files/db/managers/files/pages.py +247 -0
- matrx_files/db/managers/files/rate_limit_buckets.py +199 -0
- matrx_files/db/managers/files/share_links.py +211 -0
- matrx_files/db/managers/files/structure.py +211 -0
- matrx_files/db/managers/files/uploads_inflight.py +217 -0
- matrx_files/db/managers/files/user_account.py +217 -0
- matrx_files/db/managers/files/user_storage_usage.py +205 -0
- matrx_files/db/managers/files/webhook_deliveries.py +217 -0
- matrx_files/db/managers/files/webhook_dispatch_state.py +199 -0
- matrx_files/db/managers/files/webhooks.py +229 -0
- matrx_files/db/migrations/.gitkeep +0 -0
- matrx_files/db/models_files.py +934 -0
- matrx_files/db/models_iam.py +76 -0
- matrx_files/dedup.py +389 -0
- matrx_files/file_handler.py +298 -0
- matrx_files/file_manager.py +869 -0
- matrx_files/local_files.py +129 -0
- matrx_files/media_sniff.py +303 -0
- matrx_files/service.py +1499 -0
- matrx_files/specific_handlers/MODULE_README.md +245 -0
- matrx_files/specific_handlers/__init__.py +0 -0
- matrx_files/specific_handlers/code_handler.py +106 -0
- matrx_files/specific_handlers/html_handler.py +45 -0
- matrx_files/specific_handlers/image_handler.py +1216 -0
- matrx_files/specific_handlers/image_ops/__init__.py +171 -0
- matrx_files/specific_handlers/image_ops/adjust.py +246 -0
- matrx_files/specific_handlers/image_ops/base.py +323 -0
- matrx_files/specific_handlers/image_ops/bg_remove.py +154 -0
- matrx_files/specific_handlers/image_ops/color.py +192 -0
- matrx_files/specific_handlers/image_ops/composite.py +305 -0
- matrx_files/specific_handlers/image_ops/detect.py +214 -0
- matrx_files/specific_handlers/image_ops/filter.py +251 -0
- matrx_files/specific_handlers/image_ops/geometry.py +316 -0
- matrx_files/specific_handlers/image_ops/inpaint.py +96 -0
- matrx_files/specific_handlers/image_ops/mask_ops.py +266 -0
- matrx_files/specific_handlers/image_ops/tone.py +175 -0
- matrx_files/specific_handlers/image_studio_presets.py +207 -0
- matrx_files/specific_handlers/json_handler.py +130 -0
- matrx_files/specific_handlers/markdown_handler.py +130 -0
- matrx_files/specific_handlers/pdf/__init__.py +380 -0
- matrx_files/specific_handlers/pdf/analyze/__init__.py +86 -0
- matrx_files/specific_handlers/pdf/analyze/duplicate_pages.py +407 -0
- matrx_files/specific_handlers/pdf/analyze/embedded_images.py +104 -0
- matrx_files/specific_handlers/pdf/analyze/layout.py +245 -0
- matrx_files/specific_handlers/pdf/analyze/metadata.py +147 -0
- matrx_files/specific_handlers/pdf/analyze/models.py +128 -0
- matrx_files/specific_handlers/pdf/analyze/outline.py +111 -0
- matrx_files/specific_handlers/pdf/analyze/page_classification.py +66 -0
- matrx_files/specific_handlers/pdf/analyze/pattern_candidates.py +255 -0
- matrx_files/specific_handlers/pdf/analyze/reading_order.py +156 -0
- matrx_files/specific_handlers/pdf/analyze/repeated_regions.py +519 -0
- matrx_files/specific_handlers/pdf/analyze/repeated_regions_detector.py +62 -0
- matrx_files/specific_handlers/pdf/analyze/tables_detector.py +51 -0
- matrx_files/specific_handlers/pdf/analyze/text_extraction.py +317 -0
- matrx_files/specific_handlers/pdf/cloud_ocr/__init__.py +5 -0
- matrx_files/specific_handlers/pdf/concurrency.py +122 -0
- matrx_files/specific_handlers/pdf/extract/__init__.py +63 -0
- matrx_files/specific_handlers/pdf/extract/bbox.py +345 -0
- matrx_files/specific_handlers/pdf/extract/chunking.py +162 -0
- matrx_files/specific_handlers/pdf/extract/ocr.py +41 -0
- matrx_files/specific_handlers/pdf/extract/region.py +132 -0
- matrx_files/specific_handlers/pdf/extract/search.py +297 -0
- matrx_files/specific_handlers/pdf/extract/tables.py +356 -0
- matrx_files/specific_handlers/pdf/extract/text.py +307 -0
- matrx_files/specific_handlers/pdf/generate/__init__.py +8 -0
- matrx_files/specific_handlers/pdf/handler.py +1393 -0
- matrx_files/specific_handlers/pdf/images_to_pdf.py +172 -0
- matrx_files/specific_handlers/pdf/internal.py +118 -0
- matrx_files/specific_handlers/pdf/ml/__init__.py +5 -0
- matrx_files/specific_handlers/pdf/models.py +188 -0
- matrx_files/specific_handlers/pdf/ops/__init__.py +86 -0
- matrx_files/specific_handlers/pdf/ops/compose.py +114 -0
- matrx_files/specific_handlers/pdf/ops/compress.py +396 -0
- matrx_files/specific_handlers/pdf/ops/pages.py +354 -0
- matrx_files/specific_handlers/pdf/ops/render.py +444 -0
- matrx_files/specific_handlers/pdf/pipelines/__init__.py +6 -0
- matrx_files/specific_handlers/pdf/pipelines/ai.py +59 -0
- matrx_files/specific_handlers/pdf/redact/SCHEMA.sql +66 -0
- matrx_files/specific_handlers/pdf/redact/__init__.py +135 -0
- matrx_files/specific_handlers/pdf/redact/engine.py +326 -0
- matrx_files/specific_handlers/pdf/redact/from_regions.py +101 -0
- matrx_files/specific_handlers/pdf/redact/mapping_store.py +116 -0
- matrx_files/specific_handlers/pdf/redact/models.py +117 -0
- matrx_files/specific_handlers/pdf/redact/patterns.py +501 -0
- matrx_files/specific_handlers/pdf/redact/reversible.py +309 -0
- matrx_files/specific_handlers/pdf/redact/scrub.py +381 -0
- matrx_files/specific_handlers/pdf/redact/substitutes.py +139 -0
- matrx_files/specific_handlers/pdf/redact/validators.py +138 -0
- matrx_files/specific_handlers/pdf/render/__init__.py +18 -0
- matrx_files/specific_handlers/pdf/render/overlay.py +182 -0
- matrx_files/specific_handlers/pdf/stream_bridge.py +56 -0
- matrx_files/specific_handlers/pdf/studio/__init__.py +34 -0
- matrx_files/specific_handlers/pdf/studio/presets.py +373 -0
- matrx_files/specific_handlers/pdf/studio/render_spec.py +167 -0
- matrx_files/specific_handlers/pdf/tmp.py +97 -0
- matrx_files/specific_handlers/pdf/wire.py +73 -0
- matrx_files/specific_handlers/pdf_handler.py +178 -0
- matrx_files/specific_handlers/ppt_handler.py +101 -0
- matrx_files/specific_handlers/svg_rasterizer.py +175 -0
- matrx_files/specific_handlers/text_handler.py +45 -0
- matrx_files/specific_handlers/thumbnail_source.py +632 -0
- matrx_files/specific_handlers/video_compose.py +316 -0
- matrx_files/specific_handlers/video_handler.py +230 -0
- matrx_files/standalone/__init__.py +0 -0
- matrx_files/standalone/app.py +154 -0
- matrx_files/system_paths.py +300 -0
- matrx_files/zip_stream.py +73 -0
- matrx_files-0.1.0.dist-info/METADATA +74 -0
- matrx_files-0.1.0.dist-info/RECORD +205 -0
- 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 -->
|