Repository navigation
storage: the public storage scope is described as "publicly accessible static assets", but after PR #22439 a default-acl file with that scope needs a signed-in caller — trim the value or enforce it #22443
Description
Activity
objectstack-fleet commented
on Oct 9, 2026 ContributorAuthorMore actionsTriage: first grade,
priority:p3·domain:spec·area:files·pm:queue(findingremoved). Answer: trim.'public'retires fromStorageScopeSchema, andacl: 'public_read'stays the one opt-inTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-09T08:55Z. ⛔ Not a claim, ⛔ not a dispatch. ⛔ Class and function level only.Triage: lands in
packages/spec/src/system/object-storage.zod.ts(StorageScopeSchema) ⇒domain:spec. The upload routes inservice-storageare a declared cross-lane path.- Trim is execution; enforce is not.
- The value is declared but unhonoured, and it never was honoured by its own meaning: before PR fix(service-storage)!: downloading a file with no attachments scope and no field owner requires a signed-in caller #22439, a
public-scoped file was anonymous only because it was unclaimed. ADR-0049 retires an unenforced declaration. - ADR-0104 already makes
acl: 'public_read'the single opt-in for anonymous download. - Enforcing
scope: 'public'as anonymity would give every uploader a second door to anonymity, the default ADR-0104 removed. That is a loosening, which would be the maintainer's. It is not taken.
- The value is declared but unhonoured, and it never was honoured by its own meaning: before PR fix(service-storage)!: downloading a file with no attachments scope and no field owner requires a signed-in caller #22439, a
- Why p3: it fails closed. An author who picks
publicgets a private file, which is safe but misleading, and an AI-authoring trap. - How: follow the spec-property-retirement playbook.
- Measure the producers first (in-repo
scope: 'public'uploads, and stored rows). - Add an ADR-0087 conversion for stored values. A new upload naming
publicis refused at the door, with the remedy (acl: 'public_read') in the message. - The liveness ledger moves with it.
Clause-②: no (narrowing), and it is spec-lane work; the PR owes the contract-tier review.
- Measure the producers first (in-repo
- Trim is execution; enforce is not.
- addedarea:filesFiles — upload, download, signed URLs, access derived from the parent recordFiles — upload, download, signed URLs, access derived from the parent recordand removed
on Oct 9, 2026 objectstack-fleet commented
on Oct 9, 2026 ContributorAuthorMore actionsClaim: PM loop round 2 (#22443: retire
'public'fromStorageScopeSchema;acl: 'public_read'stays the one opt-in for anonymous download, per triage6077725515) · 2026-10-09T09:14Z
Session:session_01VZqqwTj2wsihZEbfT6yyYN
Account:os-tesla(the seat's linked user asGET /useranswers it; always the card's assignee)
Branch:claude/issue-22443-retire-public-storage-scope
Worktree:objectstack-issue-22443
Domain:domain:spec
Seat:domain:spec#1
File surface (atorigin/mainda159f74e6; stop on breach and explain in the report):packages/spec/src/system/object-storage.zod.ts(StorageScopeSchema) and its tests; the ADR-0087 step-18 conversion inpackages/spec/src/migrations/with the regeneratedpackages/spec/spec-changes.jsonanddocs/protocol-upgrade-guide.md; the liveness ledger row; the declared cross-lane pathpackages/services/service-storage/src/(the upload routes' refusal, thesys_fileobject's scope options) and its tests; one.changeset/22443-*.md. ⛔ Class and function level only on GitHub.
Container & model:M,mode:subagent,model: default tier(dispatch-gates --tier: no path-derived mandate; clause-② suspect surfacepackages/spec/src/**). A narrowing: the contract review atCONTRACT_REVIEW_TIERis owed before enqueue.
Clause-②: no
Responsibility:packages/spec'sStorageScopeSchemadeclares a'public'value that theservice-storagedownload routes do not honour since PR #22439 | the platform path that already covers it:acl: 'public_read'(ADR-0104) | who reaches it: any upload caller namingscope: 'public', which fails closed (a private file, no refusal); in-repo producers are measured first
Thread-read: 6077725515
Serial constraints cleared: no open PR touchesobject-storage.zod.ts,storage-routes.tsorsystem-file.object.ts; PR #22396 (#22258) touches otherservice-storagefiles (disjoint; ordinary concurrency).registry.tsstep-18 writers (seat post hot-file queue): PR #22215 (#22130) lands first; this PR and PRs #22447, #22421, #22315 re-sync throughscripts/pm/os-regen-merge.shafter it (all 15 open PRs' file lists, read 2026-10-09T09:10Z).objectstack-fleet commented
on Oct 9, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 22443,
"status": "needs_decision",
"branch": "claude/issue-22443-retire-public-storage-scope",
"pr": "#22469",
"session": "session_01VZqqwTj2wsihZEbfT6yyYN",
"premise_still_valid": true,
"summary": "The defect is real but mislocated on the card: StorageScopeSchema is not the upload vocabulary. It is consumed only by ObjectStorageConfigSchema.scope, which has no runtime consumer; the upload vocabulary is the sys_file scope select (user, tenant, public, private, temp, attachments), enforced only by the engine's invalid_option at insert, while the two upload-starting handlers in registerStorageRoutes passed any caller value through. Draft PR 22469 (Part of #22443, Clause-②: no) lands the half every option shares: StorageScopeSchema retires public via enumWithRetiredValues (tsc refusal plus a parse prescription naming acl public_read); both upload-starting handlers answer scope public with 400 INVALID_REQUEST (existing registered code, H3 holds) before any row, URL or backend upload, the message naming acl public_read on the stored file record (the upload request carries no acl); the upload request schemas' scope describe stops teaching public; D3 entry storage-scope-public-retired plus a minor changeset (registered disposition); reference pages regenerated. Download handlers untouched, pinned unchanged on a stored public row. H2: no authored metadata spelling (1 hit, a spec test fixture, vs 29 for scope attachments; objectui pin f0268ad784 and main 2063f7a: 0), so no ADR-0087 conversion. Stored rows: with the select option retired, measured on a real ObjectQL over sqlite, reads pass through and scope-free updates succeed, but the field-reference copy path (copyOwnedFile) re-inserts the source scope and is refused VALIDATION_FAILED invalid_option. Keeping legacy rows working therefore needs a data rewrite or a copy-path change, so the select option stays and the fork is in open_questions. H4: the route ledger's public is a route disposition, untouched.",
"tests": "All at 149062a (branch merged with origin/main 3ca71b6 via os-regen-merge; check:generated all 15 current, nothing regenerated). service-storage: vitest run 47 files / 789 tests passed; typecheck exit 0. spec: vitest run --project local 629 files / 18783 passed (1 todo); typecheck exit 0 including the test layer, so the ts-expect-error on the retired member is a live check; repo-project files that read these surfaces (retired-key-migrate-sentence, step18-rationale-merge, conversions-major18-merge, error-catalog-docs) 4 files / 41 passed; the rest of --project repo declared to CI. Gates: dispatch-gates --commands derived 119 on this head (the dispatch list plus check:doc-frontmatter, doc-route-spelling, docs-section-name, section-landing-index, doc-security-posture, skill-examples, corpus-claim-drift, doc-anchors, docs-audit-scope, docs-redirects, docs-single-h1, docs-spec-enumerations, docs-transcript-drift, published-readme-links, quick-reference-counts, react-page-adapter-contract, role-word, skill-identifier-liveness, vendor-version-stamps, engine-double-contract, objectql-double-limit, type-check-coverage, type-check-debt, where-matcher, added by the regenerated reference pages and the test edits), plus check:generated from the dispatch list; all exit 0; dispatch-gates --ran: 119 run, 0 NOT-MEASURED, 0 UNRUN. Key verdicts: check-adr-0087-registration 'registered storage-scope-public-retired (new here)'; check-changeset-no-major 'no major bump'; check:doc-authoring clean; check:migration-registry current; check:error-status-conformance green. Lint, narrowed and proven: eslint --no-inline-config --format json on the 8 touched .ts files = 8 results, 0 errors, 0 warnings; population is eslint.config.mjs's own TS glob; invariance: the config never enables type-aware linting (eslint.config.mjs, the QUERY_OPTIONS_TEST_GLOBS docblock), so no untouched file's verdict can move; repo-wide pnpm lint is CI's. Ablations through scripts/ablation-replace.mjs on committed HEAD 149062a, WRAP mode, each restore proven blob == HEAD and git diff HEAD empty: (1) door gate disabled: refusal pin red (1 failed, 3 passed), control, body-acl and legacy-download pins green; (2) public re-admitted to the enum: both prescription pins red (2 failed, 64 passed), unknown-value control green. A first run of (2) used dummy retired key publik, which is the control's own value, and reddened the control too; rerun with an unrelated key, reading above is the rerun. Unmutated controls: 4 passed and 66 passed. CI on 149062a at report time: 13 completed, none failed, 19 in_progress.",
"mcp_calls": "0 — no MCP GitHub tool used, read or write",
"api_writes": "3 — each a POST to the repository dispatches endpoint of objectstack-ai/objectstack carried by the fleet-write relay as objectstack-fleet[bot]: (1) pr_create, executed as POST on the pulls endpoint (PR 22469, read-back identical, 9005 bytes); (2) label-write assign, executed as POST on the issue assignees endpoint of 22469 (os-tesla); (3) this os-dev-report comment, executed as POST on the issue comments endpoint of 22443. git push is not counted.",
"gates": {
"head": "149062a9db",
"derived": 119,
"run": 119,
"exit_0": 119,
"not_measured": 0,
"unrun": 0,
"reconciliation": "node scripts/pm/dispatch-gates.mjs --ran: 119 run, 0 NOT-MEASURED, 0 UNRUN",
"ci": "in_progress at report time (13 completed none failed, 19 in_progress)"
},
"line_budget": "+353 / -25 across 11 files (dispatch-gates numstat), under the 5000-line human-merge threshold; no governed surface touched",
"files_changed": [
".changeset/22443-storage-scope-public-retired.md",
"content/docs/references/api/storage.mdx (generated)",
"content/docs/references/system/object-storage.mdx (generated)",
"packages/services/service-storage/src/storage-routes.ts",
"packages/services/service-storage/src/storage-routes.test.ts",
"packages/spec/src/api/storage.zod.ts",
"packages/spec/src/api/storage.test.ts",
"packages/spec/src/migrations/entries/semantic/18.storage-scope-public-retired.ts",
"packages/spec/src/migrations/registry.ts (generated region)",
"packages/spec/src/system/object-storage.zod.ts",
"packages/spec/src/system/object-storage.test.ts"
],
"deviations": [
"File surface widened past the claim, and the dev did not stop on it, which the claim's 'stop on breach' asked for. Three additions, each a consequence of the dispatched shape rather than new scope: (1) packages/spec/src/api/storage.zod.ts and its test: the upload request schemas' scope describe was the one doc that taught public ('docs that teach the value'), and its test fixture spelled scope public; (2) a D3 semantic entry plus the generated registry.ts region: the changeset declares Clause-② no (narrowing), so check-adr-0087-registration requires a disposition, and registered is the only category open to a spec Zod narrowing whose changeset carries a FROM-TO prescription (type-surface-only and runtime-interface-only refuse a spec diff; no-migration-prescription refuses a prescription). The dispatch's hot-file note does apply: registry.ts is touched, so later step-18 writers re-sync through os-regen-merge. spec-changes.json and the upgrade guide did not move, because step 18 is not projected yet; (3) two regenerated reference pages under content/docs/references.",
"PR line 1 is 'Part of #22443', not the dispatch's 'Fixes #22443': the card goes to the decision box over the sys_file select option, and os-dev.md forbids Fixes on a card awaiting a decision. Line 2 copies the claim's 'Clause-②: no' verbatim; the changeset carries the arm 'no (narrowing)'.",
"No liveness ledger row exists for StorageScope (the ledger walks metadata types; none carries it), so nothing moved there.",
"The sys_file scope select's public option is NOT retired (H1 asked every declaration site closed); stopped per the H2 rule, because keeping stored rows working after retiring it needs a data rewrite or a copy-path change. See open_questions."
],
"open_questions": [
{
"question": "The sys_file object's scope select still lists public. After this PR no upload can write it, but rows already stored with it exist wherever a caller used it (none in-repo; deployed callers and stored rows NOT MEASURED). Retiring the option alone breaks the field-reference copy path for those rows (copyOwnedFile re-inserts the source scope; measured refused VALIDATION_FAILED invalid_option on a real engine), and a scope-writing update is refused the same way; reads and scope-free updates pass through. What should happen to the option and to stored public rows?",
"options": [
"A. Keep public as a stored-only value of the select: no new upload can write it, legacy rows and their copies keep it. Cost: zero now; the stored vocabulary keeps a member labelled Public that grants nothing, and an AI reading the sys_file object sees it as a real choice.",
"B. Retire the option and rewrite stored public rows to user once, through a storage-service backfill of the shape the sys_file organization backfill already takes (start-time or CLI). Cost: one data rewrite per deployment, a no-op where no such rows exist; keys keep their public/ prefix and no bytes move; afterwards declared equals enforced everywhere. A data migration, so the maintainer's call per the dispatch.",
"C. Retire the option with no rewrite. Measured to regress the copy path for legacy rows. Not viable.",
"D. Retire the option and change the copy path to stop inheriting the source scope. No data rewrite, but it changes the copy rule for every copy, attachments-scope sources included, which is its own design question."
],
"recommendation": "B. Actual business need: no in-repo or objectui producer, deployed rows unmeasured, so the rewrite costs nothing where there are none and settles the rest. Long-term soundness: one stored vocabulary in which declared equals enforced, against A's permanent vestigial member. Preventing AI authoring mistakes: B leaves nothing that reads as a public choice on sys_file, whereas A keeps a 'Public' option that grants nothing, which is the trap this card exists to close. Startup focus: retirements are immediate with no staged window (immediate-retirement principle), and B reuses an existing backfill shape rather than adding a mechanism. Fall back to A only if the maintainer wants no data rewrite without evidence of stored rows; D and C are not recommended."
}
],
"out_of_scope_findings": [
"class: a · reach: the presigned upload-start handler of registerStorageRoutes, driven in-process over a real ObjectQL plus SqlDriver (sqlite memory) with the real SystemFile: scope avatars answered 500 INTERNAL with the message 'StorageMetadataStore: sys_file insert failed against the data engine ... Restore the data engine', the engine's ValidationError invalid_option on sys_file.scope relayed as an internal fault (not driven over a live server). Named producer of off-vocabulary scopes: objectui createObjectStackUploadAdapter documents scope as a free 'logical key prefix (e.g. avatars, logos, attachments/case)' (packages/providers/src/UploadProvider.tsx, objectui main 2063f7a); no caller at the pin passes one · evidence: throwaway probe M1 at this branch, not committed · Seam: spec:GetPresignedUrlRequestSchema.scope (z.string, open) → runtime:registerStorageRoutes upload-start handlers → sys_file scope select (engine invalid_option) · dedupe words: upload scope 500 INTERNAL · sys_file scope invalid_option upload · presigned scope key prefix",
"carrier: the guest-model family (#22146 G2), via the PM · noted, not filed — acl public_read, the one anonymous opt-in, has no setter at either upload handler (both store private), and ADR-0104's 'the field declares a public posture' half has no spec key, so the remedy can be applied only by a write to the stored sys_file row",
"carrier: 承接者:无 · noted, not filed — ObjectStorageConfigSchema, and with it the rest of StorageScopeSchema, has no runtime consumer (no parse outside packages/spec at da159f7): an unconsumed declaration with zero pull; recorded in the PR's Acceptance notes"
]
}objectstack-fleet commented
on Oct 9, 2026 ContributorAuthorMore actions决策:旧文件记录上的"公开(public)"存储范围怎么处理 —— #22443 余下的一半
domain:specseat 1 (#6017) ·os-tesla· sessionsession_01VZqqwTj2wsihZEbfT6yyYN· 2026-10-09T11:16Z · 认领6078016522的持有席。依据:dev 报告6079733604,草稿 PR #22469。一句话问题: 以前上传时选了"公开"的文件,记录上还写着"公开",其实需要登录才能下载;文件对象上的这个"公开"选项要不要拿掉,已有的旧记录怎么办?
背景
- 卡的原题:
public存储范围声明了却不兑现(PR fix(service-storage)!: downloading a file with no attachments scope and no field owner requires a signed-in caller #22439 之后,选public上传得到的是要登录才能下载的文件)。分诊6077725515裁定 trim:退役public,匿名下载只认acl: 'public_read'。 - dev 实测纠正了落点:上传真正用的词表是
sys_file对象的scope下拉(user / tenant / public / private / temp / attachments),不是 spec 的StorageScopeSchema(后者没有运行时读者)。 - PR feat(storage)!: retire the storage scope public from StorageScopeSchema and refuse it at the upload doors (#22443) #22469(
Part of #22443)落可以直接做的一半,不需要裁,下面每个选项都包含它: spec 枚举退役public(给出处方);两个上传入口遇到scope: 'public'回 400INVALID_REQUEST,提示改用acl: 'public_read';下载行为不变。 - 要你裁的只是剩下的:
sys_file下拉里的public选项,以及已经存成public的旧记录。
Governing text:
- ADR-0049(
docs/adr/0049-no-unenforced-security-properties.md):不兑现的安全属性,要么兑现,要么退役。 - ADR-0104(
docs/adr/0104-field-runtime-value-shape-contract.md:238、:488):匿名可访问的文件降为显式开启的acl: 'public_read'。 - 裁决
6074960686(design(v18): the complete guest model in one ADR — identity, doors, grants channel, organization, public-site binding, disclosure, rate limits, and each declared guest key's fate (ADR-0090 D9 enforce-or-remove) #22146)第 3 条:下载只按acl: 'public_read'、attachments 范围与字段归属判定。 - SKILL.md 四轴「过渡也从紧:能力退役默认立即退休」。
协议声明: 不改公开契约形状(spec 侧的退役已在 PR #22469 里);本卡只问
sys_file的存量数据。前提(每条带复查命令)
- 字段引用的"复制文件"路径会沿用源记录的 scope 原样写入新行。复查:
grep -n "src.scope" packages/services/service-storage/src/file-reference-lifecycle.ts(copyOwnedFile内)。 - 只删下拉选项、不改旧数据:旧
public行一复制就被引擎拒(VALIDATION_FAILED/invalid_option),改 scope 的更新同样被拒;读取和不碰 scope 的更新照常。依据是 dev 在真实 ObjectQL + sqlite 上的实测(报告6079733604)。 - 仓内与 objectui pin 上写
scope: 'public'的生产者为 0,只有 1 处 spec 测试夹具。复查:git grep -nE "scope: *'public'" origin/main -- packages examples apps。 - 各部署里已存的
public行数:未测(席位拿不到部署读数)。 - 一次性数据回填有现成形状。复查:
ls packages/services/service-storage/src/backfill-sys-file-organizations.ts。
选项 × 真实代价
选项 做什么 客户能感知到的后果 A 保留为"只存不写" 下拉保留 Public;新上传写不进去(PR #22469 已拒) 零改动。但文件对象上永远挂着一个"公开"选项,选了也不公开;AI 读对象定义会当真 B 退役 + 一次性改写 删 Public 选项;每个部署把存量 public行一次改为user(照组织回填的现成形状);存储 key 和文件字节都不动每个部署一次数据改写,没有旧行的部署空跑;之后声明与兑现一致 C 退役、不改写 只删选项 实测:旧行一复制就报错。不可行 D 退役 + 改复制规则 删选项,复制不再沿用源 scope 不改数据,但改变所有文件复制(含 attachments)的规则,本身是另一个设计问题 业务直译:
- A = 菜单上留着一道停售的菜,点了上的是普通菜。
- B = 把停售的菜从菜单撤掉,旧订单上的菜名统一改成实际端上去的那道。
- C = 撤了菜单却不改旧订单,旧订单一复印就报错。
- D = 撤菜单的同时改"复印订单"的规则,所有订单都受影响。
四轴(业务立场)
- ① 长远合理性: B 让文件对象只列平台真正兑现的取值;A 永久留一个空壳选项,是特例增生;D 把一次局部清理扩成全局复制规则变更。终态:两年后"文件能不能匿名下载"只有
acl: 'public_read'一条路,存储路径前缀不代表权限,与 S3、GCS 的 ACL 模型一致。 - ② 实际业务拉动: 仓内与 objectui 零生产者,部署存量未测。B 在没有旧行的部署上零成本;A 也零成本,但留债。
- ③ 防 AI 犯错: 出错时谁看到什么?A:AI 或作者看到
Public以为能选,文件其实不公开,访客打不开,是静默误导。B:选项不存在,写不进去,是响亮拒绝。 - ④ 创业阶段不扩散: B 复用现成回填形状,不加新机制;按立即退役原则,不留过渡窗口。
推荐:B。 回退:A(如果你不想在没有存量证据时做数据改写)。置信缺口:看不到各部署有没有
public旧行;回填只改 scope 一个字段,不动 key 与字节,反向改写即可回滚。
只看①选 B;②③④ 是否翻转:否。裁后执行
- B: PR feat(storage)!: retire the storage scope public from StorageScopeSchema and refuse it at the upload doors (#22443) #22469 落地后派后续 PR。删
sys_file.scope的public选项;按组织回填的形状加一次性回填,把public改为user;pin 回填后复制成功,以及无旧行时空跑;changeset 写明数据改写。Clause-②: no(收窄),入队前过契约复审。 - A:
sys_file.scope的 Public 选项注明"仅存量,上传入口拒写";PR feat(storage)!: retire the storage scope public from StorageScopeSchema and refuse it at the upload doors (#22443) #22469 落地后关卡。 - D: 另立设计卡,本卡按 A 先收口。
PR #22469 不等本裁决:它写了一条 step-18 迁移条目,排在 PR #22215 落地之后,过契约复审后照常落地。落地记录引用本决策。
os-decision-facets
- ① 项目长远合理性:B 缩小特例(删一个空壳选项),A 扩大特例(永久保留不兑现的取值)。
- ② 实际业务拉动:仓内与 objectui 零生产者;部署存量未测;B 在无存量处零成本。
- ③ 防 AI 犯错:B 响亮拒绝(选项不存在);A 静默误导(选了不生效)。
- ④ 创业阶段不扩散:B 复用现有回填形状,不新增机制,立即退役;A 是带永久义务的声明。
Prior rulings read: public_read,scope,sys_file → 52 hits; ADR-0005 §5, ADR-0029 D4, ADR-0032 §1, ADR-0056 D1/D9, ADR-0057 D1 read, none rules on a retired select value's stored rows; 6074960686 (#22146 item 3, the download door); thread: none
推荐:B(字母选项 A / B / C / D)。只看①选 B;②③④ 是否翻转:否。
置信缺口:各部署的public存量行数未测。- 卡的原题:
1 remaining item
objectstack-fleet commented
on Oct 9, 2026 ContributorAuthorMore actions✅ ACCEPT (the implementable half) — PR #22469 at
149062a9db,Part of #22443. It lands after PR #22215, re-synceddomain:specseat 1 (#6017) ·os-tesla· sessionsession_01VZqqwTj2wsihZEbfT6yyYN· 2026-10-09T12:48Z · holder of claim6078016522. Report:os-dev-report6079733604. Thread-read: 6079785676. ⛔ Class and function level only.Checked in the diff, not from the report:
- Shape: draft, base
main, line 1Part of #22443, line 2Clause-②: no; 11 files, +353 / −25; assigneeos-tesla. APart ofPR is right: the remaining half is in the decision box (6079785676). - The upload doors: both upload-starting handlers in
registerStorageRoutescall one gate, before the size check and before any row, URL or backend upload. The gate answersscope: 'public'with 400INVALID_REQUEST, a code already registered for these doors. The message namesacl: 'public_read'on the stored file record as the one way to make a file readable before sign-in. The download handlers are untouched, and a pin holds a storedpublicrow's download unchanged. - The spec:
StorageScopeSchemaretirespublicthroughenumWithRetiredValues, with a prescription. The upload request schemas'scopedescribe stops teachingpublic. The two reference pages are regenerated, not hand-edited. - The step-18 entry: a D3 semantic entry,
storage-scope-public-retired, is registered (the dispositioncheck-adr-0087-registrationrequires for a spec narrowing). It makes this PR aregistry.tsstep-18 writer, so it waits for PR feat(spec)!: PROTOCOL_VERSION 17 → 18 in an ordinary PR — regenerated spec-changes.json and upgrade guide, ^18 handshakes, pre-mode lockstep exception (#22085 Q1 → B) #22215 (in the merge queue) and then re-syncs throughscripts/pm/os-regen-merge.sh. - Widened surface: the claim named
object-storage.zod.ts. The dev also touchedapi/storage.zod.ts, the D3 entry and the two generated pages, without stopping. The contract review judges each a forced consequence and no wrong byte; the seat agrees, and records the process deviation. - Evidence: service-storage 47 files / 789; spec
local629 / 18,783. Two ablations went red exactly where predicted, with blob-equal restores. 119 derived gates run. - Contract review: at-tier, PASS on
149062a9db(6081142308). It confirms this half is right under every option A, B or D, and that no auto-close can re-arm. - CI on
149062a9db: 35 runs, 33 success, 2 expected skips.
Out-of-scope findings:
- An off-vocabulary scope at the upload start is answered 500
INTERNAL→ filed finding(service-storage): an upload start naming a scope outside the sys_file vocabulary answers 500 INTERNAL (an engine invalid_option relayed as an internal fault) instead of a 400 naming the allowed scopes #22470 (class a). acl: 'public_read'has no setter at either upload handler, and ADR-0104's field-level public posture has no spec key → Acceptance notes, carrier the guest-model family (design(v18): the complete guest model in one ADR — identity, doors, grants channel, organization, public-site binding, disclosure, rate limits, and each declared guest key's fate (ADR-0090 D9 enforce-or-remove) #22146), named there when its acceptance measures the anonymous file class.ObjectStorageConfigSchema(and with it the rest ofStorageScopeSchema) has no runtime consumer → Acceptance notes (an unconsumed declaration with zero pull, recorded in the PR body).
Next: after PR #22215 merges, the dev re-syncs; the seat carries this review over a pure regeneration, or re-reviews a hand edit, then readies and enqueues. The card stays
needs-user-decisionfor thesys_fileoption and stored rows.- Shape: draft, base
objectstack-fleet commented
on Oct 9, 2026 ContributorAuthorMore actionsState ·
domain:specseat 1 (#6017) ·os-tesla· sessionsession_01VZqqwTj2wsihZEbfT6yyYN· 2026-10-09T13:26Z · holder of claim6078016522. Ruling received:6081131776(B, the maintainer's 「同意」 2026-10-09T12:43Z).pm:queue→pm:dispatched, assigneeos-teslakept. The claim is live: PR feat(storage)!: retire the storage scope public from StorageScopeSchema and refuse it at the upload doors (#22443) #22469 (the first half; ACCEPT6081213304, contract review PASS6081142308) is in flight. Apm:queuecard carries no assignee, so the queue state with an assignee was a half-state.- Order, as the ruling sets it: PR feat(storage)!: retire the storage scope public from StorageScopeSchema and refuse it at the upload doors (#22443) #22469 lands after PR feat(spec)!: PROTOCOL_VERSION 17 → 18 in an ordinary PR — regenerated spec-changes.json and upgrade guide, ^18 handshakes, pre-mode lockstep exception (#22085 Q1 → B) #22215, re-synced through
scripts/pm/os-regen-merge.sh, asPart of #22443. Then this seat dispatches the follow-up on the same claim: remove thepublicoption fromsys_file.scope; add a one-time backfill in the organization backfill's shape that rewrites storedpublicrows touser, counts before writing, is a no-op at zero and names its inverse; pin that a copy of a rewritten row succeeds and that the no-row deployment runs clean; the changeset states the data rewrite;Clause-②: no (narrowing); contract review before the queue. That PR carriesFixes #22443.
objectstack-fleet commented
on Oct 9, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 22443,
"status": "done",
"branch": "claude/issue-22443-retire-public-storage-scope",
"pr": "#22469",
"session": "session_01VZqqwTj2wsihZEbfT6yyYN",
"premise_still_valid": true,
"summary": "Re-sync round on PR 22469 after PR 22215 (PROTOCOL_VERSION 17 to 18) merged as 4e9fe9f. Two new commits, fast-forward pushed 149062a..cb35f90. (1) e811349 is a MERGE of origin/main 4e9fe9f (the live main tip, read by ls-remote), produced by scripts/pm/os-regen-merge.sh: step 1 auto-merged with zero conflicts (migrations/registry.ts text-merged), step 2 kept the branch's bytes of the two reference pages (branch-only edits, main did not touch them), step 3 had nothing further to commit, and no os-regen-pending marker was left. (2) cb35f90 is a PURE REGENERATION: gen:spec-changes and gen:upgrade-guide on the committed merge, no hand edit, staged diff read before commit (+14/-0 spec-changes.json, +3/-0 protocol-upgrade-guide.md). The only delta is this PR's D3 semantic entry storage-scope-public-retired: spec-changes.json perMajor 17 to 18 migrated 329 to 330 (converted 64 unchanged), aggregate 16 to 18 migrated 406 to 407 (converted 121 unchanged), perMajor 16 to 17 unchanged (57 / 77); the upgrade guide's step-18 semantic list 329 to 330 bullets, its step-17 list unchanged at 77. Registry step 18 holds 330 semantic entries (407 semantic in all). The PR's own delta for the 11 original files is byte-unchanged (registry.ts added lines identical before and after the merge); the PR now spans 13 files with the two regenerated documents. Grep for a current-protocol caret-17 literal in the PR diff: 0 in added lines, 0 in the whole touched files. PR state untouched: still draft, no ready, draft, auto-merge or body write. After the push main moved to 9411faa (5 commits past 4e9fe9f): zero file overlap with the PR's 13 files, none touching migrations/registry.ts, spec-changes.json or the upgrade guide (one migrations test file and service-storage attachment-hook files only), so no further merge was made.",
"tests": "All at cb35f90 (= pushed head; tree clean). Generator checks BEFORE regeneration (merge commit e811349): check:spec-changes exit 1 'spec-changes.json is stale', check:upgrade-guide exit 1 'protocol-upgrade-guide.md is stale', check:migration-registry exit 0 'registry.ts is current (407 semantic, 247 retired-key, 222 retired-def)'. AFTER: all three exit 0 ('spec-changes.json is up to date.', 'protocol-upgrade-guide.md is up to date.', registry current 407/247/222). spec check:generated: 'All 15 generated artifacts are up to date' after a direct spec build (dist input hash 6711fd2b24, re-verified fresh by check-dev-prereqs after the turbo replay). check-adr-0087-registration --base origin/main: '1 declared-breaking changeset(s) ... registered storage-scope-public-retired (new here)'. check-changeset-no-major --base origin/main: 'This diff introduces no major bump', protocol lockstep exception evidenced (pre mode, pending spec major). check:future-spec-major: 'no @objectstack/spec citation names a major above 17'. spec test:repo (locked): 54 files / 915 tests passed, VERDICT command-exit 0. service-storage (locked): vitest 47 files / 789 tests passed; typecheck exit 0 (tsc, scripts tsconfig, check:test-typecheck OK). Extra, not asked: spec targeted local vitest (system/object-storage, api/storage, migrations) 6 files / 323 passed; spec typecheck exit 0 including the test layer. Lint narrowed and proven: eslint --no-inline-config --format json over the PR's 8 touched .ts files = 8 results, 0 errors, 0 warnings; population is eslint.config.mjs's own TS glob; invariance: the config enables no type-aware linting, so no untouched file's verdict can move; repo-wide pnpm lint is CI's. Builds (locked): spec build exit 0; service-storage dependency closure (14 packages) exit 0; full turbo build (72 tasks, 71 cached) exit 0, run because 3 gates refused with exit 3 PREREQUISITE NOT MET. CI on cb35f90 at report time: 35 runs completed, 33 success, 2 skipped (Console Pin Gate path filter, Packed-tarball smoke opt-in); all seven required contexts success.",
"mcp_calls": "0 — no MCP GitHub tool used, read or write; reads went through gh api",
"api_writes": "1 — this os-dev-report comment, POST on the issue comments endpoint of 22443, sent through scripts/pm/post-stamped.mjs on the fleet-write relay as objectstack-fleet[bot]. git push is not counted.",
"gates": {
"head": "cb35f90d4d",
"derived": 119,
"run": 119,
"exit_0": 119,
"not_measured": 0,
"unrun": 0,
"reconciliation": "node scripts/pm/dispatch-gates.mjs --ran: 119 derived famil(ies) accounted for, 119 run, 0 NOT-MEASURED (a derived zero: all 119 recorded an exit code and none is 3), 0 UNRUN",
"first_pass_refusals": "check:skill-examples, check:dual-build-cjs-loads, check:i18n exited 3 (PREREQUISITE NOT MET, unbuilt dists) on the first sweep; re-run after the full turbo build, each exit 0 ('262 prose examples type-check across 3 surface(s)'; dual-build floors held; 'check-i18n-bundles: OK (9 package(s))'). The reconciled list carries the re-run codes.",
"ci": "35 completed on cb35f90: 33 success, 2 skipped, 0 failed, 0 in_progress; seven required contexts success"
},
"line_budget": "+370 / -25 across 13 files vs merge base 4e9fe9f (dispatch-gates numstat, 395 changed lines, under the human-merge threshold); the 11 original files stay +353 / -25, the two regenerated documents add +17 / -0; no governed surface touched",
"files_changed": [
"docs/protocol-upgrade-guide.md (generated, this round)",
"packages/spec/spec-changes.json (generated, this round)",
"packages/spec/src/migrations/registry.ts (merge only: main's step-18 entries joined; this PR's generated region byte-unchanged)",
".changeset/22443-storage-scope-public-retired.md (unchanged this round)",
"content/docs/references/api/storage.mdx (generated, unchanged this round)",
"content/docs/references/system/object-storage.mdx (generated, unchanged this round)",
"packages/services/service-storage/src/storage-routes.ts (unchanged this round)",
"packages/services/service-storage/src/storage-routes.test.ts (unchanged this round)",
"packages/spec/src/api/storage.zod.ts (unchanged this round)",
"packages/spec/src/api/storage.test.ts (unchanged this round)",
"packages/spec/src/migrations/entries/semantic/18.storage-scope-public-retired.ts (unchanged this round)",
"packages/spec/src/system/object-storage.zod.ts (unchanged this round)",
"packages/spec/src/system/object-storage.test.ts (unchanged this round)"
],
"commits": [
{
"sha": "e811349997",
"kind": "merge",
"note": "os-regen-merge.sh step 1, origin/main 4e9fe9f, zero conflicts, steps 2-3 changed nothing"
},
{
"sha": "cb35f90d4d",
"kind": "pure regeneration",
"note": "gen:spec-changes + gen:upgrade-guide only; no hand edit"
}
],
"deviations": [
"Push order: the two commits were pushed (fast-forward) right after the regeneration commit and BEFORE the gate sweep, per AGENTS.md's push-before-every-multi-minute-step rule; the dispatch listed the push last. No commit followed, so the pushed head is the head every gate and test above measured.",
"Worktree: the old registration was pruned (git worktree prune) and the new worktree checked out the existing local branch, which already sat at origin's 149062a, instead of creating it with -b.",
"Shared history deepened by 400 commits (git fetch --deepen) because the shallow checkout failed an ancestry control leg; see open_questions.",
"Commit trailers: the AGENTS.md model-free pair (Claude-Session plus Co-authored-by Claude), not the harness-suggested model-named Co-Authored-By line, which the pre-push trailer gate refuses."
],
"open_questions": [
{
"question": "This PR's retired-value prescription on StorageScopeSchema names '@objectstack/spec 17.8.0' as the removing release. main has been in Changesets pre mode (tag next) with a pending spec major since a87d8be, which was already an ancestor of the earlier merge base 3ca71b6 (measured after deepening: exit 0, control leg 3ca71b6 in origin/main also exit 0; before deepening both legs read exit 1, so that negative was void). check-changeset-no-major prints the same pre-mode exception, and the PROTOCOL_VERSION changeset says the line ships as 18.0.0-next.N. So a 17.8.0 spec release is not on the current line, but check:future-spec-major refuses any spec version above the package.json major (17), so 17.8.0 is the spelling the gates admit. The contract review read the version as a prediction that goes stale only if a release is cut first; this reading says the pre-mode line already makes it stale.",
"options": [
"A. Leave it. The gates require a 17.x spelling while package.json reads 17.7.0; the question covers every prescription written in the pre-mode window and belongs to the release lane (check:future-spec-major and the ADR-0087 level rule disagree only in pre mode). Cost: the shipped refusal may name a release that is never cut.",
"B. Re-spell it as 18.0.0 in a hand edit now. check:future-spec-major refuses it, and the hand edit ends the carry of the PASS record.",
"C. Drop the version from the prescription. That breaks the retired-value helper's own contract (name what replaced it, the removing version, the one-line fix)."
],
"recommendation": "A for this PR. Real business need: measured, the gate admits only 17.x, so B cannot land and C breaks a helper contract that every retirement follows. Long-term soundness: one rule for every pre-mode tombstone beats a one-off spelling on one PR. Preventing AI authoring mistakes: a single rule, settled in the gate, keeps tombstones consistent, while per-PR guesses do not. Startup focus: no new gate; the seat routes the question to the release lane only if it wants it settled. Not filed; the family size is NOT MEASURED."
}
],
"out_of_scope_findings": []
}objectstack-fleet commented
on Oct 9, 2026 ContributorAuthorMore actionsLanded: PR #22469 →
ee8751d41e(the first half:publicretired fromStorageScopeSchemaand refused at the upload doors).Part of #22443; the card goes back topm:queuefor the ruled follow-updomain:specseat 1 (#6017) ·os-tesla· sessionsession_01VZqqwTj2wsihZEbfT6yyYN· 2026-10-09T19:10Z · holder of claim6078016522, released by this act. ⛔ Class and function level only.- Landed: merged through the merge queue as
ee8751d41e(2026-10-09T18:44Z). It has one parent,9411faa1ba, and is an ancestor oforigin/main. - Content check: all 13 PR paths on
ee8751d41eare blob-equal to the reviewed headcb35f90d4d. The review chain: ACCEPT6081213304; at-tier contract review PASS6081142308at149062a9db, carried tocb35f90d4dover a pure regeneration byRegen-provenance:(6086677604). - What now holds:
StorageScopeSchemarefusespublicwith a prescription namingacl: 'public_read'.- Both upload-starting handlers answer
scope: 'public'with 400INVALID_REQUESTbefore any row, URL or backend upload. - The download handlers are unchanged.
- The step-18 D3 entry
storage-scope-public-retiredis registered and projected (step 18: 330 semantic entries).
- What remains (ruling B,
6081131776):- Remove the
publicoption fromsys_file.scope. - Add a one-time backfill in the shape of
backfill-sys-file-organizations.tsthat rewrites storedpublicrows touser. It counts before it writes, does nothing at zero, and names its inverse. - Pins: a copy of a rewritten row succeeds; a deployment with no such rows runs clean.
- The changeset states the data rewrite.
Clause-②: no (narrowing), with a contract review before the queue. That PR carriesFixes #22443.
- Remove the
- Filed from this half: finding(service-storage): an upload start naming a scope outside the sys_file vocabulary answers 500 INTERNAL (an engine invalid_option relayed as an internal fault) instead of a 400 naming the allowed scopes #22470 (an off-vocabulary upload scope answered 500).
Release:
session_01VZqqwTj2wsihZEbfT6yyYN· why: a partial landing (Part of #22443) · to:pm:queue, unassigned, for the ruled follow-up above. This seat re-claims it when a dispatch slot frees. This act moves the cardpm:dispatched→pm:queueand removes the assigneeos-tesla.- Landed: merged through the merge queue as
objectstack-fleet commented
on Oct 9, 2026 ContributorAuthorMore actionsClaim: PM loop round 4 (#22443 follow-up, ruling B
6081131776: retire thepublicoption fromsys_file.scopeand add a one-time backfill that rewrites storedpublicrows touser) · 2026-10-09T21:02Z
Session:session_01VZqqwTj2wsihZEbfT6yyYN
Account:os-tesla(the seat's linked user asGET /useranswers it; always the card's assignee)
Branch:claude/issue-22443-sys-file-public-scope-backfill
Worktree:objectstack-issue-22443-b
Domain:domain:spec
Seat:domain:spec#1
File surface (atorigin/mainfaf6348508; stop on breach and explain in the report):packages/services/service-storage/src/objects/system-file.object.ts(thescopeselect); one new backfill module besidepackages/services/service-storage/src/backfill-sys-file-organizations.tsand its test, plus the package export if the precedent has one; the copy-path pin nearfile-reference-lifecycle.tscopyOwnedFile(test only); one.changeset/22443-*.md. The ruling names the spec seat to dispatch this half, so the card stays in this lane although the files areservice-storage's.
Container & model:M,mode:subagent,model: default tier(dispatch-gates --tier: no path-derived mandate). The contract review atCONTRACT_REVIEW_TIERis owed before enqueue (the ruling: "contract review before the queue").
Clause-②: no
Responsibility:n/a — not a defect card (maintainer-ruled, #22443 ruling B6081131776)
Thread-read: 6087530099
Serial constraints cleared: PR #22469 (the first half) landed asee8751d41e. No open PR touchespackages/services/service-storage/(all 14 open PRs' file lists, read 2026-10-09T21:02Z), and nopm:dispatchedcard's claimed file surface names it.
Ruled: 6081131776 · letter B · 2026-10-09T12:44Z
Filing gate: ② a declared surface the runtime does not honour, found by the contract review of PR #22439 (
6077426765on #22431, escalation E1). Filed bydomain:servicesseat 1 (#6021) ·session_01WkL6Eijt432S1Y7ekb6ovQ. ⛔ Not a claim. ⛔ Class and function level only.What is declared
packages/spec/src/system/object-storage.zod.ts'sStorageScopeSchemalists'public'with the comment "Publicly accessible static assets". Both upload routes inservice-storage'sstorage-routes.tstake ascopefrom the caller and store it on thesys_filerow.What the runtime does
public-scoped file was anonymous only because of that, not because of its scope.6074960686item 3, on design(v18): the complete guest model in one ADR — identity, doors, grants channel, organization, public-site binding, disclosure, rate limits, and each declared guest key's fate (ADR-0090 D9 enforce-or-remove) #22146): the download routes judge a file byacl: 'public_read', the attachments scope and field ownership alone. Apublic-scoped file with the defaultaclnow needs a signed-in caller.scope: 'public'expecting a public file gets a private one, with no refusal at upload.The question for triage
Per the basic principle (a declaration the runtime does not honour is an implementation gap, closed by enforcing it or retiring it):
'public'fromStorageScopeSchema(a spec change,domain:spec). ADR-0104'sacl: 'public_read'stays the one opt-in for anonymous download.public-scoped upload meanpublic_readat the door. That widens: any uploader could choose anonymity at upload, which is the default ADR-0104 removed. It would need the maintainer.Not measured: in-repo producers of
scope: 'public'uploads. PR #22439's census found no shipped surface that renders a file before sign-in.Dedupe words:
storage scope public·StorageScopeSchema public·public_read vs scopeGenerated by Claude Code