Uh oh!
There was an error while loading. Please reload this page.
fix(plugin-sharing): declared publicSharing.redactFields survive the object opting out - #14171
Conversation
…ommit) The dispatched dev agent was terminated mid-task by a session rate limit (HTTP 429) before it could commit or push. This commit preserves what was on disk so the container's reclamation cannot lose it. NOT reviewed and NOT verified: no suite was run against this state, and the work is incomplete by the agent's own account (it was still re-acquiring the shared verify lock for its reproduction leg). Treat as a starting point to amend, never as a finished change. Deliberately excluded: repro-13856.scratch.test.ts, which the agent named as scratch and plainly did not intend to ship.
…object opting out getPolicy() collapsed to an empty policy whenever the object's publicSharing block had enabled !== true — redactFields: [] included — so a link minted while the object was opted in, redeemed after it was opted out, kept resolving and started serving the very fields the object declares redacted. The declared redaction set is now read from the declared block regardless of enabled, so opting out can never widen what an existing token serves. The mint-time gate, the redemption-time eligibility gate, the per-link union, and the no-block path are unchanged; whether standing links should resolve at all after opt-out is a separate pending ruling and is deliberately not implemented here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016ZC5rNQj3WEet5HAmmAkMs
📓 Docs Drift Check1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 8 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 802bca641b4d97366ffe5cc2baf1017c3fc264cb && git checkout 802bca641b4d97366ffe5cc2baf1017c3fc264cb
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 45b9051248f86f362b042fa9de63295a8c224073 ab98ef31008845fb99874950211013ba5f0b5838 && git checkout -B drift-repro 45b9051248f86f362b042fa9de63295a8c224073 && git merge --no-ff ab98ef31008845fb99874950211013ba5f0b5838
node scripts/docs-audit/affected-docs.mjs --json 45b9051248f86f362b042fa9de63295a8c224073 |
os-steve
commented
Sep 1, 2026
契约复审:PASS —— 逐字采纳,双载体标签已清转录核验: ⇒ 每轮在档、零回退 ⇒ 逐字采纳。以下为复审席原文,⛔ 未改写、未删节、未润色: 契约复审:PASS结论一行退订后声明红act集单向收窄、无一路径放宽,#14033/#13608 栅栏完好,可照发。 逐题回答Q1 — 每条路径都严格收窄:是。 证据链(全部读自
结论:开关 OFF 时 served 集 ⊆ ON 时 served 集,且 new-served ⊆ old-served,零放宽。唯一多流出比特的通道是响应封皮里的 Q2 — 范围栅栏是真的:是。 变更文件恰为 3 个(changeset、test、service, Q3 — disabled 时"声明块"的含义,连贯且有边界。 半生效是有意的且边界可陈述:(a) spec 对 Q4 — 发布形状零变化。 Q5 — 无需制造。 无阻塞级新发现。 tsc 边界(核实,非采信):属实。 非阻塞
我核了什么 / 我没核什么核了:PR head 没核(NOT MEASURED):新测试的实际执行与 30 files / 705 tests 绿灯——由 CI 度量,dev 报绿;dev 的行为学复现([owner_id, cost, body] → [body])未复跑; PM 侧收口
⭐ 这份裁决里最值钱的是 Q1 的结构性证明,不是测试它没有停在「测试断言了子集方向」,而是从代码把单调性推了出来:单 hunk、三种输入形态逐一比对得
|
…shifted it `check-system-context-census` failed on this branch: 10 problems over 145 anchors and 109 census sites — 5 `[anchor-is-not-a-read-site]` plus the 5 `[site-without-a-row]` reads they had rotted off. Cause: this PR's single hunk in `getPolicy`'s disabled branch (the `redactFields` ternary and its explaining comment, +12/-1 at line ~103) pushed every `isSystem` read site below it down by exactly 11 lines, so row 37's citations in `content/docs/permissions/system-context.mdx` no longer resolved. Pure line rot, not a population change — the read-site count is unchanged at 109, each stale anchor pairs 1:1 with an orphaned site at a uniform delta of +11, and the old and new lines are byte-identical: :423 -> :434 :477 -> :488 :481 -> :492 :554 -> :565 :584 -> :595 Repaired with `node scripts/check-system-context-census.mjs --fix`, which rewrote 5 anchors and REFUSED nothing. No anchor was hand-edited; no source or test file is touched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016ZC5rNQj3WEet5HAmmAkMs
os-steve
commented
Sep 1, 2026
|
| stale anchor | orphaned read site | delta | old vs new line |
|---|---|---|---|
:423 | :434 | +11 | byte-identical |
:477 | :488 | +11 | byte-identical |
:481 | :492 | +11 | byte-identical |
:554 | :565 | +11 | byte-identical |
:584 | :595 | +11 | byte-identical |
Cardinality 5 to 5, uniform delta, and each old anchor line at HEAD~1 is byte-identical to its new site line at HEAD. Site count is unchanged at 109.
What --fix did.node scripts/check-system-context-census.mjs --fix, exit 0:
re-anchored content/docs/permissions/system-context.mdx:138 `plugin-sharing/src/share-link-service.ts:423` -> `plugin-sharing/src/share-link-service.ts:434`
re-anchored content/docs/permissions/system-context.mdx:138 `:477` -> `:488`
re-anchored content/docs/permissions/system-context.mdx:138 `:481` -> `:492`
re-anchored content/docs/permissions/system-context.mdx:138 `:554` -> `:565`
re-anchored content/docs/permissions/system-context.mdx:138 `:584` -> `:595`
check-system-context-census --fix: 5 anchor(s) rewritten
It REFUSED nothing. A refusal prints NOT fixable: on stdout; that line count in the captured output is 0, against 5 re-anchored lines. This is a positive reading rather than merely absent text: the same run's --self-test exercises FIX: a population change is REFUSED, never guessed and four further #13490 refusal cases, all ok, so the refusal machinery was live and silent here.
Confirming re-runs, at the final commit ab98ef310:
node scripts/check-system-context-census.mjs— exit 0:OK — 109 elevation read sites in 20 packages across 45 files, all anchored; 145 anchors resolve, 27 declared non-read.node scripts/check-system-context-census.mjs --self-test— exit 0:all cases passedgit diff --stat— one file:content/docs/permissions/system-context.mdx | 2 +-. Only the five line numbers in row 37 changed; the row's prose is untouched. No anchor was hand-edited, andshare-link-service.tsand its test are untouched.
Also green on this diff: check:doc-anchors, check:doc-frontmatter, check:doc-route-spelling, check:docs-section-name, check:section-landing-index, check:docs-single-h1, check:docs-redirects, check:docs-audit-scope, check:corpus-claim-drift, check:role-word, check:doc-authoring, check:keyed-text-bounds, check:nul-bytes.
Why no local run could have caught this
check-system-context-census is not derivable from this PR's diff. node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, run on the clean tree at 85fb05e55 before any edit, does not name it — it scores silent, the weakest verdict:
- node scripts/check-system-context-census.mjs [lint.yml] names: content/docs/permissions/system-context.mdx, packages/spec/src/kernel/execution-context.zod.ts, packages/spec/src/data/object.zod.ts, …
The gate's declared literals are the page plus a 29-entry NON_READ_ANCHORS roster; packages/plugins/plugin-sharing/src/share-link-service.ts appears nowhere in the gate source. Its real population is resolved at runtime — git ls-files packages examples in isystem-census.mjs. So a diff touching only packages/plugins/plugin-sharing/src/** can never pull this family in, while CI schedules it unconditionally. Reported for triage; no card filed from here.
Generated by Claude Code
Uh oh!
There was an error while loading. Please reload this page.
The auto-merge of origin/main kept this branch's pre-#14171 anchor rows beside the row-21 edit, flunking the census 26 ways (paired stale anchors / unanchored sites in engine.ts and share-link-service.ts — pure adjacent-row line rot, zero population change). Resolution per the tool, not by hand: took main's page wholesale, then re-derived with check-system-context-census --fix, which rewrote exactly ONE anchor (row 21, protocol.ts 1737 to 1741 — this branch's publisher-block shift). Delta vs origin/main is that single line; bare gate green: 109 sites anchored, 145 anchors resolve. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q5WBDtaUnoz5XuJ6jk8pQ5
Fixes#13856
What
ShareLinkService.getPolicy()returned an EMPTY policy —redactFields: []included — whenever the object'spublicSharingblock was absent orenabled !== true.resolveToken()computes the served redaction set aspolicy.redactFields∪ the per-linkredact_fields, so a link minted while the object was opted IN and redeemed after it was opted OUT kept resolving and started serving the very fields the object declares redacted: turning the feature OFF made the anonymous endpoint serve MORE data than it did while the feature was ON.The fix: the disabled branch of
getPolicynow reads the declaredredactFieldsfrom the object's declared block regardless ofenabled, so switching the feature off can never widen what an existing token serves.resolveTokenis the only consumer ofpolicy.redactFields(createLinknever reads it), so the seam is exactly the served set.Scope fence
publicSharing.enabledoff mean for an ALREADY-MINTED share link? — the parent switch is mint-only while its own child predicate is now a standing policy #14033 remains open — whether an already-minted link should resolve at all onceenabledis false is a deployment-visible policy ruling (needs-user-decision), and neither answer is implemented here. In particular sharing:publicSharing.eligibilityis evaluated only at mint — a link keeps serving a record after it stops being eligible #13608's standing-policy shape was NOT extended toenabled: the redemption-time eligibility gating (policy.enabled ? policy.eligibility : undefined) is byte-untouched.SHARING_NOT_ENABLED, 422) is untouched — the disabled branch still returnsenabled: false.Behavioural reproduction (the card was a source reading — "no test was run for it")
Measured pre-fix on this branch's base, which contains #13857's rework (
loadRecordForServing/stillEligiblepresent):redactFields: ['owner_id','cost'], per-linkredactFields: ['body']; redeem while ON: served redaction set["owner_id","cost","body"].publicSharing.enabledtofalse; redeem again:["body"](declared set gone; per-link only)Both card claims hold post-#13857; premise valid.
Verification (at
85fb05e55unless noted)New tests (
share-link-service.test.ts,[#13856]block):{'owner_id','cost','body'}).enabled: trueserves exactly declared ∪ per-link. Green in both directions ⇒ declared control, not ablation evidence.publicSharingblock sprouts NO redaction set out of this change; per-link-only redaction still applies there.enabled: falsestill refuses withcode: SHARING_NOT_ENABLED,status: 422(ADR-0112 envelope assertion, not bare toThrow).Reverse verification of the tests (pre-fix run): exactly the 2 predicted reds —
expected [ 'body' ] to include 'owner_id'andexpected Set{ 'body' } to deeply equal Set{ 'owner_id', 'cost', 'body' }— 24/26 green.Ablation from the committed fix. The subject resolves via the relative
./share-link-service.jssrc import under vitest transform — not via packageexports/dist — so no build leg applies to either leg (stated, not skipped). Direction predicted first: exactly 2 reds (THE REPRO, boundary).raw?.redactFields1 to 0, injectedABLATION-138560 to 1; worktree blob32a9e6988…changed to5d8f78cd9…(git hash-object, non-empty).Tests 2 failed | 24 passed (26)— exactly the predicted two.32a9e6988…,git diff HEADempty, marker counts back to 1 / 0.Package union at
85fb05e55via the verify-lock entry point (its own printed verdict:VERDICT command-exit 0): vitestTest Files 30 passed (30) · Tests 705 passed (705)— includes the #13608 redemption-eligibility pins and the #7861/#8489/#9085 suites (boundary requirement: that gate untouched and green) — plus packagetypecheckgreen.tsc program membership (
tsc --noEmit --listFiles):src/share-link-service.tsin the program (1 hit). NOT MEASURED: the test file's types — the package tsconfig and tsconfig.scripts.json both exclude*.test.ts(0 hits in both programs); standing package convention, and both halves of the coverage gate are green (check:type-check-coverageOK;check:type-check-debt --re-measure: 29 entries, none above recorded).Gate family re-derived from the actual diff (
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, no path args; derivation stderr named this repo at85fb05e55; both output sections read whole): 36 harvested families run, every exit captured before any pipe. All green except one:check-test-completeness= NOT MEASURED by the gate's own documented local branch — it grades a CI-savedturbo run testlog and instructs "running the family locally, record this gate as NOT MEASURED". CI supplies the log on every PR.check:i18nandcheck:dual-build-cjs-loadsfirst refused loudly (PREREQUISITE NOT MET, exit 3 ≠ red); green after their named closure builds —check-i18n-bundles: OK (9 package(s)), dual-build floors 90/58/520/1 vs this run 102/66/610/1.check:nul-bytesgreen; control-character self-scan of all three edited files: no hits.Fixture sweep: no existing fixture anywhere in the repo pins the disabled+
redactFieldscombination (allpublicSharingreferents checked) — which is why the defect had no reading; the new tests are an addition, not a rewrite of a standing assertion.Declared narrowing:
pnpm lint(repo-wide ESLint) is CI-owned and was not run locally.Clause-②
Clause-②: yes — this changes what the anonymous share-link endpoint actually serves (a deployment-visible narrowing on opted-out objects). Surface touched:
packages/plugins/plugin-sharing/src/share-link-service.ts(thegetPolicydisabled branch only)packages/plugins/plugin-sharing/src/share-link-service.test.ts.changeset/share-link-redactfields-survive-optout.md(patch, declares the narrowing)No
packages/spec/**surface touched; no schema accept/reject change.Session: https://claude.ai/code/session_016ZC5rNQj3WEet5HAmmAkMs
Generated by Claude Code
Generated by Claude Code