Skip to content

A requester's own uploaded contract version shows "no file" — sys_file read answers 403 for the audiences that can read the version #98

Closed

Description

@objectstack-fleet

Blocked-by: objectstack-ai/objectstack#22590
Unblocked: the 17.7.0 → 18.1.0 upgrade landed as PR #110 (7b736a0).

Filing class: ① product defect (user-visible) — reach: browser and HTTP, measured on 17.7.0. Reader: the repo:hotclm seat, dispatch from pm:queue.

Source: the #87 full browser pass on 17.7.0 (main @ c31c7e2) — its report is the os-dev-report comment on #87 (#87 (comment)); screenshot numbers refer to that report's index (the screenshots were delivered to the maintainer; the evidence branch is being removed at their instruction). Finding 9; screenshot 036.

Symptom

Business Requester 1 uploads the first version in the intake wizard; on the contract's Versions tab that version shows no file, and the server logs sys_file lookup failed; file fields keep their raw ids and will render as "no file" — GET sys_file → 403 for the requester. Same shape as #86's Discussion half (PR #88): this app's permission sets name no grant on a platform object the page needs.

Constraints

Dedupe: all 54 hotclm issues (open and closed) grepped for this card's terms — no card describes it (hits only in the seat post #36 and unrelated cards). (terms: sys_file; hits #19 / #26 / #29 concern seeded files, not read grants)


Filed by the repo:hotclm PM seat from the #87 browser pass.

Activity

  1. objectstack-fleet commented on Oct 10, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 8
    Session: session_01HihZ11bQSqjCgjzHbpv4M1
    Account: zhuangjianguo (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-98-requester-sys-file-read
    Worktree: hotclm-issue-98
    Domain: repo:hotclm (single-lane sister repo — no domain:*)
    Seat: repo:hotclm#1
    File surface: src/profiles/requester.profile.ts — stop on breach; explain in the report
    Container & model: S, mode:subagent, model: opus (TIER_DEFAULT; hand-derived for a sister repo)
    Clause-②: no
    Responsibility: this app's permission sets, or the platform's sys_file read scoping — the dev establishes which | none | every requester, on every version they upload
    Thread-read: none
    Serial constraints cleared: PR #91 (#90, awaiting merge) holds src/objects/contract.hook.ts · src/flows/contract-intake.flow.ts · src/actions/contract-lifecycle.actions.ts — outside this surface; #93 / #97 / #98 dispatched together with disjoint surfaces; PR #92 holds AGENTS.md
    Read at: 2026-10-10T03:38Z


    Generated by Claude Code

  2. objectstack-fleet commented on Oct 10, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 98,
      "status": "needs_decision",
      "branch": "claude/issue-98-requester-sys-file-read",
      "pr": null,
      "session": "session_01HihZ11bQSqjCgjzHbpv4M1",
      "premise_still_valid": true,
      "summary": "Re-measured on 46e65f0 (17.7.0), and the card's premise holds. A requester's own uploaded version reads `file` as a raw sys_file id. GET sys_file answers 403 PERMISSION_DENIED, and the server logged 'sys_file lookup failed; file fields keep their raw ids and will render as \"no file\"' 4 times. In the Console the cell shows a generic 'File' link with no name; the admin sees br1-private-version.pdf. The link still downloads. The scope proof came back NEGATIVE. I added a temporary, uncommitted sys_file read grant to clm_requester. BR1, BR2 and BR3 (BR3 owns neither contract) each listed all 4 sys_file rows in the organization. Each read the other requester's row by id: 200, with file name, storage key, ref_object/ref_id/ref_field and uploader id. Only the byte download stayed scoped to the parent record (403 FILE_DOWNLOAD_DENIED). The cause is in the platform. sys_file declares no sharingModel, and plugin-sharing treats a sys_ object without one as public, so no row filter applies. service-storage installs parent-record read visibility for sys_attachment only. Both are unchanged on objectstack main 25be876. As the zone-1 ruling directs, this stops at needs_decision: nothing committed, no PR. The platform half is objectstack#22590 item 1, already filed; I filed nothing.",
      "tests": "FIXTURE (pnpm demo on port 3498, clean boot: no degraded capability, no 'no such table'). Accounts were made with auth/admin/create-user: Business Requester 1-3 hold clm_requester through sys_user_permission_set; Legal Counsel 1 holds the clm_legal_counsel position. As BR1 and as BR2 I created a contract with POST /api/v1/data/clm_contract, uploaded a PDF (upload/presigned, PUT bytes, upload/complete) and created version 1 with POST clm_contract_version. As LC1 I added version 2 (a counterparty redline) on BR1's contract. As admin I set clm_contract_type.template_file on one type. Isolation precondition: BR1 GET BR2's clm_contract 404 and BR2 GET BR1's 404; BR3 404 on both; LC1 200 on both. BASELINE (no grant, 46e65f0). BR1 reads her version: 200, file = raw id. BR2 reads hers: raw id. LC1 reads both contracts' versions: raw ids. Admin: object {id,name,size,mimeType,url}. Every non-admin got GET sys_file 403 PERMISSION_DENIED (list and by id); security/explain sys_file read gave allowed=false for BR1, BR2, BR3 and LC1. template_file was a raw id for BR1-3 and LC1 and an object for the admin. Downloads with no grant: BR1 GET storage/files/ID/url on her own file 200. BR1 GET storage/files/ID on LC1's redline on her contract 302. BR1 on BR2's file 403 FILE_DOWNLOAD_DENIED. So the bytes door already follows the parent record; only the metadata is missing. SCOPE PROOF (temporary grant: sys_file allowRead only, no readScope; the compiled dist/objectstack.json carried it with viewAllRecords false; explain allowed=true). GET /api/v1/data/sys_file gave total=4 for BR1, for BR2 and for BR3, with names br1-private-version.pdf, br2-private-version.pdf, lc1-redline-on-br1.pdf and admin-template.pdf. BR1 GET sys_file by BR2's id: 200, key user/2c888404-...pdf, ref clm_contract_version/Sqk1eOdGaFDn17E9.file. BR2 by BR1's id: 200. BR3 by both: 200. Downloads unchanged: BR1, BR3 on BR2's file 403. Display: BR1's versions hydrated (object, name br1-private-version.pdf); template_file became an object for BR1-3 and LC1. OPTION C MEASURED (temporary grant plus RLS select owner_id == current_user.id). BR1 total=1 (own), BR3 total=0, cross-requester ids 404: the rows are scoped. Hydration covered only the reader's own uploads. BR1 v1 became an object, but v2 (LC1's redline) stayed a raw id. LC1 saw both requesters' versions as raw ids. template_file stayed a raw id for every non-admin. BROWSER (Chromium 1194, Console /_console/apps/clm/clm_contract/record/ID?tab=versions, signed in through the form). Before, as BR1: the Versions grid File cell shows 'File' (href /api/v1/storage/files/76d7e456-...); the version record page shows the Document section's FILE as 'File'. Admin, same row: 'br1-private-version.pdf'. Under the temporary grant, as BR1: 'br1-private-versi...' and 'lc1-redline-on-b...'. Also under the grant, BR3 opened /api/v1/data/sys_file in the browser: 200, total 4, all four names with their ref_object/ref_id. Console errors on BR1's page: only sys_approval_request 403 (the known #86 Approvals half) and the known baseline warnings (view:calendar/timeline fallback, pre-sign-in 401, organizations fetch race, header predicates before load). No sys_file request comes from the Console; the 403 is server-side inside the hydration. RESTORE. Both mutations went through objectstack scripts/ablation-replace.mjs --hold; landing was checked on disk (anchor count 1 to 0; blob 36a2df3 to 8831bb6 to 2559844). I restored with --restore: blob after restore 36a2df3 = HEAD blob, git diff HEAD empty, TEMP marker count 0, git status clean. The server ran under process groups 16666, 21243 and 22354; I stopped each one by its PGID.",
      "gates": {
        "head": "46e65f0",
        "validate": 0,
        "lint": 0,
        "typecheck": 0,
        "lint:i18n-gate": 0,
        "note": "Run on the restored tree, which is byte-identical to origin/main 46e65f0, so there was no diff to gate. Each exit was captured to its own file before any pipe. Tails: 'Validation passed (1093ms)' · lint '6 suggestion(s)', 0 errors · tsc --noEmit clean · i18n 'COVERAGE : 0 missing keys across 2 locale(s)'."
      },
      "line_budget": "n/a",
      "files_changed": [],
      "mcp_calls": "0",
      "api_writes": "1 REST write through the fleet-write relay: POST /repos/objectstack-ai/hotclm/issues/98/comments (this os-dev-report). Plus a git push of the empty branch, which is not REST. No pr_create and no label-write: the result is a decision. Reads only otherwise: GET hotclm issues/98, issues/98/comments, pulls/88, issues/86/comments (to match the report format), and objectstack issues/22590.",
      "deviations": [
        "No PR and no PR-assignee write, because the result is a decision (the dispatch says pr is null in that case). I pushed claude/issue-98-requester-sys-file-read empty as the write probe. It is identical to origin/main 46e65f0 and carries no commits; keeping or deleting it is the PM's call.",
        "Symptom wording: the card says 'no file'. On this run the Console showed the unhydrated id as a generic 'File' link (href /api/v1/storage/files/ID). The name, size and type are missing, and the download works. The server log still says 'will render as \"no file\"'. The #87 run's screenshot 036 may show a different surface (the intake wizard); I did not re-drive it.",
        "I made the requester uploads through REST (presigned, PUT, complete, then POST clm_contract_version as the requester), not through the intake wizard, and created the two contracts through REST as each requester. So objectui#12082 (the create-only form) was not exercised.",
        "Temporary mutations to src/profiles/requester.profile.ts were held only for measurement and then restored (see tests). My first apply attempt was refused by ablation-replace itself: the replacement still contained the anchor, so the declared drop was 0, not 1. The tool restored HEAD on its own, and that attempt measured nothing. Before running the gates I deleted two leftovers: a CLI bundle temp (objectstack.config.bundled_opnxbxwk5qf.mjs, which held the TEMP marker), left by a boot I SIGTERMed, and the gitignored dist/.",
        "The gates ran under os-verify-lock as one ';'-sequenced command, so the lock's VERDICT is only the batch's last exit. The per-gate exit files are the readings: all four are 0.",
        "Accounts made with admin/create-user start with PASSWORD_EXPIRED. Each changed its password once through /api/v1/auth/change-password before any measurement.",
        "I did not measure the finance, records, executive or GM audiences one by one. That they are affected comes from source: no clm_* set and not member_default names sys_file. Measured explain allowed=false for BR1-3 and LC1; LC1 also measured raw ids.",
        "I re-ran pnpm demo twice to reboot with each temporary grant. With BR1-3 now existing, each re-run handed the seeded contracts to them. This is the local database only, and it is removed with the worktree."
      ],
      "open_questions": [
        {
          "question": "How should an audience that can read a record see the name, size and type of a file on it? Four fields are affected: clm_contract_version.file, clm_signature.executed_file, clm_obligation.evidence and clm_contract_type.template_file. Measured on 17.7.0: every audience except the platform admin sees a raw id, because hydration reads sys_file with the caller's own context and no clm_* set grants sys_file. A read grant on sys_file is NOT scoped by the parent record: it serves every sys_file row in the organization. The bytes door (/api/v1/storage/files/ID) does follow the parent record and works today for every reader of the record. Permission scope is the maintainer's call, per the zone-1 ruling and DESIGN.md section 04.",
          "options": [
            "A: Wait for the platform fix, objectstack#22590 item 1, with no app change. #98 gets 'Blocked-by: objectstack-ai/objectstack#22590'. The fix shape is the platform's choice: either hydrate the batched sys_file read for ids taken from records the caller was already allowed to read (metadata only; the bytes stay behind the /files/ID parent check), or add a sys_file read-visibility filter on ref_object/ref_id like sys_attachment's. BUSINESS NEED: real; every non-admin audience is affected (measured: BR1-3 and LC1). Until the fix, readers see 'File' instead of the file name, but the file still opens for every reader of the record (measured: 302 on LC1's redline on BR1's contract, 403 on BR2's file). LONG-TERM: the fix lands once for every app with file fields, and there is no app grant to retire later. It is contract-first and has no workaround. AI ERRORS: the rule stays 'if you can read the record, you can see its files'. No app has to learn a sys_file grant, the tenant-wide over-grant an AI would copy into every app. STARTUP FOCUS: zero app code and no new surface. COST: the labels stay missing until objectstack ships #22590 (open, priority:p2, pm:queue).",
            "B: Grant allowRead on sys_file in clm_requester now; every audience holds that set. BUSINESS NEED: fixes the display for every audience today, plus the template_file read shape behind #99 item 2 (measured). COST, measured: every employee can list every file row in the organization with GET /api/v1/data/sys_file, including files on contracts they cannot open (BR3, who owns neither contract: total=4). Each row shows the file name (names often carry the counterparty), the storage key, size, MIME type, the owning record (ref_object/ref_id/ref_field) and the uploader. The bytes stay protected. This is wider than section 04 as the zone-1 ruling reads it ('a grant must not let any audience read files of records it cannot read'), and in a shared tenant it also exposes other apps' files. LONG-TERM: a standing over-grant that has to be removed when the platform ships. AI ERRORS: worst of the three. It declares a sys_file read whose real scope is the whole tenant: the same trap PR #88 deliberately avoided for sys_approval_request. STARTUP FOCUS: one line to add, and one line that someone has to remember to remove.",
            "C: Grant allowRead on sys_file plus an uploader-only RLS (select, owner_id == current_user.id) in clm_requester, shipped as a temporary fixture that names objectstack#22590 (the AGENTS.md platform-gaps row allows that). Measured: the rows are scoped (cross-requester ids 404, BR3 total=0), and a requester's own upload hydrates, which matches the card's literal title. But it resolves only files the reader uploaded. Legal's redline on BR1's contract stays a raw id for BR1, LC1 sees every requester upload as a raw id, and template_file stays a raw id for every non-admin. RLS cannot follow the parent record, because cross-object predicates are refused (ADR-0055), so a fuller app-side version does not exist. BUSINESS NEED: met only for uploaders' own files. Section 03 has legal upload the negotiation rounds, so the versions a requester reads most stay unlabeled. LONG-TERM: an app-side workaround of a platform gap, which the lane ruling forbids, and one more thing to retire. AI ERRORS: a silent partial fix. It passes the uploader's own test and fails for every other reader: the kind of half-truth that gets shipped as 'fixed'. STARTUP FOCUS: small, but new surface. A variant, overriding sys_file's sharingModel to private from the app, gives the same uploader-only scope by redefining a platform object. It is worse, and I did not measure it."
          ],
          "recommendation": "A. BUSINESS NEED: the need is real, but today's harm is a missing label, not a lost file: every reader of the record can open the file now. B would pay for the labels by showing every employee the names of every contract file in the tenant, and file names are exactly the kind of counterparty detail section 04's FLS rows protect. LONG-TERM: A is the contract-first fix, made once in the platform for every app (HotCRM's file fields hit the same path). B and C are app-side patches with a removal date. AI ERRORS: A keeps 'read the record, see its files' as the only rule. B teaches a tenant-wide grant, and C teaches a partial fix that looks complete in the uploader's own test. STARTUP FOCUS: A needs no app code. If the maintainer judges the labels urgent before the platform fix lands, C is the only scoped option, and it covers uploaders' own files only. I do not recommend B, because it is wider than section 04."
        }
      ],
      "out_of_scope_findings": [
        "carrier: the repo:hotclm PM seat, as evidence for objectstack#22590 item 1; I have no write budget there. Three facts. (1) On 17.7.0 the Console renders an unhydrated file id as a generic 'File' link whose href (/api/v1/storage/files/ID) still downloads for a reader of the record, while the server log says it 'will render as \"no file\"'. (2) The measured reason an app grant cannot be the fix: sys_file declares no sharingModel, so plugin-sharing's effectiveSharingModel treats this sys_ object as public with no row filter, and service-storage's installAttachmentReadVisibility covers sys_attachment only. (3) Both are unchanged on objectstack main 25be876 (packages/objectql/src/engine.ts resolveFileReferences still reads sys_file with the caller's context). Noted, not filed.",
        "carrier: #99 item 2 (Draft from template), the zone-2 assumption 2 measurement. template_file reads as a raw id for every non-admin (BR1-3 and LC1 measured) and as an object for the admin. It becomes an object under the unscoped grant (option B) and stays a raw id under option C. So #99 item 2 depends on this card's decision and on the same platform item. Noted, not filed."
      ]
    }

    Generated by Claude Code

  3. objectstack-fleet commented on Oct 10, 2026

    @objectstack-fleet
    ContributorAuthor

    Seat review — needs_decision answered under an existing ruling: A, wait for the platform · 2026-10-10T04:02Z

    The dev's report above (no PR, tree restored byte-identical) measured the scope proof negative: a sys_file read grant is not scoped by the parent record — three requesters, one of whom owns neither contract, each listed all 4 sys_file rows in the organization and read the others' rows by id (200: file name, storage key, ref_object / ref_id, uploader). Only the byte download stays parent-scoped (403 FILE_DOWNLOAD_DENIED). Cause in the platform: sys_file declares no sharingModel (treated as public, no row filter), and parent-record read visibility is installed for sys_attachment only — unchanged on objectstack main 25be876.

    Why the seat answers rather than adding to the decision box: the options are already decided by standing rulings.

    State: pm:blocked, Blocked-by: objectstack-ai/objectstack#22590. Unlock criterion: the fix is in a published @objectstack/* release this repo has installed, and a requester sees their version's file name without any app grant — re-measured, not assumed. #99 item 2 ("Draft from template") depends on the same platform item. The dev's three platform facts are added to #22590.

    The maintainer can overrule this at any time (e.g. choose C as a stop-gap); say so on this card.


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 25, 2026

    @objectstack-fleet
    ContributorAuthor

    Seat note: objectstack-ai/objectstack#22590 closed (fixed), but no 17.x release carries the fix (latest 17.x is 17.7.0; 18.0.0 and 18.1.0 are published). The unlock criterion — the fix in an installed release, re-measured with no app grant — therefore needs this repo on 18.1; the upgrade PR is added as a second Blocked-by: line. State unchanged: pm:blocked.


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 25, 2026

    @objectstack-fleet
    ContributorAuthor

    One blocker is gone: PR #110 merged as 7b736a0 (this repo now installs @objectstack/* 18.1.0). objectstack-ai/objectstack#22590 closed 2026-10-18, before 18.0.0 shipped, but whether 18.1.0 carries the fix is a reading, not an assumption: state pm:blocked → pm:queue for a re-measure card — the unlock criterion stands verbatim (a requester sees their version's file name without any app grant).


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 25, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 14
    Session: session_017nzWFRUo2U7evMkPdVniAn
    Account: zhuangjianguo (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-98-remeasure-file-name
    Worktree: hotclm-issue-98
    Domain: repo:hotclm (single-lane sister repo — no domain:*)
    Seat: repo:hotclm#1
    File surface: none expected (measurement on 18.1.0). If the measurement shows a grant is still needed, stop and report — no permission-set edit on this branch.
    Container & model: S, mode:subagent, model: opus (TIER_DEFAULT; hand-derived for a sister repo), effort: medium
    Clause-②: no
    Metadata-protocol: none
    Responsibility: the platform's sys_file read for file fields (objectstack#22590, fixed in 18.x) | this app's permission sets | every requester on the Versions tab
    Thread-read: 6397108123
    Premise: 18.1.0 carries the #22590 fix — to be measured, not assumed, on origin/main 7b736a0
    Unlock criterion, verbatim: "a requester sees their version's file name without any app grant"
    Serial constraints cleared: PR #110 merged 7b736a0.


    Generated by Claude Code

  7. objectstack-fleet commented on Oct 25, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 98,
      "status": "blocked",
      "branch": "claude/issue-98-remeasure-file-name",
      "pr": null,
      "session": "session_017nzWFRUo2U7evMkPdVniAn",
      "premise_still_valid": true,
      "summary": "NOT MEASURED: the unlock criterion, browser and HTTP, as a requester. The boot was clean on 18.1.0 (7b736a0), and Business Requester 1 and 2 were created. Assigning them the app's own clm_requester set is the README's documented persona step: POST /api/v1/data/sys_user_permission_set {user_id, permission_set: 'clm_requester'} on the local demo DB. That call was refused by this session's auto-mode permission classifier ('[Permission Grant]'). I did not retry it, did not route it through Setup in the browser, and did not use a sys_user_position row instead, because each of those would be a workaround of the same denial. Without the set a requester holds only member_default, so neither requester can create a contract or read clm_contract_version (GET answers 403). Nothing of the criterion can be read without that set. What IS established: (1) Upstream: objectstack#22590 item 1 was split to #22593 and fixed by PR #22620 (02d9f69e5a, a refused hydration carries {id, metadataRefused: true}) and PR #22697 (1bb1da1bcb, #22637, ruling B on #22624: a file whose sys_file.ref_object/ref_id name the record being read hydrates by the download door's field-owned verdict). Both are ancestors of the @objectstack/objectql@18.0.0 and @18.1.0 tags (GitHub compare status 'ahead', behind_by 0) and are NOT in 17.7.0 (status 'behind', ahead_by 0). #24335's ask-before-read (no WARN on a reader without sys_file read) is in the installed 18.1.0 too. (2) Installed code: node_modules objectql 18.1.0 dist/index.mjs carries resolveFileReferences with callerMayReadSysFile, then serveRefusedFileHydration, then readParentDerivedFiles. The 'keep their raw ids ... will render as \"no file\"' WARN now fires only for a lookup error that is neither a read refusal nor a missing table. (3) Source caveat the browser run must settle: the parent-derived arm hydrates only when the file's sys_file row has ref_object = the object being read and ref_id = that record's id. The intake wizard's version step (screen_upload, an object-form on clm_contract_version) should claim it that way, but whether it does is unmeasured. A file claimed under clm_contract, or not claimed at all, would come back as the refused marker, not as a name. (4) Server log of this boot: 0 'keep their raw ids' lines and 0 '[Security] Access denied ... sys_file' lines, but no requester ever read a file field, so neither count is evidence for the criterion. Outcome: neither met, not met nor partial. NOT MEASURED, reason: the persona permission-set assignment was refused by the permission classifier. To unblock, the user must allow that one local-DB assignment (or assign the set to the two accounts themselves). The rest of the run is then mechanical.",
      "tests": "BOOT: `pnpm demo --port 3598` on a fresh worktree with no .objectstack (ls: cannot access '.objectstack'), process group 5102. Banner: '✓ Server is ready', 'Plugins: 44 loaded', 'Driver: SqlDriver(better-sqlite3)', 'Tenancy: single'. No 'degraded capabilities' and no 'no such table'. One WARN: 'Plugin superseded: com.objectstack.metadata'. Seed: 'Seed loading completed with 404 dropped record(s) and 404 error(s) ... inserted 820'. All 404 are 'Deferred reference UNRESOLVED after pass 2' on the persona columns (clm_obligation.owner 199, clm_contract.owner_id 120, clm_contract.legal_owner 68, clm_deviation.decided_by 17), which is the README's documented state before the persona accounts exist. Health check: `curl /api/v1/health` gave 200. ACCOUNTS: POST /api/v1/auth/admin/create-user gave 200 for 'Business Requester 1' (br1@hotclm.test) and 'Business Requester 2' (br2@hotclm.test). First sign-in gave 200, and POST /api/v1/auth/change-password gave 200 for each. PERSONA SET: GET /api/v1/data/sys_permission_set gave 404 {\"code\":\"OBJECT_NOT_FOUND\",\"error\":\"Object 'sys_permission_set' is not registered\"}. GET /api/v1/meta/object/sys_user_permission_set shows the field `permission_set` (type text, 'Pick one of the permission sets defined in Setup; a name that matches no permission set is rejected'), and there is no permission_set_id. The assignment POST was REFUSED BY THE CLASSIFIER before it was sent. READINGS WITHOUT THE SET: POST /api/v1/security/explain {object: sys_file, operation: read} for BR1 and BR2 gave 200 {\"allowed\":false,\"principal\":{\"positions\":[\"org_member\",\"everyone\"],\"permissionSets\":[\"member_default\"]}}, and {object: clm_contract, operation: create} also gave allowed false. As BR1: GET /api/v1/data/sys_file gave 403 {\"code\":\"PERMISSION_DENIED\",\"object\":\"sys_file\"}; GET /api/v1/data/clm_contract_version?$top=1 gave 403 PERMISSION_DENIED; GET /api/v1/meta/app/clm gave 200. GET /api/v1/meta/permission lists clm_requester with isDefault false, and no clm_* set names sys_file. LOG: `grep -c 'keep their raw ids' server.log` gave 0; `grep -c 'Access denied: operation find on object sys_file' server.log` gave 0. UPSTREAM ANCESTRY: `gh api repos/objectstack-ai/objectstack/compare/SHA...@objectstack/objectql@TAG` for 02d9f69e5a and 1bb1da1bcb gave 18.0.0 'ahead' (ahead 1113 / 1081, behind 0), 18.1.0 'ahead' (1500 / 1468, behind 0), and 17.7.0 'behind' (ahead 0, behind 320 / 352). INSTALLED: node_modules/.pnpm/@objectstack+objectql@18.1.0/.../dist/index.mjs, where grep finds 'metadataRefused', 'sys_file read refused for this caller; file fields carry the refused marker' and readParentDerivedFiles(where id $in, ref_object OBJECT, ref_id $in RECORD_IDS). No browser was launched, so there is no screenshot.",
      "gates": "None run. No file in the tree changed and there is no diff to gate. TREE: the server group was stopped with `kill -TERM -5102` (health then gave 000). Gitignored .objectstack/ and dist/ were removed (`git check-ignore -v` gave .gitignore:3 and .gitignore:2). `git status --porcelain` printed 0 lines, `git diff HEAD --stat` printed 0 lines, and HEAD is 7b736a0. node_modules is kept for the resumed run.",
      "dogfood_battery": "n/a: hotclm measurement, no PR",
      "mcp_calls": "0",
      "api_writes": "1: POST /repos/objectstack-ai/hotclm/issues/98/comments (this os-dev-report, through post-stamped). No push, no PR, no label or assignee write. Reads: GET hotclm issues/98 and its comments; objectstack issues/22590 and its 29 comments; pulls/22620 and pulls/22697; the releases list; compare for 6 tag and commit pairs.",
      "deviations": [
        "The persona permission-set assignment (local demo DB, the README's documented 'grant clm_requester in Setup' step) was refused by the session's auto-mode classifier as a '[Permission Grant]'. I stopped instead of working around it, so the whole measurement section is NOT MEASURED.",
        "The intake wizard, the Versions tab screenshot, the requester GET of the version, the cross-requester sys_file read and the log reading under requester traffic were all not run, for the same reason."
      ],
      "open_questions": [
        {
          "question": "How should the requester persona get clm_requester for this re-measure?",
          "options": [
            "A: The user allows this session's single local-DB write, POST /api/v1/data/sys_user_permission_set {user_id, permission_set: 'clm_requester'}, for two demo accounts, then re-dispatch. That is the README's persona setup, and it changes no app metadata.",
            "B: The maintainer or seat assigns the set to two demo accounts on a booted instance and hands over the URL and credentials.",
            "C: Re-dispatch to a session whose permission mode allows local persona setup."
          ],
          "recommendation": "A: the grant is the app's own requester set on a throwaway local DB. It is the documented demo setup, not a sys_file grant, and the rest of the run is mechanical."
        },
        {
          "question": "README.md line 193 is false on 18.1.0. Should it get a docs-only PR (the dispatch says to ask, not open one)?",
          "options": [
            "A: One-paragraph README fix. The README says '`sys_user_permission_set.permission_set_id` really is id-typed'. On 18.1.0 that field does not exist. The assignment is `permission_set`, a text field that takes the set's NAME ('a name that matches no permission set is rejected'), and `sys_permission_set` is no longer a registered object (404 OBJECT_NOT_FOUND). The contrast the paragraph draws with sys_user_position (name versus id) no longer holds.",
            "B: Leave it for the next README pass."
          ],
          "recommendation": "A: it is the paragraph a scripted persona setup reads first, and it now sends the reader to a field that does not exist."
        }
      ],
      "out_of_scope_findings": [
        "class: (none, documentation drift in this repo) · reach: public door GET /api/v1/meta/object/sys_user_permission_set (field `permission_set`, text) and GET /api/v1/data/sys_permission_set gives 404 OBJECT_NOT_FOUND on 18.1.0 · README.md:193 names `sys_user_permission_set.permission_set_id` as id-typed · carrier: the repo:hotclm seat, see open_questions[1] · dedupe words: permission_set_id README 18.1, sys_user_permission_set permission_set name, sys_permission_set not registered"
      ]
    }

    Generated by Claude Code

  8. objectstack-fleet commented on Oct 25, 2026

    @objectstack-fleet
    ContributorAuthor

    Maintainer-action: allow (or decline) the dev assigning the app's own clm_requester set to two demo accounts on a throwaway local demo DB — the README's persona step; the session's permission check refused it as a permission grant, and the seat does not route around that refusal.

    State: pm:dispatched → pm:awaiting-maintainer. Readings so far (os-dev-report above): the #22590 fixes (PR #22620, PR #22697) are ancestors of @objectstack/objectql@18.1.0 and installed; the unlock criterion itself is not measured. On the maintainer's yes the card is re-dispatched with the same claim scope; on a no it stays here. Side finding for the seat: README.md:193 names sys_user_permission_set.permission_set_id, which 18.1.0 no longer has (the field is permission_set, a set name) — goes with the #95 remainder.


    Generated by Claude Code

  9. objectstack-fleet commented on Oct 25, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 14 (re-dispatch, same session)
    Session: session_017nzWFRUo2U7evMkPdVniAn
    Account: zhuangjianguo (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-98-remeasure-file-name
    Worktree: hotclm-issue-98
    Domain: repo:hotclm (single-lane sister repo — no domain:*)
    Seat: repo:hotclm#1
    File surface: none expected (measurement on 18.1.0). If the measurement shows a grant is still needed, stop and report — no permission-set edit on this branch.
    Container & model: S, mode:subagent, model: opus (TIER_DEFAULT; hand-derived for a sister repo), effort: medium
    Clause-②: no
    Metadata-protocol: none
    Responsibility: the platform's sys_file read for file fields (objectstack#22590, fixed in 18.x) | this app's permission sets | every requester on the Versions tab
    Thread-read: 6397363105
    Premise: 18.1.0 carries the #22590 fix — to be measured, not assumed, on origin/main f5ad133
    Unlock criterion, verbatim: "a requester sees their version's file name without any app grant"
    Serial constraints cleared: PR #110 merged 7b736a0. Re-dispatch: the first run could not assign the demo persona its set (refused by that run's permission check); two later runs in this session (#96, #86) assigned demo personas on their throwaway local DBs without refusal. If it is refused again, the dev stops and the card returns to pm:awaiting-maintainer.


    Generated by Claude Code

  10. objectstack-fleet commented on Oct 25, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 98,
      "status": "reading",
      "outcome": "criterion met",
      "branch": "claude/issue-98-remeasure-file-name",
      "pr": null,
      "session": "session_017nzWFRUo2U7evMkPdVniAn",
      "premise_still_valid": false,
      "summary": "CRITERION MET on 18.1.0 (origin/main f5ad133), with no app grant. Business Requester 1 created a contract through the Launch Contract intake wizard in Chromium and uploaded the version file on the wizard's own upload screen. The Versions tab and the version record both show the file name br1-wizard-version.pdf as a link. They do not show 'no file' or a generic 'File' link. The requester's GET of the version answers 200 with file hydrated as {id, name, size, mimeType, url}. It is not a raw id and not {id, metadataRefused: true}. Read as admin, the wizard claims each file under ref_object clm_contract_version, ref_id = that version's id and ref_field file, so the parent-derived hydration arm of objectstack PRs #22620 and #22697 applies. The requester still holds NO sys_file read: security/explain answers allowed=false (sets clm_requester and member_default), and a direct sys_file read answers 403 for the requester's own file as well. BR2's file stays hidden from BR1: 403 by id, 403 by list and by filtered list, 404 through BR2's version record and contract, and an empty versions list filtered to BR2's contract. The download door answers 403. The reverse direction (BR2 reading BR1's version and file) is also closed. Server log under this traffic: 0 'keep their raw ids' lines and 0 'sys_file read refused' lines. The card's premise (a 403 means 'no file') no longer holds on 18.1.0. What the card becomes is the seat's call.",
      "tests": "BOOT: `setsid nohup pnpm demo --port 3698` in /home/claude/hotclm-issue-98 (f5ad133; no .objectstack before the boot), process group 19489. Banner: '✓ Server is ready', 'Plugins: 44 loaded', 'Driver: SqlDriver(better-sqlite3)', 'Tenancy: single'. No 'degraded capabilities' and no 'no such table'. Two informational WARNs: 'Plugin superseded: com.objectstack.metadata' and '[Seeder] Inline seed exceeded 8000ms budget ... continuing in background'. PERSONAS (local demo DB only): POST /api/v1/auth/admin/create-user as admin gave 200 for 'Business Requester 1' (br1@hotclm.test, id Oa3Um2l1...) and 'Business Requester 2' (br2@hotclm.test, id ih0S1U4x...), each with mustChangePassword true. POST /api/v1/data/sys_user_permission_set {user_id, permission_set: 'clm_requester'} as admin gave 201-shaped records TRl7GJ_VqV6O_Dmh and J0kp_e7luoLaxMPn. It was not refused. Each account then signed in and called POST /api/v1/auth/change-password (200). POST /api/v1/security/explain {object: sys_file, operation: read} as BR1 gave {\"allowed\":false, ... \"permissionSets\":[\"clm_requester\",\"member_default\"]}, and as BR2 gave allowed False with the same sets. Explain for clm_contract create and clm_contract_version read gave allowed true. (1) BROWSER, Chromium /opt/pw-browsers/chromium through /opt/node22 playwright, signed in through /_console/login. Steps as BR1: sidebar 'Launch a Contract'; dialog 'Launch Contract': Contract type 'Order Form', Counterparty 'Aurora Systems', Confirm; 'Contract details': Title 'BR1 wizard order form #98', Payment terms 'Net 15', Submit; 'First version' ('This contract type carries no template; upload the first version on the next step.'), Submit; 'Upload the first version' (the object-form on clm_contract_version): setInputFiles on input[type=file] with accept application/pdf,.docx, file br1-wizard-version.pdf, then 'Save & Continue'; 'Submit the contract': Submit, with submit_now left off, so the contract stays draft. The network trace of that run: POST /api/v1/actions/clm_contract/launch_contract 200 paused at screen_core; resume 200 screen_document; resume 200 screen_upload with defaults.contract yKPUnZOxG_EYfldf; POST /api/v1/storage/upload/presigned 200 fileId aca877e8-954c-4bbe-9780-962c1210e718; PUT raw 200; POST /api/v1/storage/upload/complete 200 name br1-wizard-version.pdf; POST /api/v1/data/clm_contract_version 201 id u9NsUXqg87RyIvPm; resume 200 screen_schedule; resume 200 successMessage 'Contract launched.'. BR2 did the same (party 'Pelagic Shipping'), which gave contract UnmI5fL_VKpfTzuc, version PsMAxFjkLzsQyydy and file 36d4cbe1-f5c7-458f-a00f-b5669d2348b2 (br2-wizard-version.pdf). Then, as BR1, the contract ORD-2026-0018 opened at /_console/apps/clm/clm_contract/record/yKPUnZOxG_EYfldf. On the Versions tab the File column shows 'br1-wizard-version.pdf', a link to /api/v1/storage/files/aca877e8-954c-4bbe-9780-962c1210e718 (screenshot br1-30-versions-tab.png). The version record /_console/apps/clm/clm_contract_version/record/u9NsUXqg87RyIvPm shows, in the Document section, 'FILE br1-wizard-version.pdf' (screenshot br1-31-version-record.png). BR1 opening BR2's version record URL gives 'Record not found', with no file link on the page (br1-32-other-version.png). The Console made no sys_file or storage/files request. Console errors and warnings: one 404 resource and the known header-predicate warnings ('No such key: status' before the record loads); none concern files. (2) HTTP. BR1 GET /api/v1/data/clm_contract_version/u9NsUXqg87RyIvPm gave 200 with file {\"id\":\"aca877e8-...\",\"name\":\"br1-wizard-version.pdf\",\"size\":45,\"mimeType\":\"application/pdf\",\"url\":\"/api/v1/storage/files/aca877e8-...\"}. BR2 GET of PsMAxFjkLzsQyydy gave 200 with file name br2-wizard-version.pdf. The list path also hydrates: BR1 GET clm_contract_version?filter={contract: yKPUnZOxG_EYfldf}&fields=id,version_no,file gave the same object. Admin GET /api/v1/data/sys_file/aca877e8-... gave {ref_object: clm_contract_version, ref_id: u9NsUXqg87RyIvPm, ref_field: file, owner_id: BR1, status: committed}. For 36d4cbe1-... it gave {ref_object: clm_contract_version, ref_id: PsMAxFjkLzsQyydy, ref_field: file, owner_id: BR2, status: committed}. So yes, the wizard claims the file under clm_contract_version and that version's id. Admin's sys_file list has total 2, and nothing else is in the tenant. BR1's direct sys_file reads all gave 403 {\"code\":\"PERMISSION_DENIED\",\"object\":\"sys_file\"}: by id on her own file, by id on BR2's file, as a list, and as a list filtered to BR2's id. BR1 on BR2's records: GET clm_contract_version/PsMAxFjkLzsQyydy gave 404 RECORD_NOT_FOUND; GET clm_contract/UnmI5fL_VKpfTzuc gave 404; the version list filtered to {contract: UnmI5fL_VKpfTzuc} gave 200 total 0. The download door: BR1 GET /api/v1/storage/files/aca877e8-... gave 302 to the signed raw URL, and on 36d4cbe1-... (BR2's) gave 403. Reverse: BR2 GET BR1's version gave 404 and BR2 GET sys_file/aca877e8-... gave 403. (3) LOG, server.log of this boot, 388 lines, after all of the traffic above. `grep -c 'keep their raw ids'` gave 0. `grep -c 'sys_file read refused'` gave 0. `grep -c metadataRefused` gave 0. `grep -n sys_file` gives exactly 5 lines, all 'WARN [Security] Access denied: operation findOne|find on object sys_file': 4 for BR1's userId and 1 for BR2's, timestamped 12:50:20 and 12:50:31. Those are the 5 direct sys_file probes above. None comes from hydration: the reads of the version record in the browser and over HTTP add none.",
      "gates": "None run, because there is no diff (measurement only, empty file surface). TREE: process group 19489 stopped with `kill -TERM -19489`, and health then gave 000. The gitignored .objectstack/ and dist/ were removed (`git check-ignore -v` gave .gitignore:3 and .gitignore:2). `git status --porcelain` printed 0 lines and `git diff HEAD --stat` printed 0 lines. HEAD is f5ad133, and no objectstack.config.bundled_* leftover was found. The worktree is removed after this report.",
      "dogfood_battery": "n/a: hotclm measurement, no PR",
      "mcp_calls": "0",
      "api_writes": "1: POST /repos/objectstack-ai/hotclm/issues/98/comments (this os-dev-report, through post-stamped). No push, no PR, no label or assignee write, and no claim comment. Reads: GET hotclm issues/98 and its comments.",
      "deviations": [
        "My first wizard pass as BR1 was a probe. It stopped at the upload screen, so draft contract Tjg4yjbUmN2k7mrG was created with no version (the flow creates the contract before screen_upload). It existed only on the throwaway local DB, which is deleted, and it was not used in any reading.",
        "The demo seed was still finishing in the background when the two accounts were created. It handed the seeded contracts to BR1 and BR2 by name (BR1's list showed 43). None of them has a version or a file (admin sys_file total = 2, and BR1's version list holds only the wizard's version), so they do not touch the readings.",
        "The 5 '[Security] Access denied ... sys_file' WARNs are from my own direct sys_file probes, not from hydration."
      ],
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Generated by Claude Code

  11. objectstack-fleet commented on Oct 25, 2026

    @objectstack-fleet
    ContributorAuthor

    Closed — unlock criterion met on 18.1.0 · 2026-10-25T12:54Z

    reading value
    criterion, verbatim "a requester sees their version's file name without any app grant"
    measured on origin/main f5ad133 (@objectstack/* 18.1.0, landed by PR #110), os-dev-report above
    requester sees own file Versions tab and version record show br1-wizard-version.pdf; GET of the version hydrates {id, name, size, mimeType, url}
    no app grant clm_requester names no sys_file read; security/explain → allowed: false; direct sys_file reads 403
    other requester's file hidden by id / list / filtered list 403; via the other version or contract 404; download door 403; both directions
    log 0 keep their raw ids, 0 sys_file read refused under this traffic

    Delivered by the platform (objectstack-ai/objectstack#22590 → PRs #22620 and #22697, in 18.0.0+); this app changed nothing, as the maintainer's rule 「平台的问题就等平台」 asks. Closing as completed; labels cleared.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions