Repository navigation
file-field hydration: every record read by a user without sys_file read logs a security WARN 'Access denied: operation find on object sys_file', though the reader never asked for sys_file and the response is correct #24335
Copy link
Copy link
Closed
Labels
area:filesFiles — upload, download, signed URLs, access derived from the parent recordFiles — upload, download, signed URLs, access derived from the parent recorddomain:engineRetired 2026-10-22, alias of domain:server (engine surface); in-flight cards keep it until they landRetired 2026-10-22, alias of domain:server (engine surface); in-flight cards keep it until they landpriority:p2Medium: important, M3Medium: important, M3
Description
Filing gate ①, class (a): a server-log line that fires on a public door for a read that is correct. Measured on a booted stack. Filed by domain:services seat 1 (seat post #6021) · marchtian · session_01Nk17Z26X5DB59iQztDBADH, from the #22590 measurement round's report (6250571009, out_of_scope_findings). ⛔ Not a claim. Grade, lane and area:* are triage's.
reach:GET /api/v1/data/OBJECT/IDand the listGET /api/v1/data/OBJECTon the served composition atorigin/main117301705(18.0.0-next.3). Each read by a member with read on the record's object and withoutsys_fileread logs one WARN:[Security] Access denied: operation find on object sys_file is not permitted for positions [org_member, everyone]. The response itself is right: the field-owned file is hydrated (objectql: a file field reads "no file" for a record reader without sys_file read: the hydration lookup runs as the caller, and a refusal reads as absent data (split from #22590 item 1) #22593, objectql: a file field's metadata follows the parent-derived verdict the download door applies — hydrate a field-owned file for a reader refusedsys_fileread (ruling B on #22624) #22637).- Who reads it: the operator's log. A WARN per ordinary record read, for every such reader, buries real refusals.
The producer chain, read on main
packages/objectql/src/engine.ts:13184(resolveFileReferences) readssys_fileas the caller and then takes the refusal arm. The engine already demoted its own line for this exact reason. The comment at:13252says "fired once per read for every reader withoutsys_fileread", and the line at:13270is nowlogger.debug.- The WARN that remains is the security layer's own. It comes from
packages/plugins/plugin-security/src/security-plugin.ts:2839(ctx.logger.warn(developerMessage, …)), on thefindthe engine issued as the caller. - The same shape was already fixed once: the activity read gate asks
canReadObjectbefore it reads ("A skipped object is not an access attempt",parent-record-read-gate.tsheader).
Wanted
- An ordinary record read whose only
sys_filetouch is hydration logs no access-denied WARN. For example, the engine asks before it reads, as the activity gate does, or the hydration read carries a context that marks it internal. The lane picks the shape and says why. - ⛔ The refusal itself is unchanged. A direct
GET /api/v1/data/sys_file/IDby the same reader still answers 403PERMISSION_DENIED, and still logs as a refusal. - A pin: the reader's record read logs 0 security WARNs. Control: a direct
sys_fileread logs its one.
Dedupe
- A REST scan of the 540 objectstack issues updated since 2026-09-15 (open and closed), for the WARN's text,
sys_filebeside warn or log on every read, andresolveFileReferencesbeside warn orcanReadObject, found nothing. - Nearest by subject: objectql: a file field reads "no file" for a record reader without sys_file read: the hydration lookup runs as the caller, and a refusal reads as absent data (split from #22590 item 1) #22593 (closed: the hydration's loud half), objectql: a file field's metadata follows the parent-derived verdict the download door applies — hydrate a field-owned file for a reader refused
sys_fileread (ruling B on #22624) #22637 (closed: the field-owned access half) and runtime (17.7.0): file fields read "no file" for a record reader without sys_file read, flow run summaries report acted 0 while notifying, and the activity read pre-scan caps at 2000 rows fail-closed #22590 (item 1, verified on 18 by the round that found this line).
Dedupe words: sys_file hydration access denied warn every record read; security access denied sys_file positions org_member file field; resolveFileReferences caller read logs denial.
Generated by Claude Code
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
area:filesFiles — upload, download, signed URLs, access derived from the parent recordFiles — upload, download, signed URLs, access derived from the parent recorddomain:engineRetired 2026-10-22, alias of domain:server (engine surface); in-flight cards keep it until they landRetired 2026-10-22, alias of domain:server (engine surface); in-flight cards keep it until they landpriority:p2Medium: important, M3Medium: important, M3
Activity
objectstack-fleet commentedon Oct 18, 2026
Triage: p2 ·
area:files·domain:engine·pm:queue· triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-18T13:57ZTriage seat (objectstack-wide, seat post #6015). ⛔ Not a claim, ⛔ not a dispatch.
origin/main:packages/objectql/src/engine.tsresolveFileReferencesreadssys_fileas the caller, and the engine's own line was already demoted todebug. The remaining WARN is the security layer's (plugin-security/src/security-plugin.ts). The premise holds.domain:engine. The likely fix is "ask before reading" in the engine's hydration, as the activity gate does. If the lane picks "mark the hydration read internal" instead, the security layer's half crosses intodomain:services, and the lane says so.sys_fileread still answers 403 and still logs.Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
Claim: PM loop round 10 · 2026-10-19T01:06Z · after triage 6250776423 (p2
area:files). An ordinary record read whose onlysys_filetouch is file-field hydration logs no access-denied WARN. A directsys_fileread by the same reader still answers403and still logs.Session:
session_01JtSQQ3EWsbNPzyYMug1tXy(the dispatching seat; the dev runs as its own cloud session, named in the dispatch comment)Account:
os-project-manager(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-24335-file-hydration-no-warnWorktree:
objectstack-issue-24335Domain:
domain:engineSeat:
domain:engine#1(seat post #6367)File surface:
packages/objectql/src/engine.ts,resolveFileReferencesand the helper its docblock names (about:749–:766): the engine asks whether the caller may readsys_filebefore it issues the caller'ssys_fileread, as the activity gate does (parent-record-read-gate.ts, "A skipped object is not an access attempt").sys_fileread (ruling B on #22624) #22637).packages/plugins/plugin-security(domain:services). It also adds a bypass marker.needs_decision.GET /api/v1/data/sys_file/IDby the same reader answers403 PERMISSION_DENIEDand logs its one refusal;sys_fileread is unchanged..changeset/24335-*.md.plugin-security's refusal or its log line, no change to who may read a file, and no new error code.Container & model:
M,mode:cloud(measured on a booted composition: the security WARNs and the bodies for a member's by-id and list reads of an object with a file field, and the directsys_fileread, before and after),model: TIER_DEFAULT(this run'sdispatch-gates.mjs --tier: "no path-derived mandate")Clause-②: no (no accept set or public surface moves: the served bodies and every refusal are unchanged; owes the seat's review of record at
TIER_DEFAULTbefore the queue)Responsibility:
resolveFileReferencesreadssys_fileas the caller and takes the refusal arm, soplugin-securitylogs an access-denied WARN on every ordinary record read by a reader withoutsys_fileread | the engine already demoted its own line for this reason, and the activity gate already asks before it reads | every operator whose log the WARN floods, burying real refusalsThread-read: 6250776423
Serial constraints cleared: open PRs' file lists and the
pm:dispatchedclaims were read at this claim.engine.ts.domain:spec's Hook ctx.title is documented as part of every hook context, but @objectstack/verify bootStack runs app hooks without it: "ctx.title is not a function" under the test harness, while objectstack dev serves it #24289 rider (6257135373) touchesengine.ts'sbindHooks, disjoint from file hydration; whichever lands second mergesmain.epic:#15194lane's tenant derivation inengine.tsis not met.Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
Dispatch ·
domain:engineseat 1 ·session_01JtSQQ3EWsbNPzyYMug1tXy· 2026-10-19T01:11Z:mode:cloud. The dev session issession_01YXbt8cZ1Adcz5iV8AdwBXJ. It builds at the default tier on branchclaude/issue-24335-file-hydration-no-warn, under claim 6257482074.area:files; the ⛔ that a directsys_fileread still answers403and logs), objectql: a file field reads "no file" for a record reader without sys_file read: the hydration lookup runs as the caller, and a refusal reads as absent data (split from #22590 item 1) #22593 and objectql: a file field's metadata follows the parent-derived verdict the download door applies — hydrate a field-owned file for a reader refusedsys_fileread (ruling B on #22624) #22637 (the hydration's ruled behaviour), and the activity read gate's precedent ("A skipped object is not an access attempt").sys_filebefore issuing the caller's read, so a reader without it goes straight to the field-owned arm.plugin-securitystays the one judge and is not edited. A context flag marking the read internal would add a bypass marker indomain:services; it is not built under this claim.sys_fileread's403and its one refusal; a reader withsys_fileread; and the ask's cost per read.TIER_DEFAULTreview of record before the queue.Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
os-dev-report
{
"issue": 24335,
"status": "done",
"branch": "claude/issue-24335-file-hydration-no-warn",
"pr": "#24434",
"session": "session_01YXbt8cZ1Adcz5iV8AdwBXJ — this dev run (Claude-Session line); dispatched by session_01JtSQQ3EWsbNPzyYMug1tXy",
"premise_still_valid": true,
"summary": "Premise held on origin/main 71fa166: resolveFileReferences read sys_file as the caller, and a member without sys_file read logged one plugin-security WARN per by-id and per list read (reproduced on the booted composition: "[Security] Access denied: operation 'find' on object 'sys_file' is not permitted for positions [org_member, everyone]"). The engine now asks the registered security service's canReadObject('sys_file', caller minus operation-private keys) through its existing slot (resolveReadableIdsSecurity) before issuing the read. A refused caller is served the refused / field-owned arm without the read, and the arm moves verbatim into serveRefusedFileHydration, shared with the existing PERMISSION_DENIED catch. Unasked (no service, no canReadObject, a throwing ask) reads as before. Bodies are byte-identical, the direct sys_file read is still 403 and still logs once, and plugin-security is untouched. Main moved from the dispatch's 97cb5c5 to 71fa166 at the cut, and e22defb was merged in before the PR opened (no objectql overlap; the #24289 rider had not landed).",
"tests": "Head df035c8. (1) objectql: pnpm --filter @objectstack/objectql exec vitest run → 445 files, 8366 tests passed. The pin file engine-file-hydrate-refused.test.ts plus parent-derived: 31 passed, 7 of them new. (2) Booted pin packages/qa/dogfood/test/file-field-hydration-no-denial.dogfood.test.ts → 5 passed, before and after the merge. The before/after table is measured on one boot, with 'before' = canReadObject withdrawn = the pre-change engine path. Member by-id: denials 1 → 0, body byte-identical (res.text()), file hydrated. Member list: 1 → 0, byte-identical. sys_file reader by-id and list: 0 → 0, byte-identical, same hydrated value. Member GET /data/sys_file/ID: 403 PERMISSION_DENIED, 1 denial. (3) Ablation of the ask, via scripts/ablation-replace.mjs: anchor x1 → x0, blob 0186fa3c → b21c19e7; objectql rebuilt; ablation-dist-preflight found the marker in 4 dist files. Dogfood: 2 failed / 3 passed, with the WARN-count pins red on the exact issue line. Restore: blob == HEAD 0186fa3c, git diff HEAD empty, rebuild, --absent preflight clean, tree clean, 5 passed. (4) Cost, measured on the booted stack (shared box): 1 ask per by-id read and 1 per list read (per request, never per row). A cached permission check, no store read: 6.4 us per ask (mean of 300). The refused sys_file read it replaces cost about 1.2 ms. End-to-end latency was within noise.",
"gates": "Local, head df035c8. node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths) → 74 families. 72 run on df035c8, each exit 0, each $? captured before any pipe. 2 NOT-MEASURED: (a) check:query-options-erasure, a repo-wide eslint scan. The first attempt at a 4096 MB heap was in flight when the container killed the worker (exit 137), and the retry hit timeout 280 s (exit 124). (b) check:type-check-debt, a repo-wide tsc census, also exit 124 at 280 s. Both are left to CI. check:dual-build-cjs-loads first exited 3 (PREREQUISITE NOT MET, 8 unbuilt packages); it was built for, then needed a spec rebuild, and passes (111 require entry points load). --ran reconciliation exit 0: '74 derived, 72 run, 2 NOT-MEASURED, 0 UNRUN'. Typecheck exits: @objectstack/objectql 0, @objectstack/dogfood 0, both before and after the merge. check-platform-checklist --changed-from origin/main: exit 0. CI on PR #24434 at report time: 'No other open PR may claim the same issue' success; 'Packed-tarball smoke (opt-in)' skipped; every other check-run in_progress, including the required Lint & Repo Gates lanes, TypeScript Type Check lanes, Check Changeset and Governed Surface Queue Guard. Not re-run locally.",
"line_budget": "none set; diff vs merge base e22defb: .changeset/24335-file-hydration-no-denial.md +17/-0; packages/objectql/src/engine.ts +106/-34; packages/objectql/src/engine-file-hydrate-refused.test.ts +134/-5; packages/qa/dogfood/test/file-field-hydration-no-denial.dogfood.test.ts +284/-0; total +541/-39",
"files_changed": [
".changeset/24335-file-hydration-no-denial.md",
"packages/objectql/src/engine.ts",
"packages/objectql/src/engine-file-hydrate-refused.test.ts",
"packages/qa/dogfood/test/file-field-hydration-no-denial.dogfood.test.ts"
],
"deviations": [
"Branch: the shared checkout already had the branch checked out at origin/main. It was detached there so the worktree ../objectstack-issue-24335 could hold the branch. Same name, same base 71fa166.",
"Fence: serveRefusedFileHydration is a new private helper extracted verbatim from resolveFileReferences' refused arm, beside the docblock's readParentDerivedFiles. callerMayReadSysFile is the ask itself. Both live in engine.ts inside the claimed surface.",
"The objectql pin extends the existing engine-file-hydrate-refused.test.ts and reuses its driver double, rather than adding a new file with a new double.",
"Two derived families are NOT-MEASURED locally (repo-wide scans, timeout and an earlier container worker kill); declared to CI.",
"Commit trailers use AGENTS.md's model-free pair, not the harness reminder's model-named Co-Authored-By; AGENTS.md wins and the pre-push hook enforces it.",
"Several git pushes to the branch (WIP commits, then the merge): each a step push under os-dev resource rule 6, all on the one branch."
],
"mcp_calls": "0 — no MCP GitHub tool was called (read or write)",
"api_writes": "2 relay dispatches as objectstack-fleet[bot] via scripts/pm/with-fleet.sh --via dispatch: (1) pr_create → POST /repos/objectstack-ai/objectstack/pulls (draft) + POST /repos//issues/24434/assignees [os-project-manager]; (2) comment → POST /repos//issues/24335/comments (this report). Plus git push to claude/issue-24335-file-hydration-no-warn (not REST). Reads were unauthenticated GETs against api.github.com (issue, comments, PR, check-runs).",
"open_questions": [
{
"question": "Shape: ask before reading vs a context flag marking the hydration read internal",
"options": [
"A ask canReadObject('sys_file') first, engine-only (taken)",
"B internal-read context flag honoured by plugin-security"
],
"recommendation": "A, settled by the four axes. Real need: measured on the booted composition. Long-term soundness: the security layer stays the single judge and nothing in domain:services moves. AI-mistake resistance: no bypass marker an author could set. Startup scope: no new API or gate. Measurement showed the body is identical, so needs_decision was not reached."
},
{
"question": "What should the engine do when the canReadObject ask throws?",
"options": [
"A read as the caller, so the middleware decides (taken)",
"B fail closed to the refused arm"
],
"recommendation": "A. The ask only narrows reads, so falling back to the caller's own read never widens. B would change a served body (bare ids to refused markers) on a broken service. The real canReadObject never throws (it catches internally and denies)."
}
],
"out_of_scope_findings": [
"carrier: none · noted, not filed. canReadObject does not mirror the middleware's publicFormGrant arm (the form grant admits find only on the form's own target). A form targeting sys_file is not a realistic declaration, so this path is unreached. Recorded in PR Acceptance notes."
]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
Landed ·
domain:engineseat 1 ·session_01JtSQQ3EWsbNPzyYMug1tXy· 2026-10-19T03:39ZTIER_DEFAULTreview of record 6258712209, PASS, at headdf035c8343. ACCEPT 6258740216. Not governed.gh-readonly-queue/main/pr-24434-*branch remains.origin/main9112bcd887is onorigin/main. It carries PR #24434's 4 files, +541 / −39.completedthroughFixes #24335;pm:dispatchedis cleared.What
mainnow holds:resolveFileReferencesasks the security service'scanReadObject('sys_file', caller)before it issues the caller'ssys_fileread. A caller it refuses is served the refused / field-owned arm (serveRefusedFileHydration) with no access attempt.GET /api/v1/data/sys_file/IDstill answers403 PERMISSION_DENIEDand logs its one refusal.plugin-securityis unchanged.The dev session
session_01YXbt8cZ1Adcz5iV8AdwBXJis archived.Generated by Claude Code