You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Full attachments-storage area run of the checklist-test skill — all 8 items driven against a live showcase (opus subagent, isolated port + file DB, admin + two purpose-provisioned member personas). Text-only per RUNNER.md.
Result: 4 PASS · 4 PARTIAL · 0 FAIL. The security core of this area is genuinely solid — every partial is a surfacing/coverage gap, not a broken gate.
presigned-upload-roundtrip (5/5 + both negatives) — presign → PUT → complete → download round-trips with a sha256 match on the retrieved bytes; status moves pending→committed; the /files/:id/url response is the service-storage success bodies are three shapes, none carrying success: true — the other half of #3675 #3689{success,data:{url}} envelope with no top-level url key; the 302 door resolves to the same bytes. Owner spoofing refused: a presign carrying owner_id:'FORGED-OWNER' and status:'committed' still stored the session user's id and pending. All three upload entry points 401 AUTH_REQUIRED anonymously, and the boot log carries no open-mode notice.
read-inherits-parent-rls (4/4 + both negatives) — attachment rows inherit the parent's row-level visibility on bothfind and count: invisible parent → admin 5 rows / restricted persona 0 (HTTP 200, filtered, nothing leaked); by-id read → 404 RECORD_NOT_FOUND for the restricted persona, 200 for admin. Driven at scale: bulked to 2100 join rows to reach the READ_SCAN_LIMIT branch — the server logs the fail-closed warning and the read stays closed (2095 vs 2100, exactly the 5 withheld rows). A per-row probe found zero rows whose parent the persona cannot read, at both 12-row and 2100-row scale.
sys-file-status-pipeline (5/5 + both negatives) — one file driven through every real transition with a separate read after each: pending → committed → (last join row deleted) deleted + deleted_at → (re-attach) committed + deleted_at null. Gated downloads 404 FILE_NOT_FOUND during both the pending and the tombstoned window. The tombstone fires only on the last reference (two join rows over one file: deleting the first changes nothing), and a scope:'user' control file is left untouched by the join-row hooks.
orphan-tombstone-reap (4/4 + both negatives) — pins run first per RUNNER rule 6 (15 passed/1 skipped + 25/25), then hand-driven against the platform's own sweep (the LifecycleService initial sweep, not a bespoke sweeper): a 40-day-old tombstone lost both its row and its bytes on disk; a tombstone that regained a reference behind the hooks' back was un-tombstoned, not reaped, with the declared reap-guard log line; a fresh in-window tombstone, a committed row and a pending row all survived untouched. No half-reaped state in either direction.
🟡 PARTIAL — 4 items
1. download-authz-both-sides — server gates are perfect; the console reports a denial as "no attachments"
Server side is fully proven, on both gating classes (attachments-scope and field-owned) and both routes (/files/:id/url and the 302 door): anonymous → 401 AUTH_REQUIRED; two real member personas → 403 ATTACHMENT_DOWNLOAD_DENIED / FILE_DOWNLOAD_DENIED, with their inability to read the parent proven independently first; an entitled non-owner → 200 with matching bytes; the owner bypass on an unreadable parent → 200 for the owner while a sibling file on the identical parent is 403. Flipping acl to public_read opens the anonymous door and restoring private re-closes it. TTL contrast observed: gated grant tokens ≈300 s (downloadTtl) vs ≈3600 s (presignedTtl).
Clause 4 is partial (console surfacing). On a record whose attachment list 403s, the panel renders "No attachments yet. Upload a file to get started." — and still advertises an Upload affordance the server would refuse.
Reproduction rule — as a member denied the parent, open a record with attachments: the panel fires GET /api/v1/data/sys_attachment?...filter=parent_object=…, receives 403, and renders the empty state. Expected denial copy. Actual empty-state copy on a record that in fact holds 2095+ attachments.
The objectui#2532 failure mode (raw error dump / leaked row) is absent — nothing leaks, and no status code is shown. The gap is that a 40x is swallowed into the empty state rather than mapped to denied, so an operator cannot tell "nothing here" from "not yours".
2. attach-requires-parent-edit — gates all hold; one clause is blocked by a stale built @objectstack/core
Passing: a read-but-not-edit persona is refused 403 ATTACHMENT_PARENT_ACCESS on attach while still listing that parent's attachments (read inherits parent READ, attach requires parent EDIT — exactly the contract); uploaded_by is server-stamped over a spoofed value; delete is refused for a non-uploader without parent edit (403 ATTACHMENT_DELETE_DENIED, row still present) and allowed for the uploader even on a parent they cannot edit; deleteMany is all-or-nothing (one refused row aborted the batch, 14→14 rows, both survivors present); an object without enable.files → 403 FILES_DISABLED.
Clause 3 is blocked(environment), and this one needs attention on its own: the predicate-less multi-delete shape cannot be expressed over REST (deleteMany with no ids/where → 400 before the hook is reached), so it rests on its unit pin — and that pin cannot execute in this checkout. All 17 cases in packages/services/service-storage/src/attachment-access-hooks.test.ts error with:
TypeError: withoutOperationPrivateKeys is not a function
because the built @objectstack/core the test resolves is stale relative to packages/core/src/security/operation-private-keys.ts. That is a stale-dist artifact of the prebuilt tree, not a runtime defect — the live server enforced every other gate in this item correctly — but it means the #4757 unscoped-multi-delete refusal is currently unguarded by an executable test here.
3. upload-session-abort — two of the five declared sys_upload_session.status values have no producer
The chunked lane works end to end: init → chunk PUT → progress (percentComplete:50) → complete → committed file → download matches. A wrong resume token is 403 INVALID_RESUME_TOKEN. An abandoned session is reaped with its multipart aborted first, a completed session is reaped without an abort attempt, and the abort-failure veto is pinned green.
Clause 4 is partial. A scan of all session rows after driving both flows returned exactly ['in_progress','completed']. completing is written transiently and immediately overwritten. failed and expired have no writer anywhere in the service — a grep across packages finds them only in the enum declaration and in the retention onlyWhen ({status:{$in:['completed','failed','expired']}}) in system-upload-session.object.ts, plus tests. An expired session's row is reaped by TTL, never re-statused. So the retention rule references two states the system can never enter — an enforce-or-remove candidate.
Clause 3 is also partial on its oracle only: the local adapter's abortChunkedUpload cannot be made to fail from outside the process, so the [storage] reap guard: multipart abort failed line was never emitted live; the veto behaviour itself is pinned and passing.
Contract note: the route takes totalSize (not size) and forces chunkSize ≥ 5 MiB; the item's step text says size.
4. inline-grid-receipt-cells — works correctly; the clause text predates ADR-0104
The grid auto-derives exactly one Receipt column with a real input[type=file] (zero receipt-labelled text inputs — objectui#2360 does not reproduce). Picking a file produces a genuine presign → PUT → complete → thumbnail trace, and submitting the form emits one atomicPOST /api/v1/batch with atomic:true and a {"$ref":0} parent reference, persisting both records.
Clause 3 is partial as a checklist-vs-product drift, not a defect. The clause expects a resolved stored-file object carrying receipt.name and an absolute http(s) URL; the shipped payload is the bare opaque sys_file id. What matters was proven: the persisted value is a managed reference, not a data:/blob: placeholder — the sys_file row reads {status:'committed', ref_object:'showcase_invoice_line', ref_id:…, ref_field:'receipt', acl:'private'}, i.e. the ADR-0104 field-ownership claim fired, and the file is correctly gated (anon 401 / two members 403 / admin 200). storage-routes.ts documents the id as "the value a file field stores as a reference". The clause should be restated against ADR-0104 D3.
Fixtures — the item knownGaps were closed, not skipped
The area's declared gap ("no private files-enabled parent exists on stock showcase" — the only enable.files object is public_read_writeand readable via the showcase everyone baseline) would have forced blocked(fixture) on the entire authz core. It was closed by authoring scratch objects at runtime (POST /api/v1/packages then PUT /meta/objects/qa_vault?package=… with sharingModel:'private', plus qa_shared public_read and qa_nofiles) and provisioning two real personas via sign-up + a runtime permission-set/position binding. No repo file was touched. Worth landing as a permanent fixture so this area does not need re-provisioning each run.
Checklist maintenance falling out of this run
inline-grid-receipt-cells clause 3 — restate against ADR-0104 D3 (opaque id, not a resolved object).
upload-session-abort clause 4 — either give failed/expired a writer or drop them from the enum and the retention onlyWhen.
attach-requires-parent-edit clause 3 — the pin it depends on cannot execute against the prebuilt @objectstack/core; needs a rebuild in CI or the clause needs a live oracle.
upload-session-abort step text says size; the route takes totalSize.
Full
attachments-storagearea run of thechecklist-testskill — all 8 items driven against a live showcase (opus subagent, isolated port + file DB, admin + two purpose-provisioned member personas). Text-only per RUNNER.md.Result: 4 PASS · 4 PARTIAL · 0 FAIL. The security core of this area is genuinely solid — every partial is a surfacing/coverage gap, not a broken gate.
Environment — framework
92f26f75(branchclaude/platform-test-checklist-ocwugl) · vendored console pin6314e87f· showcase app · isolated port + file DB.✅ PASS — 4 items
presigned-upload-roundtrip(5/5 + both negatives) — presign →PUT→ complete → download round-trips with a sha256 match on the retrieved bytes;statusmovespending→committed; the/files/:id/urlresponse is the service-storage success bodies are three shapes, none carryingsuccess: true— the other half of #3675 #3689{success,data:{url}}envelope with no top-levelurlkey; the 302 door resolves to the same bytes. Owner spoofing refused: a presign carryingowner_id:'FORGED-OWNER'andstatus:'committed'still stored the session user's id andpending. All three upload entry points 401AUTH_REQUIREDanonymously, and the boot log carries no open-mode notice.read-inherits-parent-rls(4/4 + both negatives) — attachment rows inherit the parent's row-level visibility on bothfindandcount: invisible parent → admin 5 rows / restricted persona 0 (HTTP 200, filtered, nothing leaked); by-id read → 404RECORD_NOT_FOUNDfor the restricted persona, 200 for admin. Driven at scale: bulked to 2100 join rows to reach theREAD_SCAN_LIMITbranch — the server logs the fail-closed warning and the read stays closed (2095 vs 2100, exactly the 5 withheld rows). A per-row probe found zero rows whose parent the persona cannot read, at both 12-row and 2100-row scale.sys-file-status-pipeline(5/5 + both negatives) — one file driven through every real transition with a separate read after each:pending→committed→ (last join row deleted)deleted+deleted_at→ (re-attach)committed+deleted_atnull. Gated downloads 404FILE_NOT_FOUNDduring both the pending and the tombstoned window. The tombstone fires only on the last reference (two join rows over one file: deleting the first changes nothing), and ascope:'user'control file is left untouched by the join-row hooks.orphan-tombstone-reap(4/4 + both negatives) — pins run first per RUNNER rule 6 (15 passed/1 skipped + 25/25), then hand-driven against the platform's own sweep (the LifecycleService initial sweep, not a bespoke sweeper): a 40-day-old tombstone lost both its row and its bytes on disk; a tombstone that regained a reference behind the hooks' back was un-tombstoned, not reaped, with the declared reap-guard log line; a fresh in-window tombstone, a committed row and a pending row all survived untouched. No half-reaped state in either direction.🟡 PARTIAL — 4 items
1.
download-authz-both-sides— server gates are perfect; the console reports a denial as "no attachments"Server side is fully proven, on both gating classes (attachments-scope and field-owned) and both routes (
/files/:id/urland the 302 door): anonymous → 401AUTH_REQUIRED; two real member personas → 403ATTACHMENT_DOWNLOAD_DENIED/FILE_DOWNLOAD_DENIED, with their inability to read the parent proven independently first; an entitled non-owner → 200 with matching bytes; the owner bypass on an unreadable parent → 200 for the owner while a sibling file on the identical parent is 403. Flippingacltopublic_readopens the anonymous door and restoringprivatere-closes it. TTL contrast observed: gated grant tokens ≈300 s (downloadTtl) vs ≈3600 s (presignedTtl).Clause 4 is
partial(console surfacing). On a record whose attachment list 403s, the panel renders "No attachments yet. Upload a file to get started." — and still advertises an Upload affordance the server would refuse.Reproduction rule — as a member denied the parent, open a record with attachments: the panel fires
GET /api/v1/data/sys_attachment?...filter=parent_object=…, receives 403, and renders the empty state. Expected denial copy. Actual empty-state copy on a record that in fact holds 2095+ attachments.The objectui#2532 failure mode (raw error dump / leaked row) is absent — nothing leaks, and no status code is shown. The gap is that a 40x is swallowed into the empty state rather than mapped to denied, so an operator cannot tell "nothing here" from "not yours".
2.
attach-requires-parent-edit— gates all hold; one clause is blocked by a stale built@objectstack/corePassing: a read-but-not-edit persona is refused
403 ATTACHMENT_PARENT_ACCESSon attach while still listing that parent's attachments (read inherits parent READ, attach requires parent EDIT — exactly the contract);uploaded_byis server-stamped over a spoofed value; delete is refused for a non-uploader without parent edit (403ATTACHMENT_DELETE_DENIED, row still present) and allowed for the uploader even on a parent they cannot edit;deleteManyis all-or-nothing (one refused row aborted the batch, 14→14 rows, both survivors present); an object withoutenable.files→ 403FILES_DISABLED.Clause 3 is
blocked(environment), and this one needs attention on its own: the predicate-less multi-delete shape cannot be expressed over REST (deleteManywith noids/where→ 400 before the hook is reached), so it rests on its unit pin — and that pin cannot execute in this checkout. All 17 cases inpackages/services/service-storage/src/attachment-access-hooks.test.tserror with:because the built
@objectstack/corethe test resolves is stale relative topackages/core/src/security/operation-private-keys.ts. That is a stale-dist artifact of the prebuilt tree, not a runtime defect — the live server enforced every other gate in this item correctly — but it means the #4757 unscoped-multi-delete refusal is currently unguarded by an executable test here.3.
upload-session-abort— two of the five declaredsys_upload_session.statusvalues have no producerThe chunked lane works end to end: init → chunk
PUT→ progress (percentComplete:50) → complete →committedfile → download matches. A wrong resume token is 403INVALID_RESUME_TOKEN. An abandoned session is reaped with its multipart aborted first, a completed session is reaped without an abort attempt, and the abort-failure veto is pinned green.Clause 4 is
partial. A scan of all session rows after driving both flows returned exactly['in_progress','completed'].completingis written transiently and immediately overwritten.failedandexpiredhave no writer anywhere in the service — a grep across packages finds them only in the enum declaration and in the retentiononlyWhen({status:{$in:['completed','failed','expired']}}) insystem-upload-session.object.ts, plus tests. An expired session's row is reaped by TTL, never re-statused. So the retention rule references two states the system can never enter — an enforce-or-remove candidate.Clause 3 is also
partialon its oracle only: the local adapter'sabortChunkedUploadcannot be made to fail from outside the process, so the[storage] reap guard: multipart abort failedline was never emitted live; the veto behaviour itself is pinned and passing.Contract note: the route takes
totalSize(notsize) and forceschunkSize ≥ 5 MiB; the item's step text sayssize.4.
inline-grid-receipt-cells— works correctly; the clause text predates ADR-0104The grid auto-derives exactly one Receipt column with a real
input[type=file](zero receipt-labelled text inputs — objectui#2360 does not reproduce). Picking a file produces a genuine presign →PUT→ complete → thumbnail trace, and submitting the form emits one atomicPOST /api/v1/batchwithatomic:trueand a{"$ref":0}parent reference, persisting both records.Clause 3 is
partialas a checklist-vs-product drift, not a defect. The clause expects a resolved stored-file object carryingreceipt.nameand an absolute http(s) URL; the shipped payload is the bare opaquesys_fileid. What matters was proven: the persisted value is a managed reference, not adata:/blob:placeholder — thesys_filerow reads{status:'committed', ref_object:'showcase_invoice_line', ref_id:…, ref_field:'receipt', acl:'private'}, i.e. the ADR-0104 field-ownership claim fired, and the file is correctly gated (anon 401 / two members 403 / admin 200).storage-routes.tsdocuments the id as "the value a file field stores as a reference". The clause should be restated against ADR-0104 D3.Fixtures — the item
knownGaps were closed, not skippedThe area's declared gap ("no private files-enabled parent exists on stock showcase" — the only
enable.filesobject ispublic_read_writeand readable via the showcaseeveryonebaseline) would have forcedblocked(fixture)on the entire authz core. It was closed by authoring scratch objects at runtime (POST /api/v1/packagesthenPUT /meta/objects/qa_vault?package=…withsharingModel:'private', plusqa_sharedpublic_read andqa_nofiles) and provisioning two real personas via sign-up + a runtime permission-set/position binding. No repo file was touched. Worth landing as a permanent fixture so this area does not need re-provisioning each run.Checklist maintenance falling out of this run
inline-grid-receipt-cellsclause 3 — restate against ADR-0104 D3 (opaque id, not a resolved object).upload-session-abortclause 4 — either givefailed/expireda writer or drop them from the enum and the retentiononlyWhen.attach-requires-parent-editclause 3 — the pin it depends on cannot execute against the prebuilt@objectstack/core; needs a rebuild in CI or the clause needs a live oracle.upload-session-abortstep text sayssize; the route takestotalSize.