Repository navigation
Description
Filing gate: ① an observed behaviour with a live reproduction. It may be by design, so the first touch is to confirm the contract: a file field accepts a file whose upload was never completed.
Business reason: a record can be saved pointing at a sys_file that is still status: pending (presigned, bytes PUT or not, /upload/complete never called). The field reads as attached, but the file was never committed, verified (size, bytes held, #24548) or constraint-checked on completion. A later download or a lifecycle reaper can then meet a claimed file that never became real.
Reader: attachments-storage (the file-field write gate that resolves the referenced sys_file). The neighbouring rulings are #24548 (complete refuses unheld bytes) and #26043 (an unknown id is refused).
Dedupe (open AND closed): "file field accepts pending upload never completed sys_file status pending claimed" → 7 hits, none of them this one. The nearest are #24548, #22466 and #22547, all on the completed-file side.
reach: public HTTP entry, measured at 756220c4 on a stock showcase boot, as admin signed in through the form so the session cookie is held, against the qa-media-constraints scratch object (media_qa_media, poster: accept image/png, image/jpeg, maxSize 1 MiB).
Checklist item priority: P1
type: Bug
QA-source: #26476 · attachments-storage.field-accept-maxsize-server-enforced · observed beside the clauses
Reproduction:
POST /api/v1/storage/upload/presignedwith{"filename":"pend.png","mimeType":"image/png","size":100}. It answers 200 withfileIdanduploadUrl.- PUT 100 bytes to
uploadUrl. Do NOT call/upload/complete. POST /api/v1/data/media_qa_mediawith{"name":"p6b","poster":"FILE_ID"}.- Actual: 201. The record stores the id, and the
sys_filerow readsstatus=pendingwithref_object,ref_fieldandref_idstamped.
Expected (to confirm): the write is refused with a located 400, because the file is not committed, as it already is for an unknown id (#26043). Otherwise the contract should say a pending file may be claimed, and completion must still run the field's constraints.
This is not an authorization finding: the caller owns the upload.
Generated by Claude Code
Activity
objectstack-fleet commentedon Oct 27, 2026
Triage: first grade —
bug·priority:p2·domain:server·area:files·pm:queue· triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-27T05:01Z⛔ Not a claim, ⛔ not a dispatch.
sys_filestillstatus: pending. The first step is to measure whether the field'saccept/maxSizeand the Storage upload/complete commits a file record for bytes the backend never stored; the download then fails 500 #24548 byte check are skipped for a pending file. If they are (a declared constraint left unenforced at a public door), the dev reports it and triage raises the card to p1. Likely shape: the file-field write gate refuses a non-completedfile, the way files: a record write that names a file its writer may not read keeps a dangling id (the quiet answer of #25951); refuse the field instead, so no slot holds a file it does not own #26043 refuses an unknown id. Found by the 18.2.0 wave-2 sweep ([sweep] v18.2.0 release test sweep #25807).Generated by Claude Code
objectstack-fleet commentedon Oct 27, 2026
Claim: PM loop round 9 · card thread · the pending-upload file field refusal
Session:
session_01R1q2xdtYgU6FKrZ7LgjPCC(the seat); the card thread issession_01FEkQ5oz3SuMTEoG66BkXVVAccount:
os-project-manager(the seat's linked user; the card's assignee)Branch:
claude/project-thread-ymyfqe(the card thread's harness-pinned branch; it names no issue, so this claim is the identity record)Worktree: the card thread's own container checkout
Domain:
domain:serverSeat:
domain:server#1Ruling-ref: triage
6434181544: measure the contract first (constraints on a pending file, and what complete does for a claimed file), then the file-field write gate refuses a non-completedsys_filewith a located 400 shaped like #26043's. Intake split withdomain:server#2for the 18.2.0 wave-2 cards (#25807).File surface: the file-field write gate (
packages/services/service-storage/**or whereverattachments-storageresolvessys_file), every door that writes a file field, their tests andpatchchangesets. ⛔ Nopackages/spec/**(lane red line).Container & model:
S,mode:thread, default model and effort (set at thread open)Clause-②: yes (narrowing) — expected: a pending file that was accepted 201 is refused 400; the dev measures it on the diff
Metadata-protocol: none
Responsibility: a record can point at a file that was never committed, verified or constraint-checked
Thread-read: 6434181544
Premise: holds (read 2026-10-27T05:47Z at
origin/main45de8c6cba: card open,pm:queue, no live claim)Serial constraints cleared: none known on these paths; the dev re-measures open PRs on the files it touches before
pr_create.objectstack-fleet commentedon Oct 27, 2026
os-dev-report
{
"issue": 26479,
"status": "done",
"branch": "claude/project-thread-ymyfqe",
"pr": "#26531",
"head": "c3aca48d28cdc925b46533ded7d852e75e1d8b76",
"session": "session_01FEkQ5oz3SuMTEoG66BkXVV",
"premise_still_valid": true,
"summary": "The file-field write gate (copy-on-claim's judging pass in file-reference-lifecycle.ts, run by beforeInsert/beforeUpdate) now refuses a field naming a sys_file whose status is not committed or deleted, when the write has a caller: 400 VALIDATION_FAILED, one invalid_value entry naming the field and the file id, after the #26043 read check. Nothing is written or stamped, and the same write is taken once /upload/complete has run. System and context-less writes (the seed loader's storeFile -> record -> claimFiles path) are not judged, and a slot resending the pending file it already holds is not refused. Triage step 1, measured on main 45de8c6: the card reproduces (201, ref_* stamped, row pending); accept/maxSize are NOT skipped on a pending file (they run on the declared mime/size: declared application/zip and declared 2 MiB both refused 400 ERR_FILE_CONSTRAINT); the #24548 byte check IS skipped (declared 100 / PUT 2 MiB claimed 201, then /upload/complete 409, row stays pending and claimed); /upload/complete on a claimed file runs no field constraint (200, committed, ref_* kept); the download door answers 404 for a claimed pending file.",
"deviations": "(1) The ruling says 'not completed'; sys_file.status is pending|committed|deleted. Read as: committed and deleted (a released committed file that claimFile revives in its grace window) are accepted, everything else refused, the same split field-file-service describeRow uses. (2) A slot already holding the pending file (isSameSlot on an update resending its value) is not refused, so a record left that way by a pre-fix write stays editable; pinned by an ablation. (3) Report posted on #26479 per the seat's correction, not #25235.",
"tests": "New src/file-field-pending-upload.integration.test.ts (real ObjectQL on sqlite + StorageServicePlugin on the local adapter, upload doors called as routes): 8 passed (8). Red on base: file-reference-lifecycle.ts restored to 45de8c6 (grep isCompletedUpload = 0) -> 6 failed | 2 passed (the 2 are the controls); restored with git checkout HEAD, git diff HEAD empty, blob f111b144 = HEAD blob. Ablation of the held-slot term via scripts/ablation-replace.mjs (anchor x1 -> x0, blob f111b144 -> 91f32d81): exactly the held-slot test fails; restored, blob matches. pnpm --filter @objectstack/service-storage test: Test Files 69 passed (69), Tests 1253 passed (1253). typecheck: VERDICT command-exit 0. eslint --no-inline-config packages/services/service-storage: exit 0. All at c3aca48.",
"gates": "dispatch-gates --commands at c3aca48: 93 derived; 91 run exit 0; 2 NOT MEASURED (check:skill-examples, check:dual-build-cjs-loads: exit 3 PREREQUISITE NOT MET, need a full workspace build; neither reads a touched file; CI runs them). check-plugin-teardown-shape --self-test exit 3 on the shallow clone, exit 0 (48 cases) after git fetch origin 621a487. --ran: '93 derived famil(ies) accounted for, 91 run, 2 NOT-MEASURED'. PR check-runs: in_progress at report time (not waited on, per os-dev).",
"dogfood_battery": "c3aca48d · os-verify-lock VERDICT command-exit 0 (held 1383s) · Test Files 300 passed | 3 skipped (303) · Tests 2532 passed | 18 skipped (2550) · no failures",
"mcp_calls": "4 — mcp__github__issue_read x4 (get x1, get_comments x3), all reads; no MCP write",
"api_writes": "3 — git push (claude/project-thread-ymyfqe, new branch at c3aca48); relay pr_create (fleet-write run 40463304691, read-back 9576/9576 bytes identical, assignee os-project-manager) -> #26531; relay post-stamped comment on #26479 (this report)",
"open_questions": [],
"out_of_scope_findings": [
"carrier: #26479 · noted, not filed — triage step 2: accept/maxSize are not skipped on a pending file (judged on declared metadata); the skipped check is #24548's byte check, which is this card's own defect and is closed by this PR (a user write can no longer claim a pending file). No separate class (b) finding.",
"carrier: none · noted, not filed (read only, not measured) — the sys_file retention (onlyWhen status pending, 7d) and the reap guard's pending branch (attachment-lifecycle.ts) delete a pending row's bytes without reading ref_*. On main a claimed pending file would be reaped from under its record; after this PR a user write cannot produce that state, but rows claimed before it may exist. In the PR's Acceptance notes."
]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 27, 2026
Landed
PR #26531 merged at 2026-10-27T07:22Z by the merge queue as
416d9ddd3donmain(Fixes #26479; ACCEPT6435895844).git ls-remote origin 'refs/heads/gh-readonly-queue/main/pr-26531-*'origin/mainby contentgit cat-file -e origin/main:packages/services/service-storage/src/file-field-pending-upload.integration.test.tsgit merge-base --is-ancestor 416d9ddd3d origin/mainState: this card closed by
Fixes(completed);pm:dispatchedremoved.Generated by Claude Code