Repository navigation
fix(service-storage, plugin-security): every member can attach files to the records they edit — storage contributes its own attachment set to the member baseline - #32932
Conversation
Claude-Session: https://claude.ai/code/session_01L6bhgTfDBmBpoPPcjp1HMK Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L6bhgTfDBmBpoPPcjp1HMK Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L6bhgTfDBmBpoPPcjp1HMK Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L6bhgTfDBmBpoPPcjp1HMK Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L6bhgTfDBmBpoPPcjp1HMK Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L6bhgTfDBmBpoPPcjp1HMK Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 3 package(s): 53 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 14 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 176 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin a0a0df0ca297c424c0cca9dd6594d7907ada705d && git checkout a0a0df0ca297c424c0cca9dd6594d7907ada705d
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 157051ceb48d2d9ed289f21f7018a39ce7249178 3ec4b7b3ffc08a3ef131b6121e7fb6109f63f7a4 && git checkout -B drift-repro 157051ceb48d2d9ed289f21f7018a39ce7249178 && git merge --no-ff 3ec4b7b3ffc08a3ef131b6121e7fb6109f63f7a4
node scripts/docs-audit/affected-docs.mjs --json 157051ceb48d2d9ed289f21f7018a39ce7249178
|
|
domain:server#4 · os-sales · session_01L6bhgWRcTEB3toGxnWvuvX Contract-review-claim: session_01L6bhgWRcTEB3toGxnWvuvX · head e1387f6 · 2026-11-12T23:55Z Generated by Claude Code |
|
domain:server#4 · os-sales · session_01L6bhgWRcTEB3toGxnWvuvX Contract reviewServed-tier: Faces: schema and declaration, one record. Read: card #32754 and its ten comments (ruling 6837170004, the review it adopted, analysis 6833029287, report 6839436462), the PR body, its file list, the net diff against merge-base Check-runs at 2026-11-13T00:14Z: 53 success, 10 skipped, 4 still running (three Test Core shards, Test UI), none failed. Result: one must-fix, two should-fix, five nits. The three conditions of the ruling are met as written; the must-fix is a fourth gate the ruling's rule covers and the head does not. ① Derived judgmentsPublic surface:
Runtime accept-set:
The ruling's conditions:
② Semver level
③ Boundary flags
④ Design judgment
Findings:
Implemented-by: VERDICT: FAIL Generated by Claude Code |
…; delegated agent pin Claude-Session: https://claude.ai/code/session_01L6bhgTfDBmBpoPPcjp1HMK Co-authored-by: Claude <noreply@anthropic.com>
|
domain:server#4 · os-sales · session_01L6bhgWRcTEB3toGxnWvuvX Contract-review-handoff: triage#1 — contract reviews go to triage by the maintainer's rule (2026-11-13T01:17Z) · head e8eb97d The maintainer's rule was given 2026-11-13T01:13Z. This is a delta over the FAIL record on Faces: schema + declaration, Generated by Claude Code |
|
triage#1 · hotlong · session_01Q4EjjMnyLqJdzuktEmRPXH Contract-review-claim: session_01Q4EjjMnyLqJdzuktEmRPXH · head e8eb97d · 2026-11-13T01:20Z Taken on the handoff 6840888219: a delta review over the FAIL 6839888392 on |
|
triage#1 · hotlong · session_01Q4EjjMnyLqJdzuktEmRPXH Contract reviewServed-tier: Face: schema. Delta review over the FAIL record 6839888392 on Check-runs at 2026-11-13T01:24Z: 27 success, 12 skipped, 30 in progress (all eight Test Core shards, Test UI, three type checks, Build Core, two lint groups), none failed. Result: one must-fix, one should-fix, two nits. The round-1 must-fix is closed as it was written. Closing it by the first route moved the problem: the fourth gate is not part of the stock composition, so the grant no longer reaches it. Prior findings
① Derived judgmentsPublic surface: unchanged from round 1. The delta touches the contract file in its docblock only; Runtime accept-set, as the delta changes it:
② Semver level
③ Boundary flags
④ Design judgment
Findings:
Implemented-by: VERDICT: FAIL Generated by Claude Code |
…-in itself The per-object attachment opt-in (`enable.files`) is now enforced by the storage service on every attachment insert and re-point, wherever it runs, so every gate the members' attachment grant relies on is its own and the grant reaches every composition that runs it (maintainer ruling on #32754, route A). The grant's fourth flag is the installer's own answer, no longer a service lookup. The boot warning names what to add. The delegated-agent pin now shows the scope limb through a refused call. Claude-Session: https://claude.ai/code/session_01L6bhgTfDBmBpoPPcjp1HMK Co-authored-by: Claude <noreply@anthropic.com>
…et on the harness too Claude-Session: https://claude.ai/code/session_01L6bhgTfDBmBpoPPcjp1HMK Co-authored-by: Claude <noreply@anthropic.com>
…chment opt-in code; note platform baseline sets Claude-Session: https://claude.ai/code/session_01L6bhgTfDBmBpoPPcjp1HMK Co-authored-by: Claude <noreply@anthropic.com>
|
domain:server#4 · os-sales · session_01L6bhgWRcTEB3toGxnWvuvX Contract-review-handoff: triage#1 — delta after the maintainer's ruling A (2026-11-13T03:43Z) · head 4b9e6ad Delta over triage's FAIL 6841031875 on Faces: schema + declaration, Generated by Claude Code |
|
triage#1 · hotlong · session_01Q4EjjMnyLqJdzuktEmRPXH Contract reviewServed-tier: Face: schema. Delta review over the FAIL record 6841031875 on Check-runs on 2026-11-13: 30 success, 12 skipped, 27 in progress (all eight Test Core shards, all sixteen Test UI shards, three type checks), none failed. Result: no must-fix, no should-fix, three nits. Prior findings
Ruling A, by consequence:
① Derived judgmentsPublic surface: Runtime accept-set, as the delta changes it:
Tried and not found: a widening onto another user's record, a read-only record, a system object, or an object with the opt-in off. The set's body, the seam's allow-lists, the write rule, the read-visibility gate and the download check are untouched by the delta. The new gate adds a refusal and takes none away; it exempts no caller; it runs once per row on the same dispatch as the write rule; a partial registration answers not installed, so no grant follows it. ② Semver level
③ Boundary flags
④ Design judgment
Findings:
Implemented-by: VERDICT: PASS Generated by Claude Code |
…seline set The storage service's member attachment set is registered in the catalog under its package when the security service admits it, so the catalog read and the metadata door both list it in every showcase posture. Claude-Session: https://claude.ai/code/session_01L6bhgTfDBmBpoPPcjp1HMK Co-authored-by: Claude <noreply@anthropic.com>
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 43370624005 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 分类: 失败的 job(日志抽取,best effort):
跨 PR 相同签名(24h,按失败测试文件聚合):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
|
domain:server#4 · os-sales · session_01L6bhgWRcTEB3toGxnWvuvX Contract-review-handoff: triage#1 — delta after the merge-queue ejection (2026-11-13T07:31Z) · head 3ec4b7b Delta over your PASS 6843343527 on Generated by Claude Code |
|
triage#1 · hotlong · session_01Q4EjjMnyLqJdzuktEmRPXH Contract-review-claim: session_01Q4EjjMnyLqJdzuktEmRPXH · head 3ec4b7b · 2026-11-13T07:33Z Delta review after the queue ejection (#33037): one test expectation plus a main merge, per the lane. Delta reviews stay with the original reviewer, which is this seat for this PR (r1 and r2). |
|
triage#1 · hotlong · session_01Q4EjjMnyLqJdzuktEmRPXH Contract reviewServed-tier: Face: schema. Delta review over the PASS record on Check-runs on 2026-11-13: 19 success, 11 skipped, 19 in progress (all eight Test Core shards, three type checks, Build Core, five lint groups, Live E2E UI), none failed. Result: no must-fix, no should-fix, one nit. What the head changedThe branch gained four commits: two merges of
① Derived judgmentsNo accept-set or public-surface change since ② Semver levelUnchanged: ③ Boundary flags
④ Design judgment
The implementing files in scope are unchanged by the branch's own commits. The earlier judgments stand. Findings:
Implemented-by: VERDICT: PASS Generated by Claude Code |
|
domain:server#4 · os-sales · session_01L6bhgWRcTEB3toGxnWvuvX 席位核对 — ACCEPT ·
|
Requested by Sales · project thread
Fixes #32754
维护者速读
everyone发给每个登录成员;member_default一字未改(ADR-0090 D9)。安全插件新增一个只在启动期开放的贡献接口来收这个集。ATTACHMENT_PARENT_ACCESS(仍是 403);编辑、删除附件仍要显式授权。外部用户、agent 自身、fallbackPermissionSet: null的部署、不带存储服务的宿主都不变;代成员操作的 agent 受成员和自身 scope 双重约束,成员能挂它才能挂。enable.files的对象现在对所有人拒绝附件(FILES_DISABLED,403),管理员和系统路径也一样,和它的声明(默认关)一致;已经挂在这类对象上的附件保持原样,开关只判新建和改挂。ISecurityService加一个可选成员contributePlatformBaselineSet(Clause-② widening),不加元数据键,不碰docs/adr/。Clause-②: yes (widening)
Metadata-protocol: none
What changes
The grant.
@objectstack/service-storagenow contributes its own permission set,storage_attachment_member: read and create onsys_attachment, nothing else. Every signed-in member holds it through theeveryoneanchor, besidemember_default, as an additive baseline (ADR-0090 D5).member_defaultis unchanged (ADR-0090 D9).The contribution happens in the storage plugin's
kernel:readyhandler, right after it installs the attachment gates, and only when all four are installed: the per-object opt-in gate (enable.files), the write rule, the read-visibility gate and the download door's parent-record check. Each flag is that installation's own answer. A kernel without one of them contributes nothing and says at warn level which gate is missing and what to add, so the object stays refused instead of ungated.The opt-in, enforced by the storage service (ruling 6841974978, route A).
installAttachmentFilesOptInGate(new,attachment-files-opt-in.ts) refuses403 FILES_DISABLEDon everysys_attachmentinsert, and every update that re-points a row, whose target object does not declareenable: { files: true }. An unregistered object or an unreadable definition is refused too. It runs for every caller, the system path included, and reads the live definition on each write. It is installed first, before the write rule, so every gate the grant relies on is the storage service's own, and no other plugin's order is involved. Another gate that enforces the same declaration may also run; both read the one definition and refuse with the one code.Whether the hosted runtime mounted the capability that enforced the opt-in before: unknown. The hosted runtime's per-project composition is not in this repository, and nothing here composes that capability for a host kernel. Measured on head
e8eb97dc,objectstack devonexamples/app-tododoes compose it, and a member there was granted. With this change the answer no longer decides either the grant or the opt-in.An agent principal gains nothing in its own right. An agent acting for a member is bounded by that member and by its own scope (ADR-0090 D10), so it can attach where the member can, within that scope.
The seam.
ISecurityServicegains an optional member,contributePlatformBaselineSet(contributor, set).SecurityPluginimplements it withPlatformBaselineSets. Its rules:kernel:bootstrapped.allowReadandallowCreate.An admitted set is registered in the catalog under the contributor's package. When the deployment declares a member baseline, the set's name joins the one composed baseline array that every baseline reader shares: the data plane, explain, effective access, the describe fallback and the
everyonedefinition. A set that joins after thekernel:readyderivation has begun causes one re-derivation of the anchor atkernel:bootstrapped. Every admission and every refusal is logged.Docs.
content/docs/permissions/attachments-access.mdxgains a "Who holds the object grant" section. Its callout and its enforcement summary now say that the baseline grants read and create, and that edit and delete need an explicit grant. Its opt-in section says the storage service enforces the opt-in on insert and re-point, for every caller, wherever it runs.content/docs/permissions/permission-sets.mdx: a note under the built-in sets table that a platform service can contribute a baseline set of its own, and that the storage service contributesstorage_attachment_member.attachment-access-hooks.tsis fixed.Error-code ledger.
FILES_DISABLEDis listed under@objectstack/service-storageas a second emitter of a code another package already registers: provenance, not a new code.Tests
storage-service-plugin.test.ts(the order: opt-in gate, write rule, read visibility, then the grant; no middleware seam, no auth service, no hook seam: each contributes nothing and the warning names the gate);attachment-member-grant.test.ts(each missing gate, with the remedy named; no security service, no seam, a refusing seam, no baseline)storage-service-plugin.test.ts: a kernel with no other opt-in capability gets the grant, and the opt-in still refuses on insert and re-point; a kernel with a service registered under that capability's name, on an engine without the hook seam, gets no grant and the warning names the opt-in gate.attachment-files-opt-in.test.ts: installed answerstrueonly when both registrations went throughattachment-files-opt-in.test.ts:files: false, nofiles, no enable block, an unregistered object, an unreadable or absent definition all refuse;files: trueadmits; the system context is not exempt; a write naming no parent object passes; the live definition is read on each writeplatform-baseline-sets.test.ts: unnamed contributor, closed window, row policy, every non-closed key, a second set, another contributor's name or a shipped name, no registry, no object,'*', another package's object, an unregistered object, every bit other than read and create (even whenfalse), non-boolean values, an entry that grants nothing, a malformed set, the registry refusingplatform-baseline-sets.integration.test.ts: a member reads and creates, and cannot edit or delete; explain names the set as an additive baseline; theeveryonedefinition names it even though it landed after the derivation; the catalog holds it under the service's package; an external user gets nothing; an agent principal's own leg gets nothing; another package's object is refused on the real registry; the window is closed after bootstrap;fallbackPermissionSet: nulladmits the set and hands it to no oneattachToand the MCPcreate_upload_linkdoor; an agent acting for the member on an OAuth token (admitted where the member and the token's scope both allow; refused with the attach rule's code where the member does not; the same call on a read-only token refused at the door, minting nothing);ATTACHMENT_PARENT_ACCESSon a readable-but-not-editable record; unreadable records' attachments stay invisible (list, total, by id); another user's attachment on a readable record is visible;FILES_DISABLED; explain names the storage set; a host without the storage service still answers 403 with no storage set; the storage service and the security plugin alone hand the grant out, and the opt-in refuses there for the member, the administrator and a system-path re-pointmember-attachment-baseline.dogfood.test.ts(new, real stack, 13 tests)security-service.test.ts(the member is optional, and its type carries only read and create);registered-security-service-members.pin.test.tsFour existing expectations move with the change:
security-catalog-showcase: the permission catalog listsstorage_attachment_memberin every showcase posture. The security service registers an admitted platform baseline set in the catalog under the contributing package, so the catalog read and the metadata door both list it, and it resolves by name; it is the set theeveryonedefinition and explain name. The expected names add it as its producer names it, not read off a registry.showcase-d7-default-profilenow lists the storage set on theeveryonedefinition: the harness composes the storage service, and every gate is its own.print-service-archive: an auditor who can read but not edit the invoice is now refused by the attach rule (ATTACHMENT_PARENT_ACCESS) instead of by the missing object grant. It is still a 403, and still raised before any mint or render.catalog-reference-reportnow sees twokernel:bootstrappedhandlers, and the report is still the last.Verification
After the merge-queue ejection (head
3ec4b7b3): the queue's full Dogfood Regression Gate foundsecurity-catalog-showcaseexpecting the catalog without the storage set (above). That expectation moved (84c30f2b, one test file), andmainwas merged again before re-entering the queue (merge commit3ec4b7b3, 17 commits, no conflict):distrebuilt on the merged tree (70 tasks).@objectstack/service-storagefull: 81 files, 1512 passed.@objectstack/plugin-security, the four suites this PR touches: 72 passed.@objectstack/specsecurity-service.test.tsanderror-code-ledger.test.ts: 56 passed.84c30f2bbefore the merge: 458 passed, 3 skipped, 0 failed.node scripts/pm/dispatch-gates.mjs --ranover the PR's change set on the merged tree: 130 derived, 130 run, 0 NOT-MEASURED, 0 UNRUN, every exit 0. On84c30f2b, the gates the changed test file derives: 52 run, every exit 0.After the pre-queue merge of
main(merge commit1b46bf82, 56 commits, no conflict; headb9878efd, which also adds one changeset sentence: attachments already stored on an object without the opt-in stay as they are, since the opt-in judges only a new attachment and a re-point):@objectstack/service-storagefull: 81 files, 1512 passed.@objectstack/plugin-security, the four suites this PR touches: 72 passed.@objectstack/specsecurity-service.test.tsanderror-code-ledger.test.ts: 56 passed.distrebuilt): the affected suites, 57 files, 56 green and 1 skipped as onmain.node scripts/pm/dispatch-gates.mjs --ranover the PR's change set on the merged tree: 130 derived, 130 run, 0 NOT-MEASURED, 0 UNRUN, every exit 0.At head
4b9e6ad1(ruling A on top ofe8eb97dc; merge-base then5fb79bc0). Only the gates and suites that round touched were run again.@objectstack/service-storage(full: 80 files, 1499 passed, before the last one-line change to the branch without the hook seam; the three suites it touches, 54 passed after it). Typechecks for service-storage (source and test projects) and dogfood. ESLint on the changed files is clean.@objectstack/specgains only the ledger row: its ledger suites (26 passed),check:error-code-provenanceandcheck:generatedare green. plugin-security is untouched this round (its runs are in the round-1 record below).everyoneanchor or the storage service, 54 files: 53 green and one expectation that moves (showcase-d7-default-profile, above), then green.HEADand a clean tree:attachment-files-opt-in.test.tsand the storage plugin pins red);distrebuilt, the marker proved present, then proved absent after the restore): with the opt-in refusal disabled, the storage-alone host's two opt-in tests go red, and the main suite's own opt-in test stays green as the control, since another opt-in gate is composed there; with the fourth flag read from a service name, the storage-alone host loses the grant and goes red.PERMISSION_DENIEDby the token's scope ceiling at the data plane. The scope limb is defended twice, and the pin asserts the refused call, not the menu.node scripts/pm/dispatch-gates.mjs --ranover the gates this round's paths derive: 129 derived, 129 run, 0 NOT-MEASURED, 0 UNRUN, every exit 0 (one first-run red,check:error-code-provenance, fixed by the ledger row above and rerun with every gate the two late paths derive).check:generatedfor spec is current.Round 1 (head
e8eb97dc; plugin-security unchanged since): plugin-security full 266 files, 6573 passed at the round-0 head, and the four suites round 1 touched, 72 passed; specsecurity-service.test.ts30 passed;check:generatedcurrent; the seam ablations (owner check, bit allowlist, wildcard, row policy, window close, log lines, re-derivation, null-baseline join) red then restored; 128 gates, 0 NOT-MEASURED.Generated by Claude Code
🤖 Generated with Claude Code
https://claude.ai/code/session_01L6bhgTfDBmBpoPPcjp1HMK