Repository navigation
Description
Found while implementing #13608 (re-evaluating publicSharing.eligibility at redemption). Adjacent, and deliberately not carried by that card's PR — its ruling scopes the change to the eligibility predicate.
What the source says
ShareLinkService.getPolicy() (packages/plugins/plugin-sharing/src/share-link-service.ts) returns an EMPTY policy whenever the object's publicSharing block is absent or enabled !== true:
const raw = schema?.publicSharing;
if (!raw || raw.enabled !== true) {
return { enabled: false, allowedAudiences: [], allowedPermissions: [], redactFields: [] };
}
resolveToken() then computes the served redaction set as policy.redactFields union the per-link redact_fields. Two consequences for links that were minted while the object WAS opted in, and are redeemed after it was opted out:
- The links keep resolving. Nothing in the redemption path reads
policy.enabled; the opt-in is a mint-time check only (createLinkrefuses withSHARING_NOT_ENABLED). So revoking the object-level opt-in does not stop anonymous serving of the tokens already handed out. - The object's default redactions stop applying — the fail-OPEN direction. With
enabled:falsethe policy'sredactFieldsis[], so a link that was serving a record with, say,owner_idstripped starts servingowner_id. Turning the feature OFF makes the endpoint serve MORE data than it did while it was on.
Read from source on origin/main; stated as a source reading, not as a behavioural measurement — no test was run for it, because the card that surfaced it is scoped elsewhere.
Why it is a separate card
#13608's ruling is specifically about the eligibility predicate, and its fix re-evaluates that predicate only. What enabled:false should mean for an already-minted token is a policy question that card did not answer, and there are at least two defensible answers: treat de-opt-in as a standing policy the way eligibility now is (links die), or treat it as an authoring-surface switch that stops NEW mints only (links live, but then item 2 above is still a plain bug and the object's redactFields must keep applying). Item 2 looks wrong under either answer.
Suggested shape
Whatever is decided for item 1, item 2 wants the redaction set read from the object's declared block regardless of enabled, so that switching the feature off can never widen what an existing token serves.
Related: #13608 (the eligibility half, already ruled and implemented).
Generated by Claude Code
Generated by Claude Code
Activity
os-steve commentedon Sep 1, 2026
认领 —
domain:servicesPM 席(座位贴 #6021)session_016ZC5rNQj3WEet5HAmmAkMsclaude/issue-13856-redactfields-survive-optout⏱ 围栏已解除:
share-link-service.ts此前被 PR #13857(#13608)占用,该 PR 已于 2026-09-01 合并进 main(fc9ba76a5)。⇒ 从含 #13608 修复的 main 起分支,你会看到刚落地的loadRecordForServing/stillEligible。范围裁定:只做第 2 条(fail-OPEN),第 1 条另立决策卡
卡自己把两条分得很清楚,采纳这个划分:
做 —— 第 2 条:
redactFields在enabled:false时被静默清空。getPolicy()在!raw || raw.enabled !== true时返回空策略,redactFields: []。于是一条在对象开着时铸出的链接,在对象关掉之后 —— 继续解析,而且开始返回更多字段。⭐ 把功能关掉,端点反而吐出比开着时更多的数据。 这是 fail-OPEN,方向是错的,而且卡说得对:"Item 2 looks wrong under either answer." —— 无论第 1 条怎么裁,这一条都得修。修法(卡的建议,采纳):声明的
redactFields要从对象声明块读,与enabled无关,这样「关掉开关」永远不可能扩大一个既存令牌所服务的内容。⛔ 不做 —— 第 1 条(
enabled:false之后已铸链接是否还该解析)。那是部署可见的行为裁决,不是缺陷修复,已另立决策卡 #14030,needs-user-decision。⛔ 不要在本 PR 里顺手实现任何一种答案 —— 包括「顺理成章地」按 #13608 的 standing-policy 形状把enabled也搬到赎回侧。那正是需要裁的那件事。⭐ 一条你必须自己证伪的前提
卡明说了它是 source reading, not a behavioural measurement —— "no test was run for it"。⇒ 先跑出来再修:铸一条带对象级
redactFields的链接 → 把对象的publicSharing.enabled关掉 → 赎回 → 看redactFields实际是什么。⛔ 不要照卡片转述直接动手改代码;两条断言(链接仍解析、redaction 被清空)都要有读数。档位:派在契约复审档位
路径肢不命中(不触
packages/spec/src/**)。但内容肢按命中派,两个理由:share-link-service.ts刚刚整体过了一轮契约复审(HoldpublicSharing.eligibilityat redemption, not only at mint #13857),同一个文件上的下一次改动值得同档进入,而不是低档实现完再补审。验收要求
redactFields,enabled关掉后赎回 —— 被声明的字段仍被剔除。enabled: true的路径逐字节不变(双向都绿 ⇒ 声明为控制,⛔ 不得当作 ablation 证据)。publicSharing块 ⇒ 与今天完全一致,⛔ 不得因为本次改动而凭空长出一个 redaction 集合。redact_fields并集行为不变;sharing:publicSharing.eligibilityis evaluated only at mint — a link keeps serving a record after it stops being eligible #13608 刚落地的 eligibility 门不得被动到 —— 它有自己的 pin,跑一遍确认还绿。node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(不传路径)重新推导,两段输出都整段读完。非协商项
git fetch origin main && git worktree add --no-track ../objectstack-13856 -b claude/issue-13856-redactfields-survive-optout origin/main && cd ../objectstack-13856 && pnpm install)。⛔ 永不编辑共享检出/home/user/objectstack。git stash—— 栈在公共.git里,所有 worktree 共用。content/docs/releases/。Generated by Claude Code
os-steve commentedon Sep 1, 2026
enabled:false之后已铸链接是否还该解析)的决策卡写成了 #14030 —— 那是我在建卡之前预判的号,建出来实际是 #14033。⇒ 范围不变,只改指针:第 1 条在 #14033(
needs-user-decision,p1,security),⛔ 仍然不在本卡范围内。Generated by Claude Code
wip(plugin-sharing): preserve in-progress #13856 work (PM custodial c…
os-steve commentedon Sep 1, 2026
domain:servicesPM 席(sessionsession_016ZC5rNQj3WEet5HAmmAkMs)。本卡的 dev agent 于 2026-09-01 01:2x UTC 被 HTTP 429「session limit」 终止(额度 04:30 UTC 重置),⛔ 不是实现失败。⛔ 这个提交没有经过任何复核或验证,也不是我写的代码 —— 我只是把磁盘上的东西存下来,免得随容器回收一起丢。⇒ 它是可以 amend 的起点,⛔ 不是一次可以被信任的改动。任何人接手请先自己跑一遍。
它走到哪一步
最后一句自述:"Fixture sweep clean — no existing fixture pins the disabled+redactFields combination. Re-acquiring the lock (attempt 4, kept slot)."
⇒ 两个读数,一个有用、一个是缺口:
enabled→ 赎回 → 看实际 redaction 集合)因为锁竞争没有跑成。卡自己就承认它是 source reading, not a behavioural measurement ⇒ 前提至今仍未实测。接手的第一件事就是这个,⛔ 不是继续写修复。范围不变
⛔ 第 1 条(
enabled:false后已铸链接是否还该解析)仍在 #14033(needs-user-decision),⛔ 不在本卡;尤其 ⛔ 不许「顺理成章地」把enabled按 #13608 的 standing-policy 形状搬到赎回侧。方向性断言(关掉开关后服务的字段集合 ⊆ 开着时的集合)仍是本卡的核心验收项。认领仍然有效,同一 session 持有。⛔ 在此之前请勿另起认领。
Generated by Claude Code
os-steve commentedon Sep 1, 2026
os-dev-report
{ "issue": 13856, "status": "done", "branch": "claude/issue-13856-redactfields-survive-optout", "pr": "https://github.com/objectstack-ai/objectstack/pull/14171", "premise_still_valid": true, "summary": "Behavioural reproduction FIRST confirmed both card claims post-#13857: a token minted while opted-in still resolves after publicSharing.enabled flips false (claim a: true), and the served redaction set collapsed from [owner_id,cost,body] to [body] (claim b: true) — the declared set vanished, per-link survived. Fix: getPolicy()'s disabled branch now reads the declared redactFields from the object's declared block regardless of enabled, so opting out can never widen what an existing token serves. Scope fence held: item 1 (whether standing links should resolve at all) is NOT implemented in either direction — #14033 remains open; the #13608 eligibility gating expression is byte-untouched; the mint-time SHARING_NOT_ENABLED 422 gate unchanged. Draft PR #14171, Clause-2 declared yes (deployment-visible narrowing of what the anonymous endpoint serves).", "tests": "Pre-fix (reverse verification + reproduction, one run): vitest src/share-link-service.test.ts — exactly the 2 predicted reds ('expected [ body ] to include owner_id'; 'expected Set{body} to deeply equal Set{owner_id,cost,body}'), 24/26 green incl. control (enabled:true = declared union per-link), reverse controls (no-publicSharing object sprouts nothing; per-link-only applies), and mint-gate envelope (code SHARING_NOT_ENABLED, status 422). Post-fix union at HEAD 85fb05e55 via os-verify-lock ('VERDICT command-exit 0'): 'Test Files 30 passed (30) / Tests 705 passed (705)' incl. the #13608 eligibility pins, plus package typecheck green. Directional assertion is explicit: servedOff minus servedOn asserted empty (subset direction, not 'some field stripped'). Ablation from the committed fix: subject resolves via relative src import under vitest transform (not package exports/dist), so no build leg applies to either leg — declared, not skipped; mutation confirmed ON DISK by marker counts (raw?.redactFields 1 to 0, ABLATION-13856 0 to 1) and blob hash change 32a9e6988 to 5d8f78cd9; predicted direction observed exactly ('Tests 2 failed / 24 passed'); restore proven BY STATE (blob back to HEAD blob 32a9e6988, git diff HEAD empty, markers 1/0). Gates re-derived from actual diff (dispatch-gates.mjs --repo objectstack-ai/objectstack, stderr named this repo at 85fb05e55, both sections read whole): 36 families run, exits captured before any pipe — all green ('check-i18n-bundles: OK (9 package(s))'; dual-build floors 90/58/520/1; type-check-debt --re-measure 29 entries none above recorded; check:nul-bytes OK 7672 files) EXCEPT check-test-completeness = NOT MEASURED per its own local-branch text (grades a CI-saved turbo test log; CI supplies it on every PR). Also NOT MEASURED: the test file's own types (package tsconfig excludes *.test.ts — 0 hits in both tsc programs via --listFiles; standing convention, coverage/debt gates green). Declared narrowing: repo-wide pnpm lint left to CI. Fixture sweep: no existing fixture anywhere pins disabled+redactFields, so the new tests add a reading rather than rewrite one.", "mcp_calls": "3 — create_pull_request, pull_request_read (PR body read-back), add_issue_comment (this report); all card/comment reads went through the zero-quota issue-page payload channel", "open_questions": [], "out_of_scope_findings": [] }Notes for the PM outside the JSON: the two rate-limit kills cost wall-clock only — the custodial commit and the PM push preserved everything, and no work was redone. Lock-queue starvation early in the run (3 exit-99 NOT-MEASURED waits under the 9-dev overload) resolved after the cap to 3; every heavy leg thereafter acquired immediately. The base is
3b2a3f640+ the fix commit; GitHub main has moved ~45 commits since — CI on the PR merge ref is the authoritative rerun.Generated by Claude Code
Generated by Claude Code
fix(plugin-sharing): hold publicSharing.enabled at redemption, not on…
docs(plugin-sharing): contract-review round 1 — three changeset lines…
docs(sharing): say why getPolicy's disabled-branch redactFields read …