fix(plugin-sharing): hold publicSharing.enabled at redemption, not only at mint (#14033) - #14580

Merged
os-sales merged 10 commits into
mainfrom
claude/issue-14033-public-sharing-enabled-standing-policy
Sep 2, 2026
Merged

fix(plugin-sharing): hold publicSharing.enabled at redemption, not only at mint (#14033)#14580
os-sales merged 10 commits into
mainfrom
claude/issue-14033-public-sharing-enabled-standing-policy

Conversation

@claude

@claudeclaudeBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Fixes#14033

Ruling of record (verbatim, untranslated)

Maintainer ruling of 2026-09-01, recorded by the director seat in 14033#issuecomment-5491054105 (maintainer's verbatim reply to the batch: 「其他同意」). The recorded four points, verbatim:

  1. A:publicSharing.enabled 为持续策略,resolveToken() 每次兑付重查;关掉 ⇒ 存量链接停止兑付。
  2. 追溯即时生效(沿 sharing: publicSharing.eligibility is evaluated only at mint — a link keeps serving a record after it stops being eligible #13608 先例):部署一落地,禁用块上的所有既有 token 即停止解析。changeset 必须标注 breaking runtime change,与 Hold publicSharing.eligibility at redemption, not only at mint #13857 同型。
  3. 系统/宽容旁路铸造的链接同样受管:兑付是匿名动作,不因铸造方式豁免 —— enabled 关则一律拒付(含 system-context.mdx ledger row 37 的旁路铸造路径)。
  4. 兄弟键统一律入册:enabled 关 ⇒ 整块不生效 ⇒ 拒付(子键求值 moot);enabled 开 ⇒ 块内各策略键按持续策略在兑付期求值(eligibility 已然,redactFieldssharing: turning publicSharing off leaves already-minted links serving — and silently drops the object's redactFields #13856 修复)。后续兄弟键⛔ 不再单独立卡。

And its implementation notes, verbatim:

Premise verification on origin/main (BASE 53d368921) — all five hold

  • P1 — measured, not read. The first pin (THE REPRO — minted while enabled, the SAME token stops resolving the moment the block is turned off) was run against the UNMODIFIED service before any edit: AssertionError: expected { link: { …(16) }, …(1) } to be null — the token kept serving with the block off. 15 such reds (every new/reversed pin), 61 existing pins green. Log: pre-fix run, Tests 15 failed | 61 passed (76). Premise valid.
  • P2 — PR fix(plugin-sharing): declared publicSharing.redactFields survive the object opting out #14171 (sharing: turning publicSharing off leaves already-minted links serving — and silently drops the object's redactFields #13856) pins. Two pins asserted a link on a DISABLED block still serves with redactions applied (THE REPRO — opting out keeps the declared redactions applying, boundary — the per-link redact_fields union is unchanged when the switch is off), and two reverse controls asserted a bypass-seeded link on a block-LESS object serves. All four are REVERSED in place and registered (docblock naming what each pinned, which ruling point moved it, the ruling verbatim in the block header); none deleted. The pins are agent-authored test content in share-link-service.test.ts, not governed content. The enabled: true control and the mint-gate pin are untouched.
  • P3 — PR Hold publicSharing.eligibility at redemption, not only at mint #13857 form. Its changeset (.changeset/share-link-eligibility-at-redemption.md at fc9ba76a5) is minor + the **BREAKING** runtime behaviour change on a published package … Shipped as minor under the repo's launch-window convention banner + an ADR-0087 not-required (no-migration-prescription) marker. This changeset copies that form. Hold publicSharing.eligibility at redemption, not only at mint #13857 answers the refusal with the undifferentiated null (over HTTP the generic 404 INVALID_OR_EXPIRED), reason to the server-side log at warn — the same family this refusal joins.
  • P4 — no new code. The refusal reuses the answer resolveToken already gives for revoked / expired / unknown / ineligible: null. No error code is minted, nothing under packages/spec/** is touched. check:dispatcher-error-vocabulary and check:error-code-casing green (below); a check:error-code-provenance script does not exist in package.json — reported as such, not as measured.
  • P5.content/docs/releases/**, rule-hooks.ts, sharing-plugin.ts, skills/**, packages/spec/** untouched; the redactFields logic from fix(plugin-sharing): declared publicSharing.redactFields survive the object opting out #14171 is untouched (getPolicy unchanged).

PM mechanism hypotheses — verdicts

  • H1 holds. One gate in resolveToken, at the existing policy read (after the cheap revoked / expired / audience / password gates, BEFORE loadRecordForServing, BEFORE the usage stamp, BEFORE the redaction set): if (!policy.enabled) { log; return null; }. It reads the object's CURRENT block through the same getPolicy / getSchema source and nothing about how the row was minted. Measured: on a refused redemption the only find the engine sees is the sys_share_link token lookup; use_count / last_used_at are unchanged (fake engine and real SqlDriver alike).
  • H2 holds. The code is the family the anonymous caller already receives for a link that does not serve: null at the service seam, 404 INVALID_OR_EXPIRED over HTTP, byte-for-byte equal to an unknown token (pinned at the route). SHARING_NOT_ENABLED stays the authoring-side mint refusal (422, pinned unchanged) and appears at redemption only as the reason field of the server-side log line, never on the wire (pinned: the body contains no enabled, publicsharing or sharing_not_enabled).
  • H3 holds. With the block off, eligibility is not evaluated and the redaction set is not computed (the gate returns first; pinned: a record the predicate WOULD refuse is refused by the switch with no record read and a SHARING_NOT_ENABLED log line, and switching back on the same token is refused by the predicate — RECORD_NOT_ELIGIBLE — then serves once it qualifies). With the block on, Hold publicSharing.eligibility at redemption, not only at mint #13857's 12 redemption pins and fix(plugin-sharing): declared publicSharing.redactFields survive the object opting out #14171's enabled: true control are unchanged and green. No new code path for any sibling key.
  • H4 holds. The gate reads the current block on every call; no migration, no data change; the changeset's breaking-runtime-change banner is the only artefact.
  • H5 holds.share-link-routes.ts untouched: null is already mapped (401 for a live password / signed-in row exactly as today, 410 for revoked / expired, else 404 INVALID_OR_EXPIRED). The dispatcher twin (runtime/src/domains/share-links.ts) maps null the same way.

What changed

packages/plugins/plugin-sharing/src/share-link-service.ts

  • resolveToken(): the standing-policy gate described under H1, with a docblock carrying the why, the placement argument, ruling point 3 (no mark of how a row was minted is read — an absent block is the same switch at its default and refuses too) and point 4.
  • ShareLinkServiceOptions.permissive TSDoc: states the bypass is MINT only and that a link minted under it while the block is off does not resolve until the block is enabled. This TSDoc grew five lines above the five isSystem read sites row 37 anchors — see the adjacent mechanical fix below.

packages/plugins/plugin-sharing/src/share-link-service.test.ts — the reversal register for the #13856 block (four pins reversed in place, the ruling quoted verbatim once in the block header), the two harness helpers hoisted to module scope so both blocks share them, and the [#14033] block: the repro, the standing-policy restore, the shared-null shape, and ruling point 3 on three mint paths (permissive, system context, never-declared block).

packages/plugins/plugin-sharing/src/share-link-eligibility.test.ts — the [#14033] block on the real SqlDriver and the real public route: no record read / no usage stamp, the HTTP-seam equality with an unknown token, the log line, ruling point 4 both halves on one token, and the permissive-minted link refused by the bypassing service and the ordinary one alike.

.changeset/share-link-enabled-at-redemption.md@objectstack/plugin-sharing: minor, in PR #13857's form (see "Changeset level" below).

content/docs/permissions/system-context.mdx — row 37 re-anchored by check:system-context-census --fix (adjacent mechanical fix, declared here; see below) AND its "What you get" cell hand-corrected (one cell, seat-approved in round 0): the bypass buys link creation only; resolution is not bypassed since #14033.

Clause-② declaration (from the actual diff)

git diff -U0 origin/main...HEAD | grep export on the branch: zero lines. No export is added, removed or re-shaped. The accept-set narrowing is behavioural, on the anonymous redemption door: tokens on an object whose publicSharing.enabled is not true — accepted today, refused after, retroactively on deploy, for every mint path. needs:contract-review is hung on this PR and on #14033 per the claim. Hand-edited prose in this PR beyond the code and tests: exactly one cell — the "What you get" cell of content/docs/permissions/system-context.mdx row 37 (seat-approved widening of the claimed file surface, round 0); every other change to that page is the gate's own re-anchoring.

Reading the contract review may want to judge: the gate treats an object with NO publicSharing block as switched off (it is the same switch at its default — enabled defaults to false; createLink already refuses both identically), so a bypass-minted link on a never-opted-in object refuses too. Ruling point 3 is read as requiring exactly this; exempting block-less objects would leave the row-37 path open on every object that never opted in.

Log level

One new log site, warn, through the sink's GUARANTEED member (ShareLinkServiceOptions.logger.warn), spelled this.logger?.warn?.(…) exactly like stillEligible's refusal line. Why a log at all: by ruling the wire answer is deliberately undifferentiated, so the server-side log is the ONLY place the reason a link died exists — #13857's ruling put it there and the same argument holds for the parent switch. Why not error: the caller was answered (null), so this is a response, not a durability degradation; no error? growth on the published sink shape (#13398 class ruling respected). check:durability-log-level and check:optional-error-sink green.

Adjacent mechanical fix — system-context row 37 re-anchor (+ the merge regeneration)

check:system-context-census went red on this branch with exactly the 10 problems the permissive TSDoc edit predicts: 5 [site-without-a-row] reads at :445 :499 :503 :576 :606 plus the 5 [anchor-is-not-a-read-site] citations they rotted off (:440 :494 :498 :571 :601). Remedy: the gate's own --fix (5 anchors rewritten, all +5; 109 read sites / 145 anchors unchanged — no site arrived or vanished). The pre-PR merge-tree check then listed the same page as driver-deferred against a moved origin/main, so scripts/pm/os-regen-merge.sh was run and the page regenerated as the driver instructs; see the commit list.

Commits on the branch (squash-merged as one): 1925bfafc the gate + pins + changeset · a4f128f6b census re-anchor (--fix) · 1162f24df merge of origin/main (7286dd58e) via os-regen-merge.sh · a52cfa363 census regeneration on the merged tree (pnpm gen:system-context-census). Final head: a52cfa363. The union of gates, both pin files (Tests 76 passed (76)) and the package typecheck were re-run on that head; the readings above are from those runs.

Tests and ablation (ran on the head named in each line)

  • Pre-fix measurement on BASE 53d368921 + the new pins: Tests 15 failed | 61 passed (76) — all 15 expected { link: … } to be null.
  • Post-fix, head a52cfa363: pnpm --filter @objectstack/plugin-sharing exec vitest run --maxWorkers=2 src/share-link-service.test.ts src/share-link-eligibility.test.tsTest Files 2 passed (2) / Tests 76 passed (76).
  • pnpm --filter @objectstack/plugin-sharing typecheck (tsc + tsconfig.scripts + check:test-typecheck) → exit 0; check:test-typecheck: OK — … 2 file(s) / 3 error(s) / 3 pinned signature(s) held (ledger unchanged; the edited test files carry no debt entry, so they are compiled and clean).
  • Ablation on the committed tree (a4f128f6b, before the main merge; the gate is byte-identical after it): the gate's condition replaced by false with a marker, proven on disk by anchored counts (if (!policy.enabled) { 1→0, marker 0→1, git diff --stat 1 file 1+/1−). The subject is imported from source (./share-link-service.js, same package — not a dependency's exports), so no dist is involved and no rebuild leg applies. Ablated run: Tests 15 failed | 61 passed (76) — exactly the 15 pins the gate protects went red; the 61 others (all Hold publicSharing.eligibility at redemption, not only at mint #13857 eligibility pins, the mint-gate pin, the enabled: true control) stayed green. Restore: git checkout HEAD -- ABSOLUTE_PATH, proven by empty git diff HEAD + clean status + equal blob hashes (d00a6e53… both sides). Direction note vs the dispatch template: pin (b) (turn the block back ON restores the token) went RED under ablation, not green — it asserts the OFF refusal first, which the ablated tree cannot produce; the ON half is unreachable there. (d) and (e) stayed green as predicted.

Gates (derived on the final head with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands, run unlocked in four batches, exit captured before any pipe)

Green (exit 0): 61 commands — node scripts/check-adr-0087-registration.mjs · node scripts/check-changeset-no-major.mjs · node scripts/check-ci-filter-parity.mjs · node scripts/check-comment-mask-adoption.mjs · node scripts/check-cross-package-test-inputs.mjs · node scripts/check-doc-frontmatter.mjs · node scripts/check-doc-route-spelling.mjs · node scripts/check-docs-section-name.mjs · node scripts/check-empty-changeset.mjs · node scripts/check-keyed-text-bounds.mjs · node scripts/check-plugin-teardown-shape.mjs · node scripts/check-section-landing-index.mjs · node scripts/check-shard-attestation.mjs · node scripts/check-system-context-census.mjs · node scripts/check-tenant-audit-census.mjs · node scripts/check-undeclared-dep-imports.mjs · node scripts/docs-audit/check-affected-docs.mjs · node scripts/docs-audit/check-drift-comment.mjs · node scripts/pm/check-half-states.mjs · node scripts/pm/release-rehearsal-clone.mjs --self-test · pnpm --filter @objectstack/lint run check:doc-formula-expressions · pnpm --filter @objectstack/lint run check:doc-security-posture · pnpm --filter @objectstack/spec run check:docs · pnpm --filter @objectstack/spec run check:empty-state · pnpm --filter @objectstack/spec run check:liveness · pnpm --filter @objectstack/spec run check:strictness-ledger · pnpm --filter @objectstack/spec run check:variant-docs · pnpm --filter @objectstack/spec run check:yaml-examples · pnpm check:changeset-gate-self-tests · pnpm check:corpus-claim-drift · pnpm check:cross-package-test-inputs · pnpm check:doc-anchors · pnpm check:doc-authoring · pnpm check:docs-audit-scope · pnpm check:docs-redirects · pnpm check:docs-single-h1 · pnpm check:engine-double-contract · pnpm check:i18n-stale-fill · pnpm check:logger-receiver-detach · pnpm check:merge-driver · pnpm check:objectql-double-limit · pnpm check:objectui-changeset · pnpm check:page-declaration-shape · pnpm check:pm-half-states · pnpm check:published-files · pnpm check:published-readme-links · pnpm check:query-options-erasure · pnpm check:react-page-adapter-contract · pnpm check:role-word · pnpm check:skill-identifier-liveness · pnpm check:slot-lookup · pnpm check:test-source-alias · pnpm check:type-check-coverage · pnpm check:type-source-resolution · pnpm check:vendor-version-stamps · pnpm check:where-matcher · pnpm check:dispatcher-error-vocabulary · pnpm check:error-code-casing · pnpm check:durability-log-level · pnpm check:optional-error-sink · pnpm check:nul-bytes

NOT MEASURED — each exited on its own stated prerequisite, none is a red:

  • node scripts/check-test-completeness.mjs — exit 3: needs a saved test-run log; the gate's own text says the local reading is NOT MEASURED.
  • pnpm check:dual-build-cjs-loads — exit 3 PREREQUISITE NOT MET: reads built output of 54 packages that have no dist/ here.
  • pnpm check:i18n — exit 1 PREREQUISITE NOT MET: the workspace CLI is not built. This diff touches no translation bundle or object metadata.
  • pnpm --filter @objectstack/spec run check:skill-examples — exit 1: packages/client-react/dist holds no .d.ts (not built). This diff touches no skill, doc example or client SDK.
  • pnpm check:type-check-debt — exit 3 PREREQUISITE NOT MET: 40 workspace dependencies of the ledgered packages are unbuilt; no ledger number was touched.
  • check:error-code-provenance — no such script in package.json.

Repo-wide pnpm lint not run locally (CI-owned); the diff is three TypeScript files in one package plus a changeset and a regenerated page.

Changeset level

minor, not patch, deliberately: scripts/check-changeset-no-major.mjs records the launch-window convention (a breaking change ships as minor; the BREAKING banner and the ADR-0087 disposition are the carriers), and the ruling says 「与 #13857 同型」 — #13857 is minor with that banner. No export changed; the level carries the breaking banner, not a surface change. ADR-0087 disposition: not-required (no-migration-prescription) — nothing authorable is removed, renamed or re-shaped; check:adr-0087-registration and check:changeset-no-major green.

Known stale prose deliberately NOT edited here (file surface per the claim) — flagged for the PM

  1. DONE in this PR (patch round 0, seat answer A on open question 1).content/docs/permissions/system-context.mdx row 37, "What you get" column used to read "link creation/resolution while the policy is off". After this change the bypass buys creation only; resolution is refused. The page is in this PR only through the census gate's own regeneration (the claim's constraint), so the one-cell correction is proposed, not made: "Get: link creation while the policy is off — resolution is NOT bypassed since [Decision] What does turning publicSharing.enabled off mean for an ALREADY-MINTED share link? — the parent switch is mint-only while its own child predicate is now a standing policy #14033: a link minted this way does not resolve until the block is enabled".
  2. content/docs/protocol/objectql/security.mdx "When eligibility is enforced" — the enabled switch now deserves the sibling paragraph and upgrade note Hold publicSharing.eligibility at redemption, not only at mint #13857 wrote for eligibility.
  3. packages/spec/src/contracts/share-link-service.tsIShareLinkService.resolveToken TSDoc lists the null causes and should name the switched-off block; packages/spec/src/data/object.zod.tspublicSharing.enabled TSDoc says "no share links can be issued" and should add "and none resolves". TSDoc only, no schema change — outside this card's surface (packages/spec/** is a stop condition for schema changes; these are prose).

Generated by Claude Code


Patch round 0 (after the seat's collection comment 14033#issuecomment-5511082583)

Generated by Claude Code


Patch round 1 (after contract review round 1 PASS, 14033#issuecomment-5511376156 — the four prose-only notes taken)

Generated by Claude Code


Generated by Claude Code

…ly at mint
`ShareLinkService.resolveToken()` now reads the object's CURRENT
`publicSharing` block on every redemption and refuses when `enabled` is
not `true` — before the record probe, before the usage stamp, before any
sibling key inside the block is evaluated. Re-enabling the block restores
the same tokens (a standing policy, not a revocation). How a link was
minted buys it nothing at redemption: rows minted under a system context
or the `permissive` bypass on a switched-off object refuse the same way,
and an object with no block at all is the same switch at its default.
The refusal reuses the undifferentiated `null` a revoked / expired /
unknown / ineligible token already gets — no new error code, no new
response branch, no usage stamp (over HTTP the generic 404). The readable
reason goes to the server-side log at `warn`, where the eligibility
refusal already writes its own.
Tests: the standing-policy pins (fake engine and real driver), the
HTTP-seam shape, the log line, the bypass-minted path, and the reversal
register for the #13856 pins that asserted serving on a disabled block.
Changeset marked as a breaking runtime change in PR #13857's form.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
…nk hunk shifted it
`check:system-context-census --fix` — pure line rot: the `permissive`
option's TSDoc grew five lines above the five `isSystem` read sites row 37
anchors in `share-link-service.ts`, so every anchor moved by exactly +5.
No site arrived or vanished (109 read sites, 145 anchors, unchanged).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
…gin/main
`pnpm gen:system-context-census` on the merged tree (os-regen-merge.sh step 4):
main's side of the page was taken at merge, then row 37's anchors re-derived
from the merged `share-link-service.ts` (+5, the `permissive` TSDoc). No read
site arrived or vanished.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
@github-actionsgithub-actionsBot added size/l documentation Improvements or additions to documentation tests tooling labels Sep 2, 2026
@github-actions

github-actionsBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/plugin-sharing, touching 3 documentable anchor(s).

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/kernel/runtime-services/sharing-service.mdx(via SHARING_NOT_ENABLED (literal, a string literal in resolveToken))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17.mdx(via SHARING_NOT_ENABLED (literal, a string literal in resolveToken))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 8 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json a5b95ee6edd1e21e04cc43200f41fb730538b197packageMentionDocs.

Which tree this was computed on

This run read content/docs from 114b3f22a35cc3c3d766b6dd3a6b69e3b9b98810 — the merge of head c55d849d768e09bb6d6a48973995b044bdc38861 into base a5b95ee6edd1e21e04cc43200f41fb730538b197, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 114b3f22a35cc3c3d766b6dd3a6b69e3b9b98810 && git checkout 114b3f22a35cc3c3d766b6dd3a6b69e3b9b98810
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin a5b95ee6edd1e21e04cc43200f41fb730538b197 c55d849d768e09bb6d6a48973995b044bdc38861 && git checkout -B drift-repro a5b95ee6edd1e21e04cc43200f41fb730538b197 && git merge --no-ff c55d849d768e09bb6d6a48973995b044bdc38861
node scripts/docs-audit/affected-docs.mjs --json a5b95ee6edd1e21e04cc43200f41fb730538b197

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs a5b95ee6edd1e21e04cc43200f41fb730538b197 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

… resolution is a standing policy
The "What you get" cell of the system-context ledger's row 37 still said
"link creation/resolution while the policy is off". Since the redemption
gate on `publicSharing.enabled`, a link minted under the system /
`permissive` bypass while the block is off does not resolve until the block
is enabled. Prose only; the five anchors are untouched (seat-approved
one-cell widening of the claim's file surface, 14033#issuecomment-5511082583).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
…gin/main (round 0)
os-regen-merge.sh step 4 on the merged tree: main's side of the page was
taken at merge, the row-37 "What you get" cell re-applied (a hand edit no
generator reproduces), then `pnpm gen:system-context-census`. Anchors
unchanged; no read site arrived or vanished.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
…, one permissive TSDoc clause, row 37 re-anchored
Prose only; the gate, the tests and the changeset level are untouched.
Changeset: the superseded #13856 reading is named; an object the engine
cannot return a schema for is `enabled: false` by `getPolicy`'s definition
and refuses at redemption (fail-closed, as `createLink`); the refusal logs
one `warn` per refused hit, not latched. `permissive` TSDoc: during a late
schema scan the option helps minting, not serving. The four added TSDoc
lines moved the five row-37 anchors, re-anchored by the census gate's own
`--fix`; the row-37 "What you get" cell is unchanged.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
…gin/main (round 1)
os-regen-merge.sh step 4 on the merged tree: main's side of the page taken
at merge, the row-37 "What you get" cell re-applied (a hand edit no
generator reproduces), then `pnpm gen:system-context-census`. No read site
arrived or vanished.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
@os-sales
os-sales marked this pull request as ready for review September 2, 2026 15:12
@os-salesClaude

Copy link
Copy Markdown
Collaborator

Landing provenance — ready + auto-merge at head c55d849d7


Generated by Claude Code

@os-sales
os-sales deleted the claude/issue-14033-public-sharing-enabled-standing-policy branch September 2, 2026 16:12
baozhoutao pushed a commit that referenced this pull request Sep 2, 2026
Item 2 of #14582 (follow-up to #14033 / PR #14580, split from #14033's
docs surfaces that PR #14580 could not touch). The "Public Share Links"
section documented the child predicate (eligibility, #13608) as a
standing policy held at every redemption, but not the parent switch
publicSharing.enabled — #14033 made that switch the same shape: held at
every redemption, not only at mint. Adds the sibling paragraph plus its
upgrade-note callout, placed after the eligibility paragraph so a reader
who has already understood the child predicate recognises the parent's
paragraph as the same shape one level up.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WLJQhde67SeTccsmnBVarV
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

2 participants

@os-sales@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix(plugin-sharing): hold publicSharing.enabled at redemption, not only at mint (#14033) - #14580

Merged
os-sales merged 10 commits into
mainfrom
claude/issue-14033-public-sharing-enabled-standing-policy
Sep 2, 2026
Merged

fix(plugin-sharing): hold publicSharing.enabled at redemption, not only at mint (#14033)#14580
os-sales merged 10 commits into
mainfrom
claude/issue-14033-public-sharing-enabled-standing-policy

Conversation

@claude

@claudeclaudeBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Fixes#14033

Ruling of record (verbatim, untranslated)

Maintainer ruling of 2026-09-01, recorded by the director seat in 14033#issuecomment-5491054105 (maintainer's verbatim reply to the batch: 「其他同意」). The recorded four points, verbatim:

  1. A:publicSharing.enabled 为持续策略,resolveToken() 每次兑付重查;关掉 ⇒ 存量链接停止兑付。
  2. 追溯即时生效(沿 sharing: publicSharing.eligibility is evaluated only at mint — a link keeps serving a record after it stops being eligible #13608 先例):部署一落地,禁用块上的所有既有 token 即停止解析。changeset 必须标注 breaking runtime change,与 Hold publicSharing.eligibility at redemption, not only at mint #13857 同型。
  3. 系统/宽容旁路铸造的链接同样受管:兑付是匿名动作,不因铸造方式豁免 —— enabled 关则一律拒付(含 system-context.mdx ledger row 37 的旁路铸造路径)。
  4. 兄弟键统一律入册:enabled 关 ⇒ 整块不生效 ⇒ 拒付(子键求值 moot);enabled 开 ⇒ 块内各策略键按持续策略在兑付期求值(eligibility 已然,redactFieldssharing: turning publicSharing off leaves already-minted links serving — and silently drops the object's redactFields #13856 修复)。后续兄弟键⛔ 不再单独立卡。

And its implementation notes, verbatim:

Premise verification on origin/main (BASE 53d368921) — all five hold

  • P1 — measured, not read. The first pin (THE REPRO — minted while enabled, the SAME token stops resolving the moment the block is turned off) was run against the UNMODIFIED service before any edit: AssertionError: expected { link: { …(16) }, …(1) } to be null — the token kept serving with the block off. 15 such reds (every new/reversed pin), 61 existing pins green. Log: pre-fix run, Tests 15 failed | 61 passed (76). Premise valid.
  • P2 — PR fix(plugin-sharing): declared publicSharing.redactFields survive the object opting out #14171 (sharing: turning publicSharing off leaves already-minted links serving — and silently drops the object's redactFields #13856) pins. Two pins asserted a link on a DISABLED block still serves with redactions applied (THE REPRO — opting out keeps the declared redactions applying, boundary — the per-link redact_fields union is unchanged when the switch is off), and two reverse controls asserted a bypass-seeded link on a block-LESS object serves. All four are REVERSED in place and registered (docblock naming what each pinned, which ruling point moved it, the ruling verbatim in the block header); none deleted. The pins are agent-authored test content in share-link-service.test.ts, not governed content. The enabled: true control and the mint-gate pin are untouched.
  • P3 — PR Hold publicSharing.eligibility at redemption, not only at mint #13857 form. Its changeset (.changeset/share-link-eligibility-at-redemption.md at fc9ba76a5) is minor + the **BREAKING** runtime behaviour change on a published package … Shipped as minor under the repo's launch-window convention banner + an ADR-0087 not-required (no-migration-prescription) marker. This changeset copies that form. Hold publicSharing.eligibility at redemption, not only at mint #13857 answers the refusal with the undifferentiated null (over HTTP the generic 404 INVALID_OR_EXPIRED), reason to the server-side log at warn — the same family this refusal joins.
  • P4 — no new code. The refusal reuses the answer resolveToken already gives for revoked / expired / unknown / ineligible: null. No error code is minted, nothing under packages/spec/** is touched. check:dispatcher-error-vocabulary and check:error-code-casing green (below); a check:error-code-provenance script does not exist in package.json — reported as such, not as measured.
  • P5.content/docs/releases/**, rule-hooks.ts, sharing-plugin.ts, skills/**, packages/spec/** untouched; the redactFields logic from fix(plugin-sharing): declared publicSharing.redactFields survive the object opting out #14171 is untouched (getPolicy unchanged).

PM mechanism hypotheses — verdicts

  • H1 holds. One gate in resolveToken, at the existing policy read (after the cheap revoked / expired / audience / password gates, BEFORE loadRecordForServing, BEFORE the usage stamp, BEFORE the redaction set): if (!policy.enabled) { log; return null; }. It reads the object's CURRENT block through the same getPolicy / getSchema source and nothing about how the row was minted. Measured: on a refused redemption the only find the engine sees is the sys_share_link token lookup; use_count / last_used_at are unchanged (fake engine and real SqlDriver alike).
  • H2 holds. The code is the family the anonymous caller already receives for a link that does not serve: null at the service seam, 404 INVALID_OR_EXPIRED over HTTP, byte-for-byte equal to an unknown token (pinned at the route). SHARING_NOT_ENABLED stays the authoring-side mint refusal (422, pinned unchanged) and appears at redemption only as the reason field of the server-side log line, never on the wire (pinned: the body contains no enabled, publicsharing or sharing_not_enabled).
  • H3 holds. With the block off, eligibility is not evaluated and the redaction set is not computed (the gate returns first; pinned: a record the predicate WOULD refuse is refused by the switch with no record read and a SHARING_NOT_ENABLED log line, and switching back on the same token is refused by the predicate — RECORD_NOT_ELIGIBLE — then serves once it qualifies). With the block on, Hold publicSharing.eligibility at redemption, not only at mint #13857's 12 redemption pins and fix(plugin-sharing): declared publicSharing.redactFields survive the object opting out #14171's enabled: true control are unchanged and green. No new code path for any sibling key.
  • H4 holds. The gate reads the current block on every call; no migration, no data change; the changeset's breaking-runtime-change banner is the only artefact.
  • H5 holds.share-link-routes.ts untouched: null is already mapped (401 for a live password / signed-in row exactly as today, 410 for revoked / expired, else 404 INVALID_OR_EXPIRED). The dispatcher twin (runtime/src/domains/share-links.ts) maps null the same way.

What changed

packages/plugins/plugin-sharing/src/share-link-service.ts

  • resolveToken(): the standing-policy gate described under H1, with a docblock carrying the why, the placement argument, ruling point 3 (no mark of how a row was minted is read — an absent block is the same switch at its default and refuses too) and point 4.
  • ShareLinkServiceOptions.permissive TSDoc: states the bypass is MINT only and that a link minted under it while the block is off does not resolve until the block is enabled. This TSDoc grew five lines above the five isSystem read sites row 37 anchors — see the adjacent mechanical fix below.

packages/plugins/plugin-sharing/src/share-link-service.test.ts — the reversal register for the #13856 block (four pins reversed in place, the ruling quoted verbatim once in the block header), the two harness helpers hoisted to module scope so both blocks share them, and the [#14033] block: the repro, the standing-policy restore, the shared-null shape, and ruling point 3 on three mint paths (permissive, system context, never-declared block).

packages/plugins/plugin-sharing/src/share-link-eligibility.test.ts — the [#14033] block on the real SqlDriver and the real public route: no record read / no usage stamp, the HTTP-seam equality with an unknown token, the log line, ruling point 4 both halves on one token, and the permissive-minted link refused by the bypassing service and the ordinary one alike.

.changeset/share-link-enabled-at-redemption.md@objectstack/plugin-sharing: minor, in PR #13857's form (see "Changeset level" below).

content/docs/permissions/system-context.mdx — row 37 re-anchored by check:system-context-census --fix (adjacent mechanical fix, declared here; see below) AND its "What you get" cell hand-corrected (one cell, seat-approved in round 0): the bypass buys link creation only; resolution is not bypassed since #14033.

Clause-② declaration (from the actual diff)

git diff -U0 origin/main...HEAD | grep export on the branch: zero lines. No export is added, removed or re-shaped. The accept-set narrowing is behavioural, on the anonymous redemption door: tokens on an object whose publicSharing.enabled is not true — accepted today, refused after, retroactively on deploy, for every mint path. needs:contract-review is hung on this PR and on #14033 per the claim. Hand-edited prose in this PR beyond the code and tests: exactly one cell — the "What you get" cell of content/docs/permissions/system-context.mdx row 37 (seat-approved widening of the claimed file surface, round 0); every other change to that page is the gate's own re-anchoring.

Reading the contract review may want to judge: the gate treats an object with NO publicSharing block as switched off (it is the same switch at its default — enabled defaults to false; createLink already refuses both identically), so a bypass-minted link on a never-opted-in object refuses too. Ruling point 3 is read as requiring exactly this; exempting block-less objects would leave the row-37 path open on every object that never opted in.

Log level

One new log site, warn, through the sink's GUARANTEED member (ShareLinkServiceOptions.logger.warn), spelled this.logger?.warn?.(…) exactly like stillEligible's refusal line. Why a log at all: by ruling the wire answer is deliberately undifferentiated, so the server-side log is the ONLY place the reason a link died exists — #13857's ruling put it there and the same argument holds for the parent switch. Why not error: the caller was answered (null), so this is a response, not a durability degradation; no error? growth on the published sink shape (#13398 class ruling respected). check:durability-log-level and check:optional-error-sink green.

Adjacent mechanical fix — system-context row 37 re-anchor (+ the merge regeneration)

check:system-context-census went red on this branch with exactly the 10 problems the permissive TSDoc edit predicts: 5 [site-without-a-row] reads at :445 :499 :503 :576 :606 plus the 5 [anchor-is-not-a-read-site] citations they rotted off (:440 :494 :498 :571 :601). Remedy: the gate's own --fix (5 anchors rewritten, all +5; 109 read sites / 145 anchors unchanged — no site arrived or vanished). The pre-PR merge-tree check then listed the same page as driver-deferred against a moved origin/main, so scripts/pm/os-regen-merge.sh was run and the page regenerated as the driver instructs; see the commit list.

Commits on the branch (squash-merged as one): 1925bfafc the gate + pins + changeset · a4f128f6b census re-anchor (--fix) · 1162f24df merge of origin/main (7286dd58e) via os-regen-merge.sh · a52cfa363 census regeneration on the merged tree (pnpm gen:system-context-census). Final head: a52cfa363. The union of gates, both pin files (Tests 76 passed (76)) and the package typecheck were re-run on that head; the readings above are from those runs.

Tests and ablation (ran on the head named in each line)

  • Pre-fix measurement on BASE 53d368921 + the new pins: Tests 15 failed | 61 passed (76) — all 15 expected { link: … } to be null.
  • Post-fix, head a52cfa363: pnpm --filter @objectstack/plugin-sharing exec vitest run --maxWorkers=2 src/share-link-service.test.ts src/share-link-eligibility.test.tsTest Files 2 passed (2) / Tests 76 passed (76).
  • pnpm --filter @objectstack/plugin-sharing typecheck (tsc + tsconfig.scripts + check:test-typecheck) → exit 0; check:test-typecheck: OK — … 2 file(s) / 3 error(s) / 3 pinned signature(s) held (ledger unchanged; the edited test files carry no debt entry, so they are compiled and clean).
  • Ablation on the committed tree (a4f128f6b, before the main merge; the gate is byte-identical after it): the gate's condition replaced by false with a marker, proven on disk by anchored counts (if (!policy.enabled) { 1→0, marker 0→1, git diff --stat 1 file 1+/1−). The subject is imported from source (./share-link-service.js, same package — not a dependency's exports), so no dist is involved and no rebuild leg applies. Ablated run: Tests 15 failed | 61 passed (76) — exactly the 15 pins the gate protects went red; the 61 others (all Hold publicSharing.eligibility at redemption, not only at mint #13857 eligibility pins, the mint-gate pin, the enabled: true control) stayed green. Restore: git checkout HEAD -- ABSOLUTE_PATH, proven by empty git diff HEAD + clean status + equal blob hashes (d00a6e53… both sides). Direction note vs the dispatch template: pin (b) (turn the block back ON restores the token) went RED under ablation, not green — it asserts the OFF refusal first, which the ablated tree cannot produce; the ON half is unreachable there. (d) and (e) stayed green as predicted.

Gates (derived on the final head with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands, run unlocked in four batches, exit captured before any pipe)

Green (exit 0): 61 commands — node scripts/check-adr-0087-registration.mjs · node scripts/check-changeset-no-major.mjs · node scripts/check-ci-filter-parity.mjs · node scripts/check-comment-mask-adoption.mjs · node scripts/check-cross-package-test-inputs.mjs · node scripts/check-doc-frontmatter.mjs · node scripts/check-doc-route-spelling.mjs · node scripts/check-docs-section-name.mjs · node scripts/check-empty-changeset.mjs · node scripts/check-keyed-text-bounds.mjs · node scripts/check-plugin-teardown-shape.mjs · node scripts/check-section-landing-index.mjs · node scripts/check-shard-attestation.mjs · node scripts/check-system-context-census.mjs · node scripts/check-tenant-audit-census.mjs · node scripts/check-undeclared-dep-imports.mjs · node scripts/docs-audit/check-affected-docs.mjs · node scripts/docs-audit/check-drift-comment.mjs · node scripts/pm/check-half-states.mjs · node scripts/pm/release-rehearsal-clone.mjs --self-test · pnpm --filter @objectstack/lint run check:doc-formula-expressions · pnpm --filter @objectstack/lint run check:doc-security-posture · pnpm --filter @objectstack/spec run check:docs · pnpm --filter @objectstack/spec run check:empty-state · pnpm --filter @objectstack/spec run check:liveness · pnpm --filter @objectstack/spec run check:strictness-ledger · pnpm --filter @objectstack/spec run check:variant-docs · pnpm --filter @objectstack/spec run check:yaml-examples · pnpm check:changeset-gate-self-tests · pnpm check:corpus-claim-drift · pnpm check:cross-package-test-inputs · pnpm check:doc-anchors · pnpm check:doc-authoring · pnpm check:docs-audit-scope · pnpm check:docs-redirects · pnpm check:docs-single-h1 · pnpm check:engine-double-contract · pnpm check:i18n-stale-fill · pnpm check:logger-receiver-detach · pnpm check:merge-driver · pnpm check:objectql-double-limit · pnpm check:objectui-changeset · pnpm check:page-declaration-shape · pnpm check:pm-half-states · pnpm check:published-files · pnpm check:published-readme-links · pnpm check:query-options-erasure · pnpm check:react-page-adapter-contract · pnpm check:role-word · pnpm check:skill-identifier-liveness · pnpm check:slot-lookup · pnpm check:test-source-alias · pnpm check:type-check-coverage · pnpm check:type-source-resolution · pnpm check:vendor-version-stamps · pnpm check:where-matcher · pnpm check:dispatcher-error-vocabulary · pnpm check:error-code-casing · pnpm check:durability-log-level · pnpm check:optional-error-sink · pnpm check:nul-bytes

NOT MEASURED — each exited on its own stated prerequisite, none is a red:

  • node scripts/check-test-completeness.mjs — exit 3: needs a saved test-run log; the gate's own text says the local reading is NOT MEASURED.
  • pnpm check:dual-build-cjs-loads — exit 3 PREREQUISITE NOT MET: reads built output of 54 packages that have no dist/ here.
  • pnpm check:i18n — exit 1 PREREQUISITE NOT MET: the workspace CLI is not built. This diff touches no translation bundle or object metadata.
  • pnpm --filter @objectstack/spec run check:skill-examples — exit 1: packages/client-react/dist holds no .d.ts (not built). This diff touches no skill, doc example or client SDK.
  • pnpm check:type-check-debt — exit 3 PREREQUISITE NOT MET: 40 workspace dependencies of the ledgered packages are unbuilt; no ledger number was touched.
  • check:error-code-provenance — no such script in package.json.

Repo-wide pnpm lint not run locally (CI-owned); the diff is three TypeScript files in one package plus a changeset and a regenerated page.

Changeset level

minor, not patch, deliberately: scripts/check-changeset-no-major.mjs records the launch-window convention (a breaking change ships as minor; the BREAKING banner and the ADR-0087 disposition are the carriers), and the ruling says 「与 #13857 同型」 — #13857 is minor with that banner. No export changed; the level carries the breaking banner, not a surface change. ADR-0087 disposition: not-required (no-migration-prescription) — nothing authorable is removed, renamed or re-shaped; check:adr-0087-registration and check:changeset-no-major green.

Known stale prose deliberately NOT edited here (file surface per the claim) — flagged for the PM

  1. DONE in this PR (patch round 0, seat answer A on open question 1).content/docs/permissions/system-context.mdx row 37, "What you get" column used to read "link creation/resolution while the policy is off". After this change the bypass buys creation only; resolution is refused. The page is in this PR only through the census gate's own regeneration (the claim's constraint), so the one-cell correction is proposed, not made: "Get: link creation while the policy is off — resolution is NOT bypassed since [Decision] What does turning publicSharing.enabled off mean for an ALREADY-MINTED share link? — the parent switch is mint-only while its own child predicate is now a standing policy #14033: a link minted this way does not resolve until the block is enabled".
  2. content/docs/protocol/objectql/security.mdx "When eligibility is enforced" — the enabled switch now deserves the sibling paragraph and upgrade note Hold publicSharing.eligibility at redemption, not only at mint #13857 wrote for eligibility.
  3. packages/spec/src/contracts/share-link-service.tsIShareLinkService.resolveToken TSDoc lists the null causes and should name the switched-off block; packages/spec/src/data/object.zod.tspublicSharing.enabled TSDoc says "no share links can be issued" and should add "and none resolves". TSDoc only, no schema change — outside this card's surface (packages/spec/** is a stop condition for schema changes; these are prose).

Generated by Claude Code


Patch round 0 (after the seat's collection comment 14033#issuecomment-5511082583)

Generated by Claude Code


Patch round 1 (after contract review round 1 PASS, 14033#issuecomment-5511376156 — the four prose-only notes taken)

Generated by Claude Code


Generated by Claude Code

…ly at mint
`ShareLinkService.resolveToken()` now reads the object's CURRENT
`publicSharing` block on every redemption and refuses when `enabled` is
not `true` — before the record probe, before the usage stamp, before any
sibling key inside the block is evaluated. Re-enabling the block restores
the same tokens (a standing policy, not a revocation). How a link was
minted buys it nothing at redemption: rows minted under a system context
or the `permissive` bypass on a switched-off object refuse the same way,
and an object with no block at all is the same switch at its default.
The refusal reuses the undifferentiated `null` a revoked / expired /
unknown / ineligible token already gets — no new error code, no new
response branch, no usage stamp (over HTTP the generic 404). The readable
reason goes to the server-side log at `warn`, where the eligibility
refusal already writes its own.
Tests: the standing-policy pins (fake engine and real driver), the
HTTP-seam shape, the log line, the bypass-minted path, and the reversal
register for the #13856 pins that asserted serving on a disabled block.
Changeset marked as a breaking runtime change in PR #13857's form.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
…nk hunk shifted it
`check:system-context-census --fix` — pure line rot: the `permissive`
option's TSDoc grew five lines above the five `isSystem` read sites row 37
anchors in `share-link-service.ts`, so every anchor moved by exactly +5.
No site arrived or vanished (109 read sites, 145 anchors, unchanged).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
…gin/main
`pnpm gen:system-context-census` on the merged tree (os-regen-merge.sh step 4):
main's side of the page was taken at merge, then row 37's anchors re-derived
from the merged `share-link-service.ts` (+5, the `permissive` TSDoc). No read
site arrived or vanished.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
@github-actionsgithub-actionsBot added size/l documentation Improvements or additions to documentation tests tooling labels Sep 2, 2026
@github-actions

github-actionsBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/plugin-sharing, touching 3 documentable anchor(s).

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/kernel/runtime-services/sharing-service.mdx(via SHARING_NOT_ENABLED (literal, a string literal in resolveToken))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17.mdx(via SHARING_NOT_ENABLED (literal, a string literal in resolveToken))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 8 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json a5b95ee6edd1e21e04cc43200f41fb730538b197packageMentionDocs.

Which tree this was computed on

This run read content/docs from 114b3f22a35cc3c3d766b6dd3a6b69e3b9b98810 — the merge of head c55d849d768e09bb6d6a48973995b044bdc38861 into base a5b95ee6edd1e21e04cc43200f41fb730538b197, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 114b3f22a35cc3c3d766b6dd3a6b69e3b9b98810 && git checkout 114b3f22a35cc3c3d766b6dd3a6b69e3b9b98810
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin a5b95ee6edd1e21e04cc43200f41fb730538b197 c55d849d768e09bb6d6a48973995b044bdc38861 && git checkout -B drift-repro a5b95ee6edd1e21e04cc43200f41fb730538b197 && git merge --no-ff c55d849d768e09bb6d6a48973995b044bdc38861
node scripts/docs-audit/affected-docs.mjs --json a5b95ee6edd1e21e04cc43200f41fb730538b197

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs a5b95ee6edd1e21e04cc43200f41fb730538b197 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

… resolution is a standing policy
The "What you get" cell of the system-context ledger's row 37 still said
"link creation/resolution while the policy is off". Since the redemption
gate on `publicSharing.enabled`, a link minted under the system /
`permissive` bypass while the block is off does not resolve until the block
is enabled. Prose only; the five anchors are untouched (seat-approved
one-cell widening of the claim's file surface, 14033#issuecomment-5511082583).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
…gin/main (round 0)
os-regen-merge.sh step 4 on the merged tree: main's side of the page was
taken at merge, the row-37 "What you get" cell re-applied (a hand edit no
generator reproduces), then `pnpm gen:system-context-census`. Anchors
unchanged; no read site arrived or vanished.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
…, one permissive TSDoc clause, row 37 re-anchored
Prose only; the gate, the tests and the changeset level are untouched.
Changeset: the superseded #13856 reading is named; an object the engine
cannot return a schema for is `enabled: false` by `getPolicy`'s definition
and refuses at redemption (fail-closed, as `createLink`); the refusal logs
one `warn` per refused hit, not latched. `permissive` TSDoc: during a late
schema scan the option helps minting, not serving. The four added TSDoc
lines moved the five row-37 anchors, re-anchored by the census gate's own
`--fix`; the row-37 "What you get" cell is unchanged.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
…gin/main (round 1)
os-regen-merge.sh step 4 on the merged tree: main's side of the page taken
at merge, the row-37 "What you get" cell re-applied (a hand edit no
generator reproduces), then `pnpm gen:system-context-census`. No read site
arrived or vanished.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
@os-sales
os-sales marked this pull request as ready for review September 2, 2026 15:12
@os-salesClaude

Copy link
Copy Markdown
Collaborator

Landing provenance — ready + auto-merge at head c55d849d7


Generated by Claude Code

@os-sales
os-sales deleted the claude/issue-14033-public-sharing-enabled-standing-policy branch September 2, 2026 16:12
baozhoutao pushed a commit that referenced this pull request Sep 2, 2026
Item 2 of #14582 (follow-up to #14033 / PR #14580, split from #14033's
docs surfaces that PR #14580 could not touch). The "Public Share Links"
section documented the child predicate (eligibility, #13608) as a
standing policy held at every redemption, but not the parent switch
publicSharing.enabled — #14033 made that switch the same shape: held at
every redemption, not only at mint. Adds the sibling paragraph plus its
upgrade-note callout, placed after the eligibility paragraph so a reader
who has already understood the child predicate recognises the parent's
paragraph as the same shape one level up.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WLJQhde67SeTccsmnBVarV
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

2 participants

@os-sales@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(plugin-sharing): hold publicSharing.enabled at redemption, not only at mint (#14033) - #14580

Merged
os-sales merged 10 commits into
mainfrom
claude/issue-14033-public-sharing-enabled-standing-policy
Sep 2, 2026
Merged

fix(plugin-sharing): hold publicSharing.enabled at redemption, not only at mint (#14033)#14580
os-sales merged 10 commits into
mainfrom
claude/issue-14033-public-sharing-enabled-standing-policy

Conversation

@claude

@claudeclaudeBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Fixes#14033

Ruling of record (verbatim, untranslated)

Maintainer ruling of 2026-09-01, recorded by the director seat in 14033#issuecomment-5491054105 (maintainer's verbatim reply to the batch: 「其他同意」). The recorded four points, verbatim:

  1. A:publicSharing.enabled 为持续策略,resolveToken() 每次兑付重查;关掉 ⇒ 存量链接停止兑付。
  2. 追溯即时生效(沿 sharing: publicSharing.eligibility is evaluated only at mint — a link keeps serving a record after it stops being eligible #13608 先例):部署一落地,禁用块上的所有既有 token 即停止解析。changeset 必须标注 breaking runtime change,与 Hold publicSharing.eligibility at redemption, not only at mint #13857 同型。
  3. 系统/宽容旁路铸造的链接同样受管:兑付是匿名动作,不因铸造方式豁免 —— enabled 关则一律拒付(含 system-context.mdx ledger row 37 的旁路铸造路径)。
  4. 兄弟键统一律入册:enabled 关 ⇒ 整块不生效 ⇒ 拒付(子键求值 moot);enabled 开 ⇒ 块内各策略键按持续策略在兑付期求值(eligibility 已然,redactFieldssharing: turning publicSharing off leaves already-minted links serving — and silently drops the object's redactFields #13856 修复)。后续兄弟键⛔ 不再单独立卡。

And its implementation notes, verbatim:

Premise verification on origin/main (BASE 53d368921) — all five hold

  • P1 — measured, not read. The first pin (THE REPRO — minted while enabled, the SAME token stops resolving the moment the block is turned off) was run against the UNMODIFIED service before any edit: AssertionError: expected { link: { …(16) }, …(1) } to be null — the token kept serving with the block off. 15 such reds (every new/reversed pin), 61 existing pins green. Log: pre-fix run, Tests 15 failed | 61 passed (76). Premise valid.
  • P2 — PR fix(plugin-sharing): declared publicSharing.redactFields survive the object opting out #14171 (sharing: turning publicSharing off leaves already-minted links serving — and silently drops the object's redactFields #13856) pins. Two pins asserted a link on a DISABLED block still serves with redactions applied (THE REPRO — opting out keeps the declared redactions applying, boundary — the per-link redact_fields union is unchanged when the switch is off), and two reverse controls asserted a bypass-seeded link on a block-LESS object serves. All four are REVERSED in place and registered (docblock naming what each pinned, which ruling point moved it, the ruling verbatim in the block header); none deleted. The pins are agent-authored test content in share-link-service.test.ts, not governed content. The enabled: true control and the mint-gate pin are untouched.
  • P3 — PR Hold publicSharing.eligibility at redemption, not only at mint #13857 form. Its changeset (.changeset/share-link-eligibility-at-redemption.md at fc9ba76a5) is minor + the **BREAKING** runtime behaviour change on a published package … Shipped as minor under the repo's launch-window convention banner + an ADR-0087 not-required (no-migration-prescription) marker. This changeset copies that form. Hold publicSharing.eligibility at redemption, not only at mint #13857 answers the refusal with the undifferentiated null (over HTTP the generic 404 INVALID_OR_EXPIRED), reason to the server-side log at warn — the same family this refusal joins.
  • P4 — no new code. The refusal reuses the answer resolveToken already gives for revoked / expired / unknown / ineligible: null. No error code is minted, nothing under packages/spec/** is touched. check:dispatcher-error-vocabulary and check:error-code-casing green (below); a check:error-code-provenance script does not exist in package.json — reported as such, not as measured.
  • P5.content/docs/releases/**, rule-hooks.ts, sharing-plugin.ts, skills/**, packages/spec/** untouched; the redactFields logic from fix(plugin-sharing): declared publicSharing.redactFields survive the object opting out #14171 is untouched (getPolicy unchanged).

PM mechanism hypotheses — verdicts

  • H1 holds. One gate in resolveToken, at the existing policy read (after the cheap revoked / expired / audience / password gates, BEFORE loadRecordForServing, BEFORE the usage stamp, BEFORE the redaction set): if (!policy.enabled) { log; return null; }. It reads the object's CURRENT block through the same getPolicy / getSchema source and nothing about how the row was minted. Measured: on a refused redemption the only find the engine sees is the sys_share_link token lookup; use_count / last_used_at are unchanged (fake engine and real SqlDriver alike).
  • H2 holds. The code is the family the anonymous caller already receives for a link that does not serve: null at the service seam, 404 INVALID_OR_EXPIRED over HTTP, byte-for-byte equal to an unknown token (pinned at the route). SHARING_NOT_ENABLED stays the authoring-side mint refusal (422, pinned unchanged) and appears at redemption only as the reason field of the server-side log line, never on the wire (pinned: the body contains no enabled, publicsharing or sharing_not_enabled).
  • H3 holds. With the block off, eligibility is not evaluated and the redaction set is not computed (the gate returns first; pinned: a record the predicate WOULD refuse is refused by the switch with no record read and a SHARING_NOT_ENABLED log line, and switching back on the same token is refused by the predicate — RECORD_NOT_ELIGIBLE — then serves once it qualifies). With the block on, Hold publicSharing.eligibility at redemption, not only at mint #13857's 12 redemption pins and fix(plugin-sharing): declared publicSharing.redactFields survive the object opting out #14171's enabled: true control are unchanged and green. No new code path for any sibling key.
  • H4 holds. The gate reads the current block on every call; no migration, no data change; the changeset's breaking-runtime-change banner is the only artefact.
  • H5 holds.share-link-routes.ts untouched: null is already mapped (401 for a live password / signed-in row exactly as today, 410 for revoked / expired, else 404 INVALID_OR_EXPIRED). The dispatcher twin (runtime/src/domains/share-links.ts) maps null the same way.

What changed

packages/plugins/plugin-sharing/src/share-link-service.ts

  • resolveToken(): the standing-policy gate described under H1, with a docblock carrying the why, the placement argument, ruling point 3 (no mark of how a row was minted is read — an absent block is the same switch at its default and refuses too) and point 4.
  • ShareLinkServiceOptions.permissive TSDoc: states the bypass is MINT only and that a link minted under it while the block is off does not resolve until the block is enabled. This TSDoc grew five lines above the five isSystem read sites row 37 anchors — see the adjacent mechanical fix below.

packages/plugins/plugin-sharing/src/share-link-service.test.ts — the reversal register for the #13856 block (four pins reversed in place, the ruling quoted verbatim once in the block header), the two harness helpers hoisted to module scope so both blocks share them, and the [#14033] block: the repro, the standing-policy restore, the shared-null shape, and ruling point 3 on three mint paths (permissive, system context, never-declared block).

packages/plugins/plugin-sharing/src/share-link-eligibility.test.ts — the [#14033] block on the real SqlDriver and the real public route: no record read / no usage stamp, the HTTP-seam equality with an unknown token, the log line, ruling point 4 both halves on one token, and the permissive-minted link refused by the bypassing service and the ordinary one alike.

.changeset/share-link-enabled-at-redemption.md@objectstack/plugin-sharing: minor, in PR #13857's form (see "Changeset level" below).

content/docs/permissions/system-context.mdx — row 37 re-anchored by check:system-context-census --fix (adjacent mechanical fix, declared here; see below) AND its "What you get" cell hand-corrected (one cell, seat-approved in round 0): the bypass buys link creation only; resolution is not bypassed since #14033.

Clause-② declaration (from the actual diff)

git diff -U0 origin/main...HEAD | grep export on the branch: zero lines. No export is added, removed or re-shaped. The accept-set narrowing is behavioural, on the anonymous redemption door: tokens on an object whose publicSharing.enabled is not true — accepted today, refused after, retroactively on deploy, for every mint path. needs:contract-review is hung on this PR and on #14033 per the claim. Hand-edited prose in this PR beyond the code and tests: exactly one cell — the "What you get" cell of content/docs/permissions/system-context.mdx row 37 (seat-approved widening of the claimed file surface, round 0); every other change to that page is the gate's own re-anchoring.

Reading the contract review may want to judge: the gate treats an object with NO publicSharing block as switched off (it is the same switch at its default — enabled defaults to false; createLink already refuses both identically), so a bypass-minted link on a never-opted-in object refuses too. Ruling point 3 is read as requiring exactly this; exempting block-less objects would leave the row-37 path open on every object that never opted in.

Log level

One new log site, warn, through the sink's GUARANTEED member (ShareLinkServiceOptions.logger.warn), spelled this.logger?.warn?.(…) exactly like stillEligible's refusal line. Why a log at all: by ruling the wire answer is deliberately undifferentiated, so the server-side log is the ONLY place the reason a link died exists — #13857's ruling put it there and the same argument holds for the parent switch. Why not error: the caller was answered (null), so this is a response, not a durability degradation; no error? growth on the published sink shape (#13398 class ruling respected). check:durability-log-level and check:optional-error-sink green.

Adjacent mechanical fix — system-context row 37 re-anchor (+ the merge regeneration)

check:system-context-census went red on this branch with exactly the 10 problems the permissive TSDoc edit predicts: 5 [site-without-a-row] reads at :445 :499 :503 :576 :606 plus the 5 [anchor-is-not-a-read-site] citations they rotted off (:440 :494 :498 :571 :601). Remedy: the gate's own --fix (5 anchors rewritten, all +5; 109 read sites / 145 anchors unchanged — no site arrived or vanished). The pre-PR merge-tree check then listed the same page as driver-deferred against a moved origin/main, so scripts/pm/os-regen-merge.sh was run and the page regenerated as the driver instructs; see the commit list.

Commits on the branch (squash-merged as one): 1925bfafc the gate + pins + changeset · a4f128f6b census re-anchor (--fix) · 1162f24df merge of origin/main (7286dd58e) via os-regen-merge.sh · a52cfa363 census regeneration on the merged tree (pnpm gen:system-context-census). Final head: a52cfa363. The union of gates, both pin files (Tests 76 passed (76)) and the package typecheck were re-run on that head; the readings above are from those runs.

Tests and ablation (ran on the head named in each line)

  • Pre-fix measurement on BASE 53d368921 + the new pins: Tests 15 failed | 61 passed (76) — all 15 expected { link: … } to be null.
  • Post-fix, head a52cfa363: pnpm --filter @objectstack/plugin-sharing exec vitest run --maxWorkers=2 src/share-link-service.test.ts src/share-link-eligibility.test.tsTest Files 2 passed (2) / Tests 76 passed (76).
  • pnpm --filter @objectstack/plugin-sharing typecheck (tsc + tsconfig.scripts + check:test-typecheck) → exit 0; check:test-typecheck: OK — … 2 file(s) / 3 error(s) / 3 pinned signature(s) held (ledger unchanged; the edited test files carry no debt entry, so they are compiled and clean).
  • Ablation on the committed tree (a4f128f6b, before the main merge; the gate is byte-identical after it): the gate's condition replaced by false with a marker, proven on disk by anchored counts (if (!policy.enabled) { 1→0, marker 0→1, git diff --stat 1 file 1+/1−). The subject is imported from source (./share-link-service.js, same package — not a dependency's exports), so no dist is involved and no rebuild leg applies. Ablated run: Tests 15 failed | 61 passed (76) — exactly the 15 pins the gate protects went red; the 61 others (all Hold publicSharing.eligibility at redemption, not only at mint #13857 eligibility pins, the mint-gate pin, the enabled: true control) stayed green. Restore: git checkout HEAD -- ABSOLUTE_PATH, proven by empty git diff HEAD + clean status + equal blob hashes (d00a6e53… both sides). Direction note vs the dispatch template: pin (b) (turn the block back ON restores the token) went RED under ablation, not green — it asserts the OFF refusal first, which the ablated tree cannot produce; the ON half is unreachable there. (d) and (e) stayed green as predicted.

Gates (derived on the final head with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands, run unlocked in four batches, exit captured before any pipe)

Green (exit 0): 61 commands — node scripts/check-adr-0087-registration.mjs · node scripts/check-changeset-no-major.mjs · node scripts/check-ci-filter-parity.mjs · node scripts/check-comment-mask-adoption.mjs · node scripts/check-cross-package-test-inputs.mjs · node scripts/check-doc-frontmatter.mjs · node scripts/check-doc-route-spelling.mjs · node scripts/check-docs-section-name.mjs · node scripts/check-empty-changeset.mjs · node scripts/check-keyed-text-bounds.mjs · node scripts/check-plugin-teardown-shape.mjs · node scripts/check-section-landing-index.mjs · node scripts/check-shard-attestation.mjs · node scripts/check-system-context-census.mjs · node scripts/check-tenant-audit-census.mjs · node scripts/check-undeclared-dep-imports.mjs · node scripts/docs-audit/check-affected-docs.mjs · node scripts/docs-audit/check-drift-comment.mjs · node scripts/pm/check-half-states.mjs · node scripts/pm/release-rehearsal-clone.mjs --self-test · pnpm --filter @objectstack/lint run check:doc-formula-expressions · pnpm --filter @objectstack/lint run check:doc-security-posture · pnpm --filter @objectstack/spec run check:docs · pnpm --filter @objectstack/spec run check:empty-state · pnpm --filter @objectstack/spec run check:liveness · pnpm --filter @objectstack/spec run check:strictness-ledger · pnpm --filter @objectstack/spec run check:variant-docs · pnpm --filter @objectstack/spec run check:yaml-examples · pnpm check:changeset-gate-self-tests · pnpm check:corpus-claim-drift · pnpm check:cross-package-test-inputs · pnpm check:doc-anchors · pnpm check:doc-authoring · pnpm check:docs-audit-scope · pnpm check:docs-redirects · pnpm check:docs-single-h1 · pnpm check:engine-double-contract · pnpm check:i18n-stale-fill · pnpm check:logger-receiver-detach · pnpm check:merge-driver · pnpm check:objectql-double-limit · pnpm check:objectui-changeset · pnpm check:page-declaration-shape · pnpm check:pm-half-states · pnpm check:published-files · pnpm check:published-readme-links · pnpm check:query-options-erasure · pnpm check:react-page-adapter-contract · pnpm check:role-word · pnpm check:skill-identifier-liveness · pnpm check:slot-lookup · pnpm check:test-source-alias · pnpm check:type-check-coverage · pnpm check:type-source-resolution · pnpm check:vendor-version-stamps · pnpm check:where-matcher · pnpm check:dispatcher-error-vocabulary · pnpm check:error-code-casing · pnpm check:durability-log-level · pnpm check:optional-error-sink · pnpm check:nul-bytes

NOT MEASURED — each exited on its own stated prerequisite, none is a red:

  • node scripts/check-test-completeness.mjs — exit 3: needs a saved test-run log; the gate's own text says the local reading is NOT MEASURED.
  • pnpm check:dual-build-cjs-loads — exit 3 PREREQUISITE NOT MET: reads built output of 54 packages that have no dist/ here.
  • pnpm check:i18n — exit 1 PREREQUISITE NOT MET: the workspace CLI is not built. This diff touches no translation bundle or object metadata.
  • pnpm --filter @objectstack/spec run check:skill-examples — exit 1: packages/client-react/dist holds no .d.ts (not built). This diff touches no skill, doc example or client SDK.
  • pnpm check:type-check-debt — exit 3 PREREQUISITE NOT MET: 40 workspace dependencies of the ledgered packages are unbuilt; no ledger number was touched.
  • check:error-code-provenance — no such script in package.json.

Repo-wide pnpm lint not run locally (CI-owned); the diff is three TypeScript files in one package plus a changeset and a regenerated page.

Changeset level

minor, not patch, deliberately: scripts/check-changeset-no-major.mjs records the launch-window convention (a breaking change ships as minor; the BREAKING banner and the ADR-0087 disposition are the carriers), and the ruling says 「与 #13857 同型」 — #13857 is minor with that banner. No export changed; the level carries the breaking banner, not a surface change. ADR-0087 disposition: not-required (no-migration-prescription) — nothing authorable is removed, renamed or re-shaped; check:adr-0087-registration and check:changeset-no-major green.

Known stale prose deliberately NOT edited here (file surface per the claim) — flagged for the PM

  1. DONE in this PR (patch round 0, seat answer A on open question 1).content/docs/permissions/system-context.mdx row 37, "What you get" column used to read "link creation/resolution while the policy is off". After this change the bypass buys creation only; resolution is refused. The page is in this PR only through the census gate's own regeneration (the claim's constraint), so the one-cell correction is proposed, not made: "Get: link creation while the policy is off — resolution is NOT bypassed since [Decision] What does turning publicSharing.enabled off mean for an ALREADY-MINTED share link? — the parent switch is mint-only while its own child predicate is now a standing policy #14033: a link minted this way does not resolve until the block is enabled".
  2. content/docs/protocol/objectql/security.mdx "When eligibility is enforced" — the enabled switch now deserves the sibling paragraph and upgrade note Hold publicSharing.eligibility at redemption, not only at mint #13857 wrote for eligibility.
  3. packages/spec/src/contracts/share-link-service.tsIShareLinkService.resolveToken TSDoc lists the null causes and should name the switched-off block; packages/spec/src/data/object.zod.tspublicSharing.enabled TSDoc says "no share links can be issued" and should add "and none resolves". TSDoc only, no schema change — outside this card's surface (packages/spec/** is a stop condition for schema changes; these are prose).

Generated by Claude Code


Patch round 0 (after the seat's collection comment 14033#issuecomment-5511082583)

Generated by Claude Code


Patch round 1 (after contract review round 1 PASS, 14033#issuecomment-5511376156 — the four prose-only notes taken)

Generated by Claude Code


Generated by Claude Code

…ly at mint
`ShareLinkService.resolveToken()` now reads the object's CURRENT
`publicSharing` block on every redemption and refuses when `enabled` is
not `true` — before the record probe, before the usage stamp, before any
sibling key inside the block is evaluated. Re-enabling the block restores
the same tokens (a standing policy, not a revocation). How a link was
minted buys it nothing at redemption: rows minted under a system context
or the `permissive` bypass on a switched-off object refuse the same way,
and an object with no block at all is the same switch at its default.
The refusal reuses the undifferentiated `null` a revoked / expired /
unknown / ineligible token already gets — no new error code, no new
response branch, no usage stamp (over HTTP the generic 404). The readable
reason goes to the server-side log at `warn`, where the eligibility
refusal already writes its own.
Tests: the standing-policy pins (fake engine and real driver), the
HTTP-seam shape, the log line, the bypass-minted path, and the reversal
register for the #13856 pins that asserted serving on a disabled block.
Changeset marked as a breaking runtime change in PR #13857's form.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
…nk hunk shifted it
`check:system-context-census --fix` — pure line rot: the `permissive`
option's TSDoc grew five lines above the five `isSystem` read sites row 37
anchors in `share-link-service.ts`, so every anchor moved by exactly +5.
No site arrived or vanished (109 read sites, 145 anchors, unchanged).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
…gin/main
`pnpm gen:system-context-census` on the merged tree (os-regen-merge.sh step 4):
main's side of the page was taken at merge, then row 37's anchors re-derived
from the merged `share-link-service.ts` (+5, the `permissive` TSDoc). No read
site arrived or vanished.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
@github-actionsgithub-actionsBot added size/l documentation Improvements or additions to documentation tests tooling labels Sep 2, 2026
@github-actions

github-actionsBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/plugin-sharing, touching 3 documentable anchor(s).

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/kernel/runtime-services/sharing-service.mdx(via SHARING_NOT_ENABLED (literal, a string literal in resolveToken))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17.mdx(via SHARING_NOT_ENABLED (literal, a string literal in resolveToken))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 8 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json a5b95ee6edd1e21e04cc43200f41fb730538b197packageMentionDocs.

Which tree this was computed on

This run read content/docs from 114b3f22a35cc3c3d766b6dd3a6b69e3b9b98810 — the merge of head c55d849d768e09bb6d6a48973995b044bdc38861 into base a5b95ee6edd1e21e04cc43200f41fb730538b197, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 114b3f22a35cc3c3d766b6dd3a6b69e3b9b98810 && git checkout 114b3f22a35cc3c3d766b6dd3a6b69e3b9b98810
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin a5b95ee6edd1e21e04cc43200f41fb730538b197 c55d849d768e09bb6d6a48973995b044bdc38861 && git checkout -B drift-repro a5b95ee6edd1e21e04cc43200f41fb730538b197 && git merge --no-ff c55d849d768e09bb6d6a48973995b044bdc38861
node scripts/docs-audit/affected-docs.mjs --json a5b95ee6edd1e21e04cc43200f41fb730538b197

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs a5b95ee6edd1e21e04cc43200f41fb730538b197 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

… resolution is a standing policy
The "What you get" cell of the system-context ledger's row 37 still said
"link creation/resolution while the policy is off". Since the redemption
gate on `publicSharing.enabled`, a link minted under the system /
`permissive` bypass while the block is off does not resolve until the block
is enabled. Prose only; the five anchors are untouched (seat-approved
one-cell widening of the claim's file surface, 14033#issuecomment-5511082583).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
…gin/main (round 0)
os-regen-merge.sh step 4 on the merged tree: main's side of the page was
taken at merge, the row-37 "What you get" cell re-applied (a hand edit no
generator reproduces), then `pnpm gen:system-context-census`. Anchors
unchanged; no read site arrived or vanished.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
…, one permissive TSDoc clause, row 37 re-anchored
Prose only; the gate, the tests and the changeset level are untouched.
Changeset: the superseded #13856 reading is named; an object the engine
cannot return a schema for is `enabled: false` by `getPolicy`'s definition
and refuses at redemption (fail-closed, as `createLink`); the refusal logs
one `warn` per refused hit, not latched. `permissive` TSDoc: during a late
schema scan the option helps minting, not serving. The four added TSDoc
lines moved the five row-37 anchors, re-anchored by the census gate's own
`--fix`; the row-37 "What you get" cell is unchanged.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
…gin/main (round 1)
os-regen-merge.sh step 4 on the merged tree: main's side of the page taken
at merge, the row-37 "What you get" cell re-applied (a hand edit no
generator reproduces), then `pnpm gen:system-context-census`. No read site
arrived or vanished.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
@os-sales
os-sales marked this pull request as ready for review September 2, 2026 15:12
@os-salesClaude

Copy link
Copy Markdown
Collaborator

Landing provenance — ready + auto-merge at head c55d849d7


Generated by Claude Code

@os-sales
os-sales deleted the claude/issue-14033-public-sharing-enabled-standing-policy branch September 2, 2026 16:12
baozhoutao pushed a commit that referenced this pull request Sep 2, 2026
Item 2 of #14582 (follow-up to #14033 / PR #14580, split from #14033's
docs surfaces that PR #14580 could not touch). The "Public Share Links"
section documented the child predicate (eligibility, #13608) as a
standing policy held at every redemption, but not the parent switch
publicSharing.enabled — #14033 made that switch the same shape: held at
every redemption, not only at mint. Adds the sibling paragraph plus its
upgrade-note callout, placed after the eligibility paragraph so a reader
who has already understood the child predicate recognises the parent's
paragraph as the same shape one level up.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WLJQhde67SeTccsmnBVarV
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

2 participants

@os-sales@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(plugin-sharing): hold publicSharing.enabled at redemption, not only at mint (#14033) - #14580

Merged
os-sales merged 10 commits into
mainfrom
claude/issue-14033-public-sharing-enabled-standing-policy
Sep 2, 2026
Merged

fix(plugin-sharing): hold publicSharing.enabled at redemption, not only at mint (#14033)#14580
os-sales merged 10 commits into
mainfrom
claude/issue-14033-public-sharing-enabled-standing-policy

Conversation

@claude

@claudeclaudeBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Fixes#14033

Ruling of record (verbatim, untranslated)

Maintainer ruling of 2026-09-01, recorded by the director seat in 14033#issuecomment-5491054105 (maintainer's verbatim reply to the batch: 「其他同意」). The recorded four points, verbatim:

  1. A:publicSharing.enabled 为持续策略,resolveToken() 每次兑付重查;关掉 ⇒ 存量链接停止兑付。
  2. 追溯即时生效(沿 sharing: publicSharing.eligibility is evaluated only at mint — a link keeps serving a record after it stops being eligible #13608 先例):部署一落地,禁用块上的所有既有 token 即停止解析。changeset 必须标注 breaking runtime change,与 Hold publicSharing.eligibility at redemption, not only at mint #13857 同型。
  3. 系统/宽容旁路铸造的链接同样受管:兑付是匿名动作,不因铸造方式豁免 —— enabled 关则一律拒付(含 system-context.mdx ledger row 37 的旁路铸造路径)。
  4. 兄弟键统一律入册:enabled 关 ⇒ 整块不生效 ⇒ 拒付(子键求值 moot);enabled 开 ⇒ 块内各策略键按持续策略在兑付期求值(eligibility 已然,redactFieldssharing: turning publicSharing off leaves already-minted links serving — and silently drops the object's redactFields #13856 修复)。后续兄弟键⛔ 不再单独立卡。

And its implementation notes, verbatim:

Premise verification on origin/main (BASE 53d368921) — all five hold

  • P1 — measured, not read. The first pin (THE REPRO — minted while enabled, the SAME token stops resolving the moment the block is turned off) was run against the UNMODIFIED service before any edit: AssertionError: expected { link: { …(16) }, …(1) } to be null — the token kept serving with the block off. 15 such reds (every new/reversed pin), 61 existing pins green. Log: pre-fix run, Tests 15 failed | 61 passed (76). Premise valid.
  • P2 — PR fix(plugin-sharing): declared publicSharing.redactFields survive the object opting out #14171 (sharing: turning publicSharing off leaves already-minted links serving — and silently drops the object's redactFields #13856) pins. Two pins asserted a link on a DISABLED block still serves with redactions applied (THE REPRO — opting out keeps the declared redactions applying, boundary — the per-link redact_fields union is unchanged when the switch is off), and two reverse controls asserted a bypass-seeded link on a block-LESS object serves. All four are REVERSED in place and registered (docblock naming what each pinned, which ruling point moved it, the ruling verbatim in the block header); none deleted. The pins are agent-authored test content in share-link-service.test.ts, not governed content. The enabled: true control and the mint-gate pin are untouched.
  • P3 — PR Hold publicSharing.eligibility at redemption, not only at mint #13857 form. Its changeset (.changeset/share-link-eligibility-at-redemption.md at fc9ba76a5) is minor + the **BREAKING** runtime behaviour change on a published package … Shipped as minor under the repo's launch-window convention banner + an ADR-0087 not-required (no-migration-prescription) marker. This changeset copies that form. Hold publicSharing.eligibility at redemption, not only at mint #13857 answers the refusal with the undifferentiated null (over HTTP the generic 404 INVALID_OR_EXPIRED), reason to the server-side log at warn — the same family this refusal joins.
  • P4 — no new code. The refusal reuses the answer resolveToken already gives for revoked / expired / unknown / ineligible: null. No error code is minted, nothing under packages/spec/** is touched. check:dispatcher-error-vocabulary and check:error-code-casing green (below); a check:error-code-provenance script does not exist in package.json — reported as such, not as measured.
  • P5.content/docs/releases/**, rule-hooks.ts, sharing-plugin.ts, skills/**, packages/spec/** untouched; the redactFields logic from fix(plugin-sharing): declared publicSharing.redactFields survive the object opting out #14171 is untouched (getPolicy unchanged).

PM mechanism hypotheses — verdicts

  • H1 holds. One gate in resolveToken, at the existing policy read (after the cheap revoked / expired / audience / password gates, BEFORE loadRecordForServing, BEFORE the usage stamp, BEFORE the redaction set): if (!policy.enabled) { log; return null; }. It reads the object's CURRENT block through the same getPolicy / getSchema source and nothing about how the row was minted. Measured: on a refused redemption the only find the engine sees is the sys_share_link token lookup; use_count / last_used_at are unchanged (fake engine and real SqlDriver alike).
  • H2 holds. The code is the family the anonymous caller already receives for a link that does not serve: null at the service seam, 404 INVALID_OR_EXPIRED over HTTP, byte-for-byte equal to an unknown token (pinned at the route). SHARING_NOT_ENABLED stays the authoring-side mint refusal (422, pinned unchanged) and appears at redemption only as the reason field of the server-side log line, never on the wire (pinned: the body contains no enabled, publicsharing or sharing_not_enabled).
  • H3 holds. With the block off, eligibility is not evaluated and the redaction set is not computed (the gate returns first; pinned: a record the predicate WOULD refuse is refused by the switch with no record read and a SHARING_NOT_ENABLED log line, and switching back on the same token is refused by the predicate — RECORD_NOT_ELIGIBLE — then serves once it qualifies). With the block on, Hold publicSharing.eligibility at redemption, not only at mint #13857's 12 redemption pins and fix(plugin-sharing): declared publicSharing.redactFields survive the object opting out #14171's enabled: true control are unchanged and green. No new code path for any sibling key.
  • H4 holds. The gate reads the current block on every call; no migration, no data change; the changeset's breaking-runtime-change banner is the only artefact.
  • H5 holds.share-link-routes.ts untouched: null is already mapped (401 for a live password / signed-in row exactly as today, 410 for revoked / expired, else 404 INVALID_OR_EXPIRED). The dispatcher twin (runtime/src/domains/share-links.ts) maps null the same way.

What changed

packages/plugins/plugin-sharing/src/share-link-service.ts

  • resolveToken(): the standing-policy gate described under H1, with a docblock carrying the why, the placement argument, ruling point 3 (no mark of how a row was minted is read — an absent block is the same switch at its default and refuses too) and point 4.
  • ShareLinkServiceOptions.permissive TSDoc: states the bypass is MINT only and that a link minted under it while the block is off does not resolve until the block is enabled. This TSDoc grew five lines above the five isSystem read sites row 37 anchors — see the adjacent mechanical fix below.

packages/plugins/plugin-sharing/src/share-link-service.test.ts — the reversal register for the #13856 block (four pins reversed in place, the ruling quoted verbatim once in the block header), the two harness helpers hoisted to module scope so both blocks share them, and the [#14033] block: the repro, the standing-policy restore, the shared-null shape, and ruling point 3 on three mint paths (permissive, system context, never-declared block).

packages/plugins/plugin-sharing/src/share-link-eligibility.test.ts — the [#14033] block on the real SqlDriver and the real public route: no record read / no usage stamp, the HTTP-seam equality with an unknown token, the log line, ruling point 4 both halves on one token, and the permissive-minted link refused by the bypassing service and the ordinary one alike.

.changeset/share-link-enabled-at-redemption.md@objectstack/plugin-sharing: minor, in PR #13857's form (see "Changeset level" below).

content/docs/permissions/system-context.mdx — row 37 re-anchored by check:system-context-census --fix (adjacent mechanical fix, declared here; see below) AND its "What you get" cell hand-corrected (one cell, seat-approved in round 0): the bypass buys link creation only; resolution is not bypassed since #14033.

Clause-② declaration (from the actual diff)

git diff -U0 origin/main...HEAD | grep export on the branch: zero lines. No export is added, removed or re-shaped. The accept-set narrowing is behavioural, on the anonymous redemption door: tokens on an object whose publicSharing.enabled is not true — accepted today, refused after, retroactively on deploy, for every mint path. needs:contract-review is hung on this PR and on #14033 per the claim. Hand-edited prose in this PR beyond the code and tests: exactly one cell — the "What you get" cell of content/docs/permissions/system-context.mdx row 37 (seat-approved widening of the claimed file surface, round 0); every other change to that page is the gate's own re-anchoring.

Reading the contract review may want to judge: the gate treats an object with NO publicSharing block as switched off (it is the same switch at its default — enabled defaults to false; createLink already refuses both identically), so a bypass-minted link on a never-opted-in object refuses too. Ruling point 3 is read as requiring exactly this; exempting block-less objects would leave the row-37 path open on every object that never opted in.

Log level

One new log site, warn, through the sink's GUARANTEED member (ShareLinkServiceOptions.logger.warn), spelled this.logger?.warn?.(…) exactly like stillEligible's refusal line. Why a log at all: by ruling the wire answer is deliberately undifferentiated, so the server-side log is the ONLY place the reason a link died exists — #13857's ruling put it there and the same argument holds for the parent switch. Why not error: the caller was answered (null), so this is a response, not a durability degradation; no error? growth on the published sink shape (#13398 class ruling respected). check:durability-log-level and check:optional-error-sink green.

Adjacent mechanical fix — system-context row 37 re-anchor (+ the merge regeneration)

check:system-context-census went red on this branch with exactly the 10 problems the permissive TSDoc edit predicts: 5 [site-without-a-row] reads at :445 :499 :503 :576 :606 plus the 5 [anchor-is-not-a-read-site] citations they rotted off (:440 :494 :498 :571 :601). Remedy: the gate's own --fix (5 anchors rewritten, all +5; 109 read sites / 145 anchors unchanged — no site arrived or vanished). The pre-PR merge-tree check then listed the same page as driver-deferred against a moved origin/main, so scripts/pm/os-regen-merge.sh was run and the page regenerated as the driver instructs; see the commit list.

Commits on the branch (squash-merged as one): 1925bfafc the gate + pins + changeset · a4f128f6b census re-anchor (--fix) · 1162f24df merge of origin/main (7286dd58e) via os-regen-merge.sh · a52cfa363 census regeneration on the merged tree (pnpm gen:system-context-census). Final head: a52cfa363. The union of gates, both pin files (Tests 76 passed (76)) and the package typecheck were re-run on that head; the readings above are from those runs.

Tests and ablation (ran on the head named in each line)

  • Pre-fix measurement on BASE 53d368921 + the new pins: Tests 15 failed | 61 passed (76) — all 15 expected { link: … } to be null.
  • Post-fix, head a52cfa363: pnpm --filter @objectstack/plugin-sharing exec vitest run --maxWorkers=2 src/share-link-service.test.ts src/share-link-eligibility.test.tsTest Files 2 passed (2) / Tests 76 passed (76).
  • pnpm --filter @objectstack/plugin-sharing typecheck (tsc + tsconfig.scripts + check:test-typecheck) → exit 0; check:test-typecheck: OK — … 2 file(s) / 3 error(s) / 3 pinned signature(s) held (ledger unchanged; the edited test files carry no debt entry, so they are compiled and clean).
  • Ablation on the committed tree (a4f128f6b, before the main merge; the gate is byte-identical after it): the gate's condition replaced by false with a marker, proven on disk by anchored counts (if (!policy.enabled) { 1→0, marker 0→1, git diff --stat 1 file 1+/1−). The subject is imported from source (./share-link-service.js, same package — not a dependency's exports), so no dist is involved and no rebuild leg applies. Ablated run: Tests 15 failed | 61 passed (76) — exactly the 15 pins the gate protects went red; the 61 others (all Hold publicSharing.eligibility at redemption, not only at mint #13857 eligibility pins, the mint-gate pin, the enabled: true control) stayed green. Restore: git checkout HEAD -- ABSOLUTE_PATH, proven by empty git diff HEAD + clean status + equal blob hashes (d00a6e53… both sides). Direction note vs the dispatch template: pin (b) (turn the block back ON restores the token) went RED under ablation, not green — it asserts the OFF refusal first, which the ablated tree cannot produce; the ON half is unreachable there. (d) and (e) stayed green as predicted.

Gates (derived on the final head with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands, run unlocked in four batches, exit captured before any pipe)

Green (exit 0): 61 commands — node scripts/check-adr-0087-registration.mjs · node scripts/check-changeset-no-major.mjs · node scripts/check-ci-filter-parity.mjs · node scripts/check-comment-mask-adoption.mjs · node scripts/check-cross-package-test-inputs.mjs · node scripts/check-doc-frontmatter.mjs · node scripts/check-doc-route-spelling.mjs · node scripts/check-docs-section-name.mjs · node scripts/check-empty-changeset.mjs · node scripts/check-keyed-text-bounds.mjs · node scripts/check-plugin-teardown-shape.mjs · node scripts/check-section-landing-index.mjs · node scripts/check-shard-attestation.mjs · node scripts/check-system-context-census.mjs · node scripts/check-tenant-audit-census.mjs · node scripts/check-undeclared-dep-imports.mjs · node scripts/docs-audit/check-affected-docs.mjs · node scripts/docs-audit/check-drift-comment.mjs · node scripts/pm/check-half-states.mjs · node scripts/pm/release-rehearsal-clone.mjs --self-test · pnpm --filter @objectstack/lint run check:doc-formula-expressions · pnpm --filter @objectstack/lint run check:doc-security-posture · pnpm --filter @objectstack/spec run check:docs · pnpm --filter @objectstack/spec run check:empty-state · pnpm --filter @objectstack/spec run check:liveness · pnpm --filter @objectstack/spec run check:strictness-ledger · pnpm --filter @objectstack/spec run check:variant-docs · pnpm --filter @objectstack/spec run check:yaml-examples · pnpm check:changeset-gate-self-tests · pnpm check:corpus-claim-drift · pnpm check:cross-package-test-inputs · pnpm check:doc-anchors · pnpm check:doc-authoring · pnpm check:docs-audit-scope · pnpm check:docs-redirects · pnpm check:docs-single-h1 · pnpm check:engine-double-contract · pnpm check:i18n-stale-fill · pnpm check:logger-receiver-detach · pnpm check:merge-driver · pnpm check:objectql-double-limit · pnpm check:objectui-changeset · pnpm check:page-declaration-shape · pnpm check:pm-half-states · pnpm check:published-files · pnpm check:published-readme-links · pnpm check:query-options-erasure · pnpm check:react-page-adapter-contract · pnpm check:role-word · pnpm check:skill-identifier-liveness · pnpm check:slot-lookup · pnpm check:test-source-alias · pnpm check:type-check-coverage · pnpm check:type-source-resolution · pnpm check:vendor-version-stamps · pnpm check:where-matcher · pnpm check:dispatcher-error-vocabulary · pnpm check:error-code-casing · pnpm check:durability-log-level · pnpm check:optional-error-sink · pnpm check:nul-bytes

NOT MEASURED — each exited on its own stated prerequisite, none is a red:

  • node scripts/check-test-completeness.mjs — exit 3: needs a saved test-run log; the gate's own text says the local reading is NOT MEASURED.
  • pnpm check:dual-build-cjs-loads — exit 3 PREREQUISITE NOT MET: reads built output of 54 packages that have no dist/ here.
  • pnpm check:i18n — exit 1 PREREQUISITE NOT MET: the workspace CLI is not built. This diff touches no translation bundle or object metadata.
  • pnpm --filter @objectstack/spec run check:skill-examples — exit 1: packages/client-react/dist holds no .d.ts (not built). This diff touches no skill, doc example or client SDK.
  • pnpm check:type-check-debt — exit 3 PREREQUISITE NOT MET: 40 workspace dependencies of the ledgered packages are unbuilt; no ledger number was touched.
  • check:error-code-provenance — no such script in package.json.

Repo-wide pnpm lint not run locally (CI-owned); the diff is three TypeScript files in one package plus a changeset and a regenerated page.

Changeset level

minor, not patch, deliberately: scripts/check-changeset-no-major.mjs records the launch-window convention (a breaking change ships as minor; the BREAKING banner and the ADR-0087 disposition are the carriers), and the ruling says 「与 #13857 同型」 — #13857 is minor with that banner. No export changed; the level carries the breaking banner, not a surface change. ADR-0087 disposition: not-required (no-migration-prescription) — nothing authorable is removed, renamed or re-shaped; check:adr-0087-registration and check:changeset-no-major green.

Known stale prose deliberately NOT edited here (file surface per the claim) — flagged for the PM

  1. DONE in this PR (patch round 0, seat answer A on open question 1).content/docs/permissions/system-context.mdx row 37, "What you get" column used to read "link creation/resolution while the policy is off". After this change the bypass buys creation only; resolution is refused. The page is in this PR only through the census gate's own regeneration (the claim's constraint), so the one-cell correction is proposed, not made: "Get: link creation while the policy is off — resolution is NOT bypassed since [Decision] What does turning publicSharing.enabled off mean for an ALREADY-MINTED share link? — the parent switch is mint-only while its own child predicate is now a standing policy #14033: a link minted this way does not resolve until the block is enabled".
  2. content/docs/protocol/objectql/security.mdx "When eligibility is enforced" — the enabled switch now deserves the sibling paragraph and upgrade note Hold publicSharing.eligibility at redemption, not only at mint #13857 wrote for eligibility.
  3. packages/spec/src/contracts/share-link-service.tsIShareLinkService.resolveToken TSDoc lists the null causes and should name the switched-off block; packages/spec/src/data/object.zod.tspublicSharing.enabled TSDoc says "no share links can be issued" and should add "and none resolves". TSDoc only, no schema change — outside this card's surface (packages/spec/** is a stop condition for schema changes; these are prose).

Generated by Claude Code


Patch round 0 (after the seat's collection comment 14033#issuecomment-5511082583)

Generated by Claude Code


Patch round 1 (after contract review round 1 PASS, 14033#issuecomment-5511376156 — the four prose-only notes taken)

Generated by Claude Code


Generated by Claude Code

…ly at mint
`ShareLinkService.resolveToken()` now reads the object's CURRENT
`publicSharing` block on every redemption and refuses when `enabled` is
not `true` — before the record probe, before the usage stamp, before any
sibling key inside the block is evaluated. Re-enabling the block restores
the same tokens (a standing policy, not a revocation). How a link was
minted buys it nothing at redemption: rows minted under a system context
or the `permissive` bypass on a switched-off object refuse the same way,
and an object with no block at all is the same switch at its default.
The refusal reuses the undifferentiated `null` a revoked / expired /
unknown / ineligible token already gets — no new error code, no new
response branch, no usage stamp (over HTTP the generic 404). The readable
reason goes to the server-side log at `warn`, where the eligibility
refusal already writes its own.
Tests: the standing-policy pins (fake engine and real driver), the
HTTP-seam shape, the log line, the bypass-minted path, and the reversal
register for the #13856 pins that asserted serving on a disabled block.
Changeset marked as a breaking runtime change in PR #13857's form.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
…nk hunk shifted it
`check:system-context-census --fix` — pure line rot: the `permissive`
option's TSDoc grew five lines above the five `isSystem` read sites row 37
anchors in `share-link-service.ts`, so every anchor moved by exactly +5.
No site arrived or vanished (109 read sites, 145 anchors, unchanged).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
…gin/main
`pnpm gen:system-context-census` on the merged tree (os-regen-merge.sh step 4):
main's side of the page was taken at merge, then row 37's anchors re-derived
from the merged `share-link-service.ts` (+5, the `permissive` TSDoc). No read
site arrived or vanished.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
@github-actionsgithub-actionsBot added size/l documentation Improvements or additions to documentation tests tooling labels Sep 2, 2026
@github-actions

github-actionsBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/plugin-sharing, touching 3 documentable anchor(s).

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/kernel/runtime-services/sharing-service.mdx(via SHARING_NOT_ENABLED (literal, a string literal in resolveToken))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17.mdx(via SHARING_NOT_ENABLED (literal, a string literal in resolveToken))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 8 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json a5b95ee6edd1e21e04cc43200f41fb730538b197packageMentionDocs.

Which tree this was computed on

This run read content/docs from 114b3f22a35cc3c3d766b6dd3a6b69e3b9b98810 — the merge of head c55d849d768e09bb6d6a48973995b044bdc38861 into base a5b95ee6edd1e21e04cc43200f41fb730538b197, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 114b3f22a35cc3c3d766b6dd3a6b69e3b9b98810 && git checkout 114b3f22a35cc3c3d766b6dd3a6b69e3b9b98810
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin a5b95ee6edd1e21e04cc43200f41fb730538b197 c55d849d768e09bb6d6a48973995b044bdc38861 && git checkout -B drift-repro a5b95ee6edd1e21e04cc43200f41fb730538b197 && git merge --no-ff c55d849d768e09bb6d6a48973995b044bdc38861
node scripts/docs-audit/affected-docs.mjs --json a5b95ee6edd1e21e04cc43200f41fb730538b197

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs a5b95ee6edd1e21e04cc43200f41fb730538b197 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

… resolution is a standing policy
The "What you get" cell of the system-context ledger's row 37 still said
"link creation/resolution while the policy is off". Since the redemption
gate on `publicSharing.enabled`, a link minted under the system /
`permissive` bypass while the block is off does not resolve until the block
is enabled. Prose only; the five anchors are untouched (seat-approved
one-cell widening of the claim's file surface, 14033#issuecomment-5511082583).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
…gin/main (round 0)
os-regen-merge.sh step 4 on the merged tree: main's side of the page was
taken at merge, the row-37 "What you get" cell re-applied (a hand edit no
generator reproduces), then `pnpm gen:system-context-census`. Anchors
unchanged; no read site arrived or vanished.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
…, one permissive TSDoc clause, row 37 re-anchored
Prose only; the gate, the tests and the changeset level are untouched.
Changeset: the superseded #13856 reading is named; an object the engine
cannot return a schema for is `enabled: false` by `getPolicy`'s definition
and refuses at redemption (fail-closed, as `createLink`); the refusal logs
one `warn` per refused hit, not latched. `permissive` TSDoc: during a late
schema scan the option helps minting, not serving. The four added TSDoc
lines moved the five row-37 anchors, re-anchored by the census gate's own
`--fix`; the row-37 "What you get" cell is unchanged.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
…gin/main (round 1)
os-regen-merge.sh step 4 on the merged tree: main's side of the page taken
at merge, the row-37 "What you get" cell re-applied (a hand edit no
generator reproduces), then `pnpm gen:system-context-census`. No read site
arrived or vanished.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
@os-sales
os-sales marked this pull request as ready for review September 2, 2026 15:12
@os-salesClaude

Copy link
Copy Markdown
Collaborator

Landing provenance — ready + auto-merge at head c55d849d7


Generated by Claude Code

@os-sales
os-sales deleted the claude/issue-14033-public-sharing-enabled-standing-policy branch September 2, 2026 16:12
baozhoutao pushed a commit that referenced this pull request Sep 2, 2026
Item 2 of #14582 (follow-up to #14033 / PR #14580, split from #14033's
docs surfaces that PR #14580 could not touch). The "Public Share Links"
section documented the child predicate (eligibility, #13608) as a
standing policy held at every redemption, but not the parent switch
publicSharing.enabled — #14033 made that switch the same shape: held at
every redemption, not only at mint. Adds the sibling paragraph plus its
upgrade-note callout, placed after the eligibility paragraph so a reader
who has already understood the child predicate recognises the parent's
paragraph as the same shape one level up.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WLJQhde67SeTccsmnBVarV
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

2 participants

@os-sales@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix(plugin-sharing): hold publicSharing.enabled at redemption, not only at mint (#14033) - #14580

Merged
os-sales merged 10 commits into
mainfrom
claude/issue-14033-public-sharing-enabled-standing-policy
Sep 2, 2026
Merged

fix(plugin-sharing): hold publicSharing.enabled at redemption, not only at mint (#14033)#14580
os-sales merged 10 commits into
mainfrom
claude/issue-14033-public-sharing-enabled-standing-policy

Conversation

@claude

@claudeclaudeBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Fixes#14033

Ruling of record (verbatim, untranslated)

Maintainer ruling of 2026-09-01, recorded by the director seat in 14033#issuecomment-5491054105 (maintainer's verbatim reply to the batch: 「其他同意」). The recorded four points, verbatim:

  1. A:publicSharing.enabled 为持续策略,resolveToken() 每次兑付重查;关掉 ⇒ 存量链接停止兑付。
  2. 追溯即时生效(沿 sharing: publicSharing.eligibility is evaluated only at mint — a link keeps serving a record after it stops being eligible #13608 先例):部署一落地,禁用块上的所有既有 token 即停止解析。changeset 必须标注 breaking runtime change,与 Hold publicSharing.eligibility at redemption, not only at mint #13857 同型。
  3. 系统/宽容旁路铸造的链接同样受管:兑付是匿名动作,不因铸造方式豁免 —— enabled 关则一律拒付(含 system-context.mdx ledger row 37 的旁路铸造路径)。
  4. 兄弟键统一律入册:enabled 关 ⇒ 整块不生效 ⇒ 拒付(子键求值 moot);enabled 开 ⇒ 块内各策略键按持续策略在兑付期求值(eligibility 已然,redactFieldssharing: turning publicSharing off leaves already-minted links serving — and silently drops the object's redactFields #13856 修复)。后续兄弟键⛔ 不再单独立卡。

And its implementation notes, verbatim:

Premise verification on origin/main (BASE 53d368921) — all five hold

  • P1 — measured, not read. The first pin (THE REPRO — minted while enabled, the SAME token stops resolving the moment the block is turned off) was run against the UNMODIFIED service before any edit: AssertionError: expected { link: { …(16) }, …(1) } to be null — the token kept serving with the block off. 15 such reds (every new/reversed pin), 61 existing pins green. Log: pre-fix run, Tests 15 failed | 61 passed (76). Premise valid.
  • P2 — PR fix(plugin-sharing): declared publicSharing.redactFields survive the object opting out #14171 (sharing: turning publicSharing off leaves already-minted links serving — and silently drops the object's redactFields #13856) pins. Two pins asserted a link on a DISABLED block still serves with redactions applied (THE REPRO — opting out keeps the declared redactions applying, boundary — the per-link redact_fields union is unchanged when the switch is off), and two reverse controls asserted a bypass-seeded link on a block-LESS object serves. All four are REVERSED in place and registered (docblock naming what each pinned, which ruling point moved it, the ruling verbatim in the block header); none deleted. The pins are agent-authored test content in share-link-service.test.ts, not governed content. The enabled: true control and the mint-gate pin are untouched.
  • P3 — PR Hold publicSharing.eligibility at redemption, not only at mint #13857 form. Its changeset (.changeset/share-link-eligibility-at-redemption.md at fc9ba76a5) is minor + the **BREAKING** runtime behaviour change on a published package … Shipped as minor under the repo's launch-window convention banner + an ADR-0087 not-required (no-migration-prescription) marker. This changeset copies that form. Hold publicSharing.eligibility at redemption, not only at mint #13857 answers the refusal with the undifferentiated null (over HTTP the generic 404 INVALID_OR_EXPIRED), reason to the server-side log at warn — the same family this refusal joins.
  • P4 — no new code. The refusal reuses the answer resolveToken already gives for revoked / expired / unknown / ineligible: null. No error code is minted, nothing under packages/spec/** is touched. check:dispatcher-error-vocabulary and check:error-code-casing green (below); a check:error-code-provenance script does not exist in package.json — reported as such, not as measured.
  • P5.content/docs/releases/**, rule-hooks.ts, sharing-plugin.ts, skills/**, packages/spec/** untouched; the redactFields logic from fix(plugin-sharing): declared publicSharing.redactFields survive the object opting out #14171 is untouched (getPolicy unchanged).

PM mechanism hypotheses — verdicts

  • H1 holds. One gate in resolveToken, at the existing policy read (after the cheap revoked / expired / audience / password gates, BEFORE loadRecordForServing, BEFORE the usage stamp, BEFORE the redaction set): if (!policy.enabled) { log; return null; }. It reads the object's CURRENT block through the same getPolicy / getSchema source and nothing about how the row was minted. Measured: on a refused redemption the only find the engine sees is the sys_share_link token lookup; use_count / last_used_at are unchanged (fake engine and real SqlDriver alike).
  • H2 holds. The code is the family the anonymous caller already receives for a link that does not serve: null at the service seam, 404 INVALID_OR_EXPIRED over HTTP, byte-for-byte equal to an unknown token (pinned at the route). SHARING_NOT_ENABLED stays the authoring-side mint refusal (422, pinned unchanged) and appears at redemption only as the reason field of the server-side log line, never on the wire (pinned: the body contains no enabled, publicsharing or sharing_not_enabled).
  • H3 holds. With the block off, eligibility is not evaluated and the redaction set is not computed (the gate returns first; pinned: a record the predicate WOULD refuse is refused by the switch with no record read and a SHARING_NOT_ENABLED log line, and switching back on the same token is refused by the predicate — RECORD_NOT_ELIGIBLE — then serves once it qualifies). With the block on, Hold publicSharing.eligibility at redemption, not only at mint #13857's 12 redemption pins and fix(plugin-sharing): declared publicSharing.redactFields survive the object opting out #14171's enabled: true control are unchanged and green. No new code path for any sibling key.
  • H4 holds. The gate reads the current block on every call; no migration, no data change; the changeset's breaking-runtime-change banner is the only artefact.
  • H5 holds.share-link-routes.ts untouched: null is already mapped (401 for a live password / signed-in row exactly as today, 410 for revoked / expired, else 404 INVALID_OR_EXPIRED). The dispatcher twin (runtime/src/domains/share-links.ts) maps null the same way.

What changed

packages/plugins/plugin-sharing/src/share-link-service.ts

  • resolveToken(): the standing-policy gate described under H1, with a docblock carrying the why, the placement argument, ruling point 3 (no mark of how a row was minted is read — an absent block is the same switch at its default and refuses too) and point 4.
  • ShareLinkServiceOptions.permissive TSDoc: states the bypass is MINT only and that a link minted under it while the block is off does not resolve until the block is enabled. This TSDoc grew five lines above the five isSystem read sites row 37 anchors — see the adjacent mechanical fix below.

packages/plugins/plugin-sharing/src/share-link-service.test.ts — the reversal register for the #13856 block (four pins reversed in place, the ruling quoted verbatim once in the block header), the two harness helpers hoisted to module scope so both blocks share them, and the [#14033] block: the repro, the standing-policy restore, the shared-null shape, and ruling point 3 on three mint paths (permissive, system context, never-declared block).

packages/plugins/plugin-sharing/src/share-link-eligibility.test.ts — the [#14033] block on the real SqlDriver and the real public route: no record read / no usage stamp, the HTTP-seam equality with an unknown token, the log line, ruling point 4 both halves on one token, and the permissive-minted link refused by the bypassing service and the ordinary one alike.

.changeset/share-link-enabled-at-redemption.md@objectstack/plugin-sharing: minor, in PR #13857's form (see "Changeset level" below).

content/docs/permissions/system-context.mdx — row 37 re-anchored by check:system-context-census --fix (adjacent mechanical fix, declared here; see below) AND its "What you get" cell hand-corrected (one cell, seat-approved in round 0): the bypass buys link creation only; resolution is not bypassed since #14033.

Clause-② declaration (from the actual diff)

git diff -U0 origin/main...HEAD | grep export on the branch: zero lines. No export is added, removed or re-shaped. The accept-set narrowing is behavioural, on the anonymous redemption door: tokens on an object whose publicSharing.enabled is not true — accepted today, refused after, retroactively on deploy, for every mint path. needs:contract-review is hung on this PR and on #14033 per the claim. Hand-edited prose in this PR beyond the code and tests: exactly one cell — the "What you get" cell of content/docs/permissions/system-context.mdx row 37 (seat-approved widening of the claimed file surface, round 0); every other change to that page is the gate's own re-anchoring.

Reading the contract review may want to judge: the gate treats an object with NO publicSharing block as switched off (it is the same switch at its default — enabled defaults to false; createLink already refuses both identically), so a bypass-minted link on a never-opted-in object refuses too. Ruling point 3 is read as requiring exactly this; exempting block-less objects would leave the row-37 path open on every object that never opted in.

Log level

One new log site, warn, through the sink's GUARANTEED member (ShareLinkServiceOptions.logger.warn), spelled this.logger?.warn?.(…) exactly like stillEligible's refusal line. Why a log at all: by ruling the wire answer is deliberately undifferentiated, so the server-side log is the ONLY place the reason a link died exists — #13857's ruling put it there and the same argument holds for the parent switch. Why not error: the caller was answered (null), so this is a response, not a durability degradation; no error? growth on the published sink shape (#13398 class ruling respected). check:durability-log-level and check:optional-error-sink green.

Adjacent mechanical fix — system-context row 37 re-anchor (+ the merge regeneration)

check:system-context-census went red on this branch with exactly the 10 problems the permissive TSDoc edit predicts: 5 [site-without-a-row] reads at :445 :499 :503 :576 :606 plus the 5 [anchor-is-not-a-read-site] citations they rotted off (:440 :494 :498 :571 :601). Remedy: the gate's own --fix (5 anchors rewritten, all +5; 109 read sites / 145 anchors unchanged — no site arrived or vanished). The pre-PR merge-tree check then listed the same page as driver-deferred against a moved origin/main, so scripts/pm/os-regen-merge.sh was run and the page regenerated as the driver instructs; see the commit list.

Commits on the branch (squash-merged as one): 1925bfafc the gate + pins + changeset · a4f128f6b census re-anchor (--fix) · 1162f24df merge of origin/main (7286dd58e) via os-regen-merge.sh · a52cfa363 census regeneration on the merged tree (pnpm gen:system-context-census). Final head: a52cfa363. The union of gates, both pin files (Tests 76 passed (76)) and the package typecheck were re-run on that head; the readings above are from those runs.

Tests and ablation (ran on the head named in each line)

  • Pre-fix measurement on BASE 53d368921 + the new pins: Tests 15 failed | 61 passed (76) — all 15 expected { link: … } to be null.
  • Post-fix, head a52cfa363: pnpm --filter @objectstack/plugin-sharing exec vitest run --maxWorkers=2 src/share-link-service.test.ts src/share-link-eligibility.test.tsTest Files 2 passed (2) / Tests 76 passed (76).
  • pnpm --filter @objectstack/plugin-sharing typecheck (tsc + tsconfig.scripts + check:test-typecheck) → exit 0; check:test-typecheck: OK — … 2 file(s) / 3 error(s) / 3 pinned signature(s) held (ledger unchanged; the edited test files carry no debt entry, so they are compiled and clean).
  • Ablation on the committed tree (a4f128f6b, before the main merge; the gate is byte-identical after it): the gate's condition replaced by false with a marker, proven on disk by anchored counts (if (!policy.enabled) { 1→0, marker 0→1, git diff --stat 1 file 1+/1−). The subject is imported from source (./share-link-service.js, same package — not a dependency's exports), so no dist is involved and no rebuild leg applies. Ablated run: Tests 15 failed | 61 passed (76) — exactly the 15 pins the gate protects went red; the 61 others (all Hold publicSharing.eligibility at redemption, not only at mint #13857 eligibility pins, the mint-gate pin, the enabled: true control) stayed green. Restore: git checkout HEAD -- ABSOLUTE_PATH, proven by empty git diff HEAD + clean status + equal blob hashes (d00a6e53… both sides). Direction note vs the dispatch template: pin (b) (turn the block back ON restores the token) went RED under ablation, not green — it asserts the OFF refusal first, which the ablated tree cannot produce; the ON half is unreachable there. (d) and (e) stayed green as predicted.

Gates (derived on the final head with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands, run unlocked in four batches, exit captured before any pipe)

Green (exit 0): 61 commands — node scripts/check-adr-0087-registration.mjs · node scripts/check-changeset-no-major.mjs · node scripts/check-ci-filter-parity.mjs · node scripts/check-comment-mask-adoption.mjs · node scripts/check-cross-package-test-inputs.mjs · node scripts/check-doc-frontmatter.mjs · node scripts/check-doc-route-spelling.mjs · node scripts/check-docs-section-name.mjs · node scripts/check-empty-changeset.mjs · node scripts/check-keyed-text-bounds.mjs · node scripts/check-plugin-teardown-shape.mjs · node scripts/check-section-landing-index.mjs · node scripts/check-shard-attestation.mjs · node scripts/check-system-context-census.mjs · node scripts/check-tenant-audit-census.mjs · node scripts/check-undeclared-dep-imports.mjs · node scripts/docs-audit/check-affected-docs.mjs · node scripts/docs-audit/check-drift-comment.mjs · node scripts/pm/check-half-states.mjs · node scripts/pm/release-rehearsal-clone.mjs --self-test · pnpm --filter @objectstack/lint run check:doc-formula-expressions · pnpm --filter @objectstack/lint run check:doc-security-posture · pnpm --filter @objectstack/spec run check:docs · pnpm --filter @objectstack/spec run check:empty-state · pnpm --filter @objectstack/spec run check:liveness · pnpm --filter @objectstack/spec run check:strictness-ledger · pnpm --filter @objectstack/spec run check:variant-docs · pnpm --filter @objectstack/spec run check:yaml-examples · pnpm check:changeset-gate-self-tests · pnpm check:corpus-claim-drift · pnpm check:cross-package-test-inputs · pnpm check:doc-anchors · pnpm check:doc-authoring · pnpm check:docs-audit-scope · pnpm check:docs-redirects · pnpm check:docs-single-h1 · pnpm check:engine-double-contract · pnpm check:i18n-stale-fill · pnpm check:logger-receiver-detach · pnpm check:merge-driver · pnpm check:objectql-double-limit · pnpm check:objectui-changeset · pnpm check:page-declaration-shape · pnpm check:pm-half-states · pnpm check:published-files · pnpm check:published-readme-links · pnpm check:query-options-erasure · pnpm check:react-page-adapter-contract · pnpm check:role-word · pnpm check:skill-identifier-liveness · pnpm check:slot-lookup · pnpm check:test-source-alias · pnpm check:type-check-coverage · pnpm check:type-source-resolution · pnpm check:vendor-version-stamps · pnpm check:where-matcher · pnpm check:dispatcher-error-vocabulary · pnpm check:error-code-casing · pnpm check:durability-log-level · pnpm check:optional-error-sink · pnpm check:nul-bytes

NOT MEASURED — each exited on its own stated prerequisite, none is a red:

  • node scripts/check-test-completeness.mjs — exit 3: needs a saved test-run log; the gate's own text says the local reading is NOT MEASURED.
  • pnpm check:dual-build-cjs-loads — exit 3 PREREQUISITE NOT MET: reads built output of 54 packages that have no dist/ here.
  • pnpm check:i18n — exit 1 PREREQUISITE NOT MET: the workspace CLI is not built. This diff touches no translation bundle or object metadata.
  • pnpm --filter @objectstack/spec run check:skill-examples — exit 1: packages/client-react/dist holds no .d.ts (not built). This diff touches no skill, doc example or client SDK.
  • pnpm check:type-check-debt — exit 3 PREREQUISITE NOT MET: 40 workspace dependencies of the ledgered packages are unbuilt; no ledger number was touched.
  • check:error-code-provenance — no such script in package.json.

Repo-wide pnpm lint not run locally (CI-owned); the diff is three TypeScript files in one package plus a changeset and a regenerated page.

Changeset level

minor, not patch, deliberately: scripts/check-changeset-no-major.mjs records the launch-window convention (a breaking change ships as minor; the BREAKING banner and the ADR-0087 disposition are the carriers), and the ruling says 「与 #13857 同型」 — #13857 is minor with that banner. No export changed; the level carries the breaking banner, not a surface change. ADR-0087 disposition: not-required (no-migration-prescription) — nothing authorable is removed, renamed or re-shaped; check:adr-0087-registration and check:changeset-no-major green.

Known stale prose deliberately NOT edited here (file surface per the claim) — flagged for the PM

  1. DONE in this PR (patch round 0, seat answer A on open question 1).content/docs/permissions/system-context.mdx row 37, "What you get" column used to read "link creation/resolution while the policy is off". After this change the bypass buys creation only; resolution is refused. The page is in this PR only through the census gate's own regeneration (the claim's constraint), so the one-cell correction is proposed, not made: "Get: link creation while the policy is off — resolution is NOT bypassed since [Decision] What does turning publicSharing.enabled off mean for an ALREADY-MINTED share link? — the parent switch is mint-only while its own child predicate is now a standing policy #14033: a link minted this way does not resolve until the block is enabled".
  2. content/docs/protocol/objectql/security.mdx "When eligibility is enforced" — the enabled switch now deserves the sibling paragraph and upgrade note Hold publicSharing.eligibility at redemption, not only at mint #13857 wrote for eligibility.
  3. packages/spec/src/contracts/share-link-service.tsIShareLinkService.resolveToken TSDoc lists the null causes and should name the switched-off block; packages/spec/src/data/object.zod.tspublicSharing.enabled TSDoc says "no share links can be issued" and should add "and none resolves". TSDoc only, no schema change — outside this card's surface (packages/spec/** is a stop condition for schema changes; these are prose).

Generated by Claude Code


Patch round 0 (after the seat's collection comment 14033#issuecomment-5511082583)

Generated by Claude Code


Patch round 1 (after contract review round 1 PASS, 14033#issuecomment-5511376156 — the four prose-only notes taken)

Generated by Claude Code


Generated by Claude Code

…ly at mint
`ShareLinkService.resolveToken()` now reads the object's CURRENT
`publicSharing` block on every redemption and refuses when `enabled` is
not `true` — before the record probe, before the usage stamp, before any
sibling key inside the block is evaluated. Re-enabling the block restores
the same tokens (a standing policy, not a revocation). How a link was
minted buys it nothing at redemption: rows minted under a system context
or the `permissive` bypass on a switched-off object refuse the same way,
and an object with no block at all is the same switch at its default.
The refusal reuses the undifferentiated `null` a revoked / expired /
unknown / ineligible token already gets — no new error code, no new
response branch, no usage stamp (over HTTP the generic 404). The readable
reason goes to the server-side log at `warn`, where the eligibility
refusal already writes its own.
Tests: the standing-policy pins (fake engine and real driver), the
HTTP-seam shape, the log line, the bypass-minted path, and the reversal
register for the #13856 pins that asserted serving on a disabled block.
Changeset marked as a breaking runtime change in PR #13857's form.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
…nk hunk shifted it
`check:system-context-census --fix` — pure line rot: the `permissive`
option's TSDoc grew five lines above the five `isSystem` read sites row 37
anchors in `share-link-service.ts`, so every anchor moved by exactly +5.
No site arrived or vanished (109 read sites, 145 anchors, unchanged).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
…gin/main
`pnpm gen:system-context-census` on the merged tree (os-regen-merge.sh step 4):
main's side of the page was taken at merge, then row 37's anchors re-derived
from the merged `share-link-service.ts` (+5, the `permissive` TSDoc). No read
site arrived or vanished.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
@github-actionsgithub-actionsBot added size/l documentation Improvements or additions to documentation tests tooling labels Sep 2, 2026
@github-actions

github-actionsBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/plugin-sharing, touching 3 documentable anchor(s).

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/kernel/runtime-services/sharing-service.mdx(via SHARING_NOT_ENABLED (literal, a string literal in resolveToken))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17.mdx(via SHARING_NOT_ENABLED (literal, a string literal in resolveToken))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 8 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json a5b95ee6edd1e21e04cc43200f41fb730538b197packageMentionDocs.

Which tree this was computed on

This run read content/docs from 114b3f22a35cc3c3d766b6dd3a6b69e3b9b98810 — the merge of head c55d849d768e09bb6d6a48973995b044bdc38861 into base a5b95ee6edd1e21e04cc43200f41fb730538b197, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 114b3f22a35cc3c3d766b6dd3a6b69e3b9b98810 && git checkout 114b3f22a35cc3c3d766b6dd3a6b69e3b9b98810
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin a5b95ee6edd1e21e04cc43200f41fb730538b197 c55d849d768e09bb6d6a48973995b044bdc38861 && git checkout -B drift-repro a5b95ee6edd1e21e04cc43200f41fb730538b197 && git merge --no-ff c55d849d768e09bb6d6a48973995b044bdc38861
node scripts/docs-audit/affected-docs.mjs --json a5b95ee6edd1e21e04cc43200f41fb730538b197

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs a5b95ee6edd1e21e04cc43200f41fb730538b197 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

… resolution is a standing policy
The "What you get" cell of the system-context ledger's row 37 still said
"link creation/resolution while the policy is off". Since the redemption
gate on `publicSharing.enabled`, a link minted under the system /
`permissive` bypass while the block is off does not resolve until the block
is enabled. Prose only; the five anchors are untouched (seat-approved
one-cell widening of the claim's file surface, 14033#issuecomment-5511082583).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
…gin/main (round 0)
os-regen-merge.sh step 4 on the merged tree: main's side of the page was
taken at merge, the row-37 "What you get" cell re-applied (a hand edit no
generator reproduces), then `pnpm gen:system-context-census`. Anchors
unchanged; no read site arrived or vanished.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
…, one permissive TSDoc clause, row 37 re-anchored
Prose only; the gate, the tests and the changeset level are untouched.
Changeset: the superseded #13856 reading is named; an object the engine
cannot return a schema for is `enabled: false` by `getPolicy`'s definition
and refuses at redemption (fail-closed, as `createLink`); the refusal logs
one `warn` per refused hit, not latched. `permissive` TSDoc: during a late
schema scan the option helps minting, not serving. The four added TSDoc
lines moved the five row-37 anchors, re-anchored by the census gate's own
`--fix`; the row-37 "What you get" cell is unchanged.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
…gin/main (round 1)
os-regen-merge.sh step 4 on the merged tree: main's side of the page taken
at merge, the row-37 "What you get" cell re-applied (a hand edit no
generator reproduces), then `pnpm gen:system-context-census`. No read site
arrived or vanished.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
@os-sales
os-sales marked this pull request as ready for review September 2, 2026 15:12
@os-salesClaude

Copy link
Copy Markdown
Collaborator

Landing provenance — ready + auto-merge at head c55d849d7


Generated by Claude Code

@os-sales
os-sales deleted the claude/issue-14033-public-sharing-enabled-standing-policy branch September 2, 2026 16:12
baozhoutao pushed a commit that referenced this pull request Sep 2, 2026
Item 2 of #14582 (follow-up to #14033 / PR #14580, split from #14033's
docs surfaces that PR #14580 could not touch). The "Public Share Links"
section documented the child predicate (eligibility, #13608) as a
standing policy held at every redemption, but not the parent switch
publicSharing.enabled — #14033 made that switch the same shape: held at
every redemption, not only at mint. Adds the sibling paragraph plus its
upgrade-note callout, placed after the eligibility paragraph so a reader
who has already understood the child predicate recognises the parent's
paragraph as the same shape one level up.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WLJQhde67SeTccsmnBVarV
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

2 participants

@os-sales@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(plugin-sharing): hold publicSharing.enabled at redemption, not only at mint (#14033) - #14580

Merged
os-sales merged 10 commits into
mainfrom
claude/issue-14033-public-sharing-enabled-standing-policy
Sep 2, 2026
Merged

fix(plugin-sharing): hold publicSharing.enabled at redemption, not only at mint (#14033)#14580
os-sales merged 10 commits into
mainfrom
claude/issue-14033-public-sharing-enabled-standing-policy

Conversation

@claude

@claudeclaudeBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Fixes#14033

Ruling of record (verbatim, untranslated)

Maintainer ruling of 2026-09-01, recorded by the director seat in 14033#issuecomment-5491054105 (maintainer's verbatim reply to the batch: 「其他同意」). The recorded four points, verbatim:

  1. A:publicSharing.enabled 为持续策略,resolveToken() 每次兑付重查;关掉 ⇒ 存量链接停止兑付。
  2. 追溯即时生效(沿 sharing: publicSharing.eligibility is evaluated only at mint — a link keeps serving a record after it stops being eligible #13608 先例):部署一落地,禁用块上的所有既有 token 即停止解析。changeset 必须标注 breaking runtime change,与 Hold publicSharing.eligibility at redemption, not only at mint #13857 同型。
  3. 系统/宽容旁路铸造的链接同样受管:兑付是匿名动作,不因铸造方式豁免 —— enabled 关则一律拒付(含 system-context.mdx ledger row 37 的旁路铸造路径)。
  4. 兄弟键统一律入册:enabled 关 ⇒ 整块不生效 ⇒ 拒付(子键求值 moot);enabled 开 ⇒ 块内各策略键按持续策略在兑付期求值(eligibility 已然,redactFieldssharing: turning publicSharing off leaves already-minted links serving — and silently drops the object's redactFields #13856 修复)。后续兄弟键⛔ 不再单独立卡。

And its implementation notes, verbatim:

Premise verification on origin/main (BASE 53d368921) — all five hold

  • P1 — measured, not read. The first pin (THE REPRO — minted while enabled, the SAME token stops resolving the moment the block is turned off) was run against the UNMODIFIED service before any edit: AssertionError: expected { link: { …(16) }, …(1) } to be null — the token kept serving with the block off. 15 such reds (every new/reversed pin), 61 existing pins green. Log: pre-fix run, Tests 15 failed | 61 passed (76). Premise valid.
  • P2 — PR fix(plugin-sharing): declared publicSharing.redactFields survive the object opting out #14171 (sharing: turning publicSharing off leaves already-minted links serving — and silently drops the object's redactFields #13856) pins. Two pins asserted a link on a DISABLED block still serves with redactions applied (THE REPRO — opting out keeps the declared redactions applying, boundary — the per-link redact_fields union is unchanged when the switch is off), and two reverse controls asserted a bypass-seeded link on a block-LESS object serves. All four are REVERSED in place and registered (docblock naming what each pinned, which ruling point moved it, the ruling verbatim in the block header); none deleted. The pins are agent-authored test content in share-link-service.test.ts, not governed content. The enabled: true control and the mint-gate pin are untouched.
  • P3 — PR Hold publicSharing.eligibility at redemption, not only at mint #13857 form. Its changeset (.changeset/share-link-eligibility-at-redemption.md at fc9ba76a5) is minor + the **BREAKING** runtime behaviour change on a published package … Shipped as minor under the repo's launch-window convention banner + an ADR-0087 not-required (no-migration-prescription) marker. This changeset copies that form. Hold publicSharing.eligibility at redemption, not only at mint #13857 answers the refusal with the undifferentiated null (over HTTP the generic 404 INVALID_OR_EXPIRED), reason to the server-side log at warn — the same family this refusal joins.
  • P4 — no new code. The refusal reuses the answer resolveToken already gives for revoked / expired / unknown / ineligible: null. No error code is minted, nothing under packages/spec/** is touched. check:dispatcher-error-vocabulary and check:error-code-casing green (below); a check:error-code-provenance script does not exist in package.json — reported as such, not as measured.
  • P5.content/docs/releases/**, rule-hooks.ts, sharing-plugin.ts, skills/**, packages/spec/** untouched; the redactFields logic from fix(plugin-sharing): declared publicSharing.redactFields survive the object opting out #14171 is untouched (getPolicy unchanged).

PM mechanism hypotheses — verdicts

  • H1 holds. One gate in resolveToken, at the existing policy read (after the cheap revoked / expired / audience / password gates, BEFORE loadRecordForServing, BEFORE the usage stamp, BEFORE the redaction set): if (!policy.enabled) { log; return null; }. It reads the object's CURRENT block through the same getPolicy / getSchema source and nothing about how the row was minted. Measured: on a refused redemption the only find the engine sees is the sys_share_link token lookup; use_count / last_used_at are unchanged (fake engine and real SqlDriver alike).
  • H2 holds. The code is the family the anonymous caller already receives for a link that does not serve: null at the service seam, 404 INVALID_OR_EXPIRED over HTTP, byte-for-byte equal to an unknown token (pinned at the route). SHARING_NOT_ENABLED stays the authoring-side mint refusal (422, pinned unchanged) and appears at redemption only as the reason field of the server-side log line, never on the wire (pinned: the body contains no enabled, publicsharing or sharing_not_enabled).
  • H3 holds. With the block off, eligibility is not evaluated and the redaction set is not computed (the gate returns first; pinned: a record the predicate WOULD refuse is refused by the switch with no record read and a SHARING_NOT_ENABLED log line, and switching back on the same token is refused by the predicate — RECORD_NOT_ELIGIBLE — then serves once it qualifies). With the block on, Hold publicSharing.eligibility at redemption, not only at mint #13857's 12 redemption pins and fix(plugin-sharing): declared publicSharing.redactFields survive the object opting out #14171's enabled: true control are unchanged and green. No new code path for any sibling key.
  • H4 holds. The gate reads the current block on every call; no migration, no data change; the changeset's breaking-runtime-change banner is the only artefact.
  • H5 holds.share-link-routes.ts untouched: null is already mapped (401 for a live password / signed-in row exactly as today, 410 for revoked / expired, else 404 INVALID_OR_EXPIRED). The dispatcher twin (runtime/src/domains/share-links.ts) maps null the same way.

What changed

packages/plugins/plugin-sharing/src/share-link-service.ts

  • resolveToken(): the standing-policy gate described under H1, with a docblock carrying the why, the placement argument, ruling point 3 (no mark of how a row was minted is read — an absent block is the same switch at its default and refuses too) and point 4.
  • ShareLinkServiceOptions.permissive TSDoc: states the bypass is MINT only and that a link minted under it while the block is off does not resolve until the block is enabled. This TSDoc grew five lines above the five isSystem read sites row 37 anchors — see the adjacent mechanical fix below.

packages/plugins/plugin-sharing/src/share-link-service.test.ts — the reversal register for the #13856 block (four pins reversed in place, the ruling quoted verbatim once in the block header), the two harness helpers hoisted to module scope so both blocks share them, and the [#14033] block: the repro, the standing-policy restore, the shared-null shape, and ruling point 3 on three mint paths (permissive, system context, never-declared block).

packages/plugins/plugin-sharing/src/share-link-eligibility.test.ts — the [#14033] block on the real SqlDriver and the real public route: no record read / no usage stamp, the HTTP-seam equality with an unknown token, the log line, ruling point 4 both halves on one token, and the permissive-minted link refused by the bypassing service and the ordinary one alike.

.changeset/share-link-enabled-at-redemption.md@objectstack/plugin-sharing: minor, in PR #13857's form (see "Changeset level" below).

content/docs/permissions/system-context.mdx — row 37 re-anchored by check:system-context-census --fix (adjacent mechanical fix, declared here; see below) AND its "What you get" cell hand-corrected (one cell, seat-approved in round 0): the bypass buys link creation only; resolution is not bypassed since #14033.

Clause-② declaration (from the actual diff)

git diff -U0 origin/main...HEAD | grep export on the branch: zero lines. No export is added, removed or re-shaped. The accept-set narrowing is behavioural, on the anonymous redemption door: tokens on an object whose publicSharing.enabled is not true — accepted today, refused after, retroactively on deploy, for every mint path. needs:contract-review is hung on this PR and on #14033 per the claim. Hand-edited prose in this PR beyond the code and tests: exactly one cell — the "What you get" cell of content/docs/permissions/system-context.mdx row 37 (seat-approved widening of the claimed file surface, round 0); every other change to that page is the gate's own re-anchoring.

Reading the contract review may want to judge: the gate treats an object with NO publicSharing block as switched off (it is the same switch at its default — enabled defaults to false; createLink already refuses both identically), so a bypass-minted link on a never-opted-in object refuses too. Ruling point 3 is read as requiring exactly this; exempting block-less objects would leave the row-37 path open on every object that never opted in.

Log level

One new log site, warn, through the sink's GUARANTEED member (ShareLinkServiceOptions.logger.warn), spelled this.logger?.warn?.(…) exactly like stillEligible's refusal line. Why a log at all: by ruling the wire answer is deliberately undifferentiated, so the server-side log is the ONLY place the reason a link died exists — #13857's ruling put it there and the same argument holds for the parent switch. Why not error: the caller was answered (null), so this is a response, not a durability degradation; no error? growth on the published sink shape (#13398 class ruling respected). check:durability-log-level and check:optional-error-sink green.

Adjacent mechanical fix — system-context row 37 re-anchor (+ the merge regeneration)

check:system-context-census went red on this branch with exactly the 10 problems the permissive TSDoc edit predicts: 5 [site-without-a-row] reads at :445 :499 :503 :576 :606 plus the 5 [anchor-is-not-a-read-site] citations they rotted off (:440 :494 :498 :571 :601). Remedy: the gate's own --fix (5 anchors rewritten, all +5; 109 read sites / 145 anchors unchanged — no site arrived or vanished). The pre-PR merge-tree check then listed the same page as driver-deferred against a moved origin/main, so scripts/pm/os-regen-merge.sh was run and the page regenerated as the driver instructs; see the commit list.

Commits on the branch (squash-merged as one): 1925bfafc the gate + pins + changeset · a4f128f6b census re-anchor (--fix) · 1162f24df merge of origin/main (7286dd58e) via os-regen-merge.sh · a52cfa363 census regeneration on the merged tree (pnpm gen:system-context-census). Final head: a52cfa363. The union of gates, both pin files (Tests 76 passed (76)) and the package typecheck were re-run on that head; the readings above are from those runs.

Tests and ablation (ran on the head named in each line)

  • Pre-fix measurement on BASE 53d368921 + the new pins: Tests 15 failed | 61 passed (76) — all 15 expected { link: … } to be null.
  • Post-fix, head a52cfa363: pnpm --filter @objectstack/plugin-sharing exec vitest run --maxWorkers=2 src/share-link-service.test.ts src/share-link-eligibility.test.tsTest Files 2 passed (2) / Tests 76 passed (76).
  • pnpm --filter @objectstack/plugin-sharing typecheck (tsc + tsconfig.scripts + check:test-typecheck) → exit 0; check:test-typecheck: OK — … 2 file(s) / 3 error(s) / 3 pinned signature(s) held (ledger unchanged; the edited test files carry no debt entry, so they are compiled and clean).
  • Ablation on the committed tree (a4f128f6b, before the main merge; the gate is byte-identical after it): the gate's condition replaced by false with a marker, proven on disk by anchored counts (if (!policy.enabled) { 1→0, marker 0→1, git diff --stat 1 file 1+/1−). The subject is imported from source (./share-link-service.js, same package — not a dependency's exports), so no dist is involved and no rebuild leg applies. Ablated run: Tests 15 failed | 61 passed (76) — exactly the 15 pins the gate protects went red; the 61 others (all Hold publicSharing.eligibility at redemption, not only at mint #13857 eligibility pins, the mint-gate pin, the enabled: true control) stayed green. Restore: git checkout HEAD -- ABSOLUTE_PATH, proven by empty git diff HEAD + clean status + equal blob hashes (d00a6e53… both sides). Direction note vs the dispatch template: pin (b) (turn the block back ON restores the token) went RED under ablation, not green — it asserts the OFF refusal first, which the ablated tree cannot produce; the ON half is unreachable there. (d) and (e) stayed green as predicted.

Gates (derived on the final head with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands, run unlocked in four batches, exit captured before any pipe)

Green (exit 0): 61 commands — node scripts/check-adr-0087-registration.mjs · node scripts/check-changeset-no-major.mjs · node scripts/check-ci-filter-parity.mjs · node scripts/check-comment-mask-adoption.mjs · node scripts/check-cross-package-test-inputs.mjs · node scripts/check-doc-frontmatter.mjs · node scripts/check-doc-route-spelling.mjs · node scripts/check-docs-section-name.mjs · node scripts/check-empty-changeset.mjs · node scripts/check-keyed-text-bounds.mjs · node scripts/check-plugin-teardown-shape.mjs · node scripts/check-section-landing-index.mjs · node scripts/check-shard-attestation.mjs · node scripts/check-system-context-census.mjs · node scripts/check-tenant-audit-census.mjs · node scripts/check-undeclared-dep-imports.mjs · node scripts/docs-audit/check-affected-docs.mjs · node scripts/docs-audit/check-drift-comment.mjs · node scripts/pm/check-half-states.mjs · node scripts/pm/release-rehearsal-clone.mjs --self-test · pnpm --filter @objectstack/lint run check:doc-formula-expressions · pnpm --filter @objectstack/lint run check:doc-security-posture · pnpm --filter @objectstack/spec run check:docs · pnpm --filter @objectstack/spec run check:empty-state · pnpm --filter @objectstack/spec run check:liveness · pnpm --filter @objectstack/spec run check:strictness-ledger · pnpm --filter @objectstack/spec run check:variant-docs · pnpm --filter @objectstack/spec run check:yaml-examples · pnpm check:changeset-gate-self-tests · pnpm check:corpus-claim-drift · pnpm check:cross-package-test-inputs · pnpm check:doc-anchors · pnpm check:doc-authoring · pnpm check:docs-audit-scope · pnpm check:docs-redirects · pnpm check:docs-single-h1 · pnpm check:engine-double-contract · pnpm check:i18n-stale-fill · pnpm check:logger-receiver-detach · pnpm check:merge-driver · pnpm check:objectql-double-limit · pnpm check:objectui-changeset · pnpm check:page-declaration-shape · pnpm check:pm-half-states · pnpm check:published-files · pnpm check:published-readme-links · pnpm check:query-options-erasure · pnpm check:react-page-adapter-contract · pnpm check:role-word · pnpm check:skill-identifier-liveness · pnpm check:slot-lookup · pnpm check:test-source-alias · pnpm check:type-check-coverage · pnpm check:type-source-resolution · pnpm check:vendor-version-stamps · pnpm check:where-matcher · pnpm check:dispatcher-error-vocabulary · pnpm check:error-code-casing · pnpm check:durability-log-level · pnpm check:optional-error-sink · pnpm check:nul-bytes

NOT MEASURED — each exited on its own stated prerequisite, none is a red:

  • node scripts/check-test-completeness.mjs — exit 3: needs a saved test-run log; the gate's own text says the local reading is NOT MEASURED.
  • pnpm check:dual-build-cjs-loads — exit 3 PREREQUISITE NOT MET: reads built output of 54 packages that have no dist/ here.
  • pnpm check:i18n — exit 1 PREREQUISITE NOT MET: the workspace CLI is not built. This diff touches no translation bundle or object metadata.
  • pnpm --filter @objectstack/spec run check:skill-examples — exit 1: packages/client-react/dist holds no .d.ts (not built). This diff touches no skill, doc example or client SDK.
  • pnpm check:type-check-debt — exit 3 PREREQUISITE NOT MET: 40 workspace dependencies of the ledgered packages are unbuilt; no ledger number was touched.
  • check:error-code-provenance — no such script in package.json.

Repo-wide pnpm lint not run locally (CI-owned); the diff is three TypeScript files in one package plus a changeset and a regenerated page.

Changeset level

minor, not patch, deliberately: scripts/check-changeset-no-major.mjs records the launch-window convention (a breaking change ships as minor; the BREAKING banner and the ADR-0087 disposition are the carriers), and the ruling says 「与 #13857 同型」 — #13857 is minor with that banner. No export changed; the level carries the breaking banner, not a surface change. ADR-0087 disposition: not-required (no-migration-prescription) — nothing authorable is removed, renamed or re-shaped; check:adr-0087-registration and check:changeset-no-major green.

Known stale prose deliberately NOT edited here (file surface per the claim) — flagged for the PM

  1. DONE in this PR (patch round 0, seat answer A on open question 1).content/docs/permissions/system-context.mdx row 37, "What you get" column used to read "link creation/resolution while the policy is off". After this change the bypass buys creation only; resolution is refused. The page is in this PR only through the census gate's own regeneration (the claim's constraint), so the one-cell correction is proposed, not made: "Get: link creation while the policy is off — resolution is NOT bypassed since [Decision] What does turning publicSharing.enabled off mean for an ALREADY-MINTED share link? — the parent switch is mint-only while its own child predicate is now a standing policy #14033: a link minted this way does not resolve until the block is enabled".
  2. content/docs/protocol/objectql/security.mdx "When eligibility is enforced" — the enabled switch now deserves the sibling paragraph and upgrade note Hold publicSharing.eligibility at redemption, not only at mint #13857 wrote for eligibility.
  3. packages/spec/src/contracts/share-link-service.tsIShareLinkService.resolveToken TSDoc lists the null causes and should name the switched-off block; packages/spec/src/data/object.zod.tspublicSharing.enabled TSDoc says "no share links can be issued" and should add "and none resolves". TSDoc only, no schema change — outside this card's surface (packages/spec/** is a stop condition for schema changes; these are prose).

Generated by Claude Code


Patch round 0 (after the seat's collection comment 14033#issuecomment-5511082583)

Generated by Claude Code


Patch round 1 (after contract review round 1 PASS, 14033#issuecomment-5511376156 — the four prose-only notes taken)

Generated by Claude Code


Generated by Claude Code

…ly at mint
`ShareLinkService.resolveToken()` now reads the object's CURRENT
`publicSharing` block on every redemption and refuses when `enabled` is
not `true` — before the record probe, before the usage stamp, before any
sibling key inside the block is evaluated. Re-enabling the block restores
the same tokens (a standing policy, not a revocation). How a link was
minted buys it nothing at redemption: rows minted under a system context
or the `permissive` bypass on a switched-off object refuse the same way,
and an object with no block at all is the same switch at its default.
The refusal reuses the undifferentiated `null` a revoked / expired /
unknown / ineligible token already gets — no new error code, no new
response branch, no usage stamp (over HTTP the generic 404). The readable
reason goes to the server-side log at `warn`, where the eligibility
refusal already writes its own.
Tests: the standing-policy pins (fake engine and real driver), the
HTTP-seam shape, the log line, the bypass-minted path, and the reversal
register for the #13856 pins that asserted serving on a disabled block.
Changeset marked as a breaking runtime change in PR #13857's form.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
…nk hunk shifted it
`check:system-context-census --fix` — pure line rot: the `permissive`
option's TSDoc grew five lines above the five `isSystem` read sites row 37
anchors in `share-link-service.ts`, so every anchor moved by exactly +5.
No site arrived or vanished (109 read sites, 145 anchors, unchanged).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
…gin/main
`pnpm gen:system-context-census` on the merged tree (os-regen-merge.sh step 4):
main's side of the page was taken at merge, then row 37's anchors re-derived
from the merged `share-link-service.ts` (+5, the `permissive` TSDoc). No read
site arrived or vanished.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
@github-actionsgithub-actionsBot added size/l documentation Improvements or additions to documentation tests tooling labels Sep 2, 2026
@github-actions

github-actionsBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/plugin-sharing, touching 3 documentable anchor(s).

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/kernel/runtime-services/sharing-service.mdx(via SHARING_NOT_ENABLED (literal, a string literal in resolveToken))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17.mdx(via SHARING_NOT_ENABLED (literal, a string literal in resolveToken))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 8 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json a5b95ee6edd1e21e04cc43200f41fb730538b197packageMentionDocs.

Which tree this was computed on

This run read content/docs from 114b3f22a35cc3c3d766b6dd3a6b69e3b9b98810 — the merge of head c55d849d768e09bb6d6a48973995b044bdc38861 into base a5b95ee6edd1e21e04cc43200f41fb730538b197, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 114b3f22a35cc3c3d766b6dd3a6b69e3b9b98810 && git checkout 114b3f22a35cc3c3d766b6dd3a6b69e3b9b98810
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin a5b95ee6edd1e21e04cc43200f41fb730538b197 c55d849d768e09bb6d6a48973995b044bdc38861 && git checkout -B drift-repro a5b95ee6edd1e21e04cc43200f41fb730538b197 && git merge --no-ff c55d849d768e09bb6d6a48973995b044bdc38861
node scripts/docs-audit/affected-docs.mjs --json a5b95ee6edd1e21e04cc43200f41fb730538b197

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs a5b95ee6edd1e21e04cc43200f41fb730538b197 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

… resolution is a standing policy
The "What you get" cell of the system-context ledger's row 37 still said
"link creation/resolution while the policy is off". Since the redemption
gate on `publicSharing.enabled`, a link minted under the system /
`permissive` bypass while the block is off does not resolve until the block
is enabled. Prose only; the five anchors are untouched (seat-approved
one-cell widening of the claim's file surface, 14033#issuecomment-5511082583).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
…gin/main (round 0)
os-regen-merge.sh step 4 on the merged tree: main's side of the page was
taken at merge, the row-37 "What you get" cell re-applied (a hand edit no
generator reproduces), then `pnpm gen:system-context-census`. Anchors
unchanged; no read site arrived or vanished.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
…, one permissive TSDoc clause, row 37 re-anchored
Prose only; the gate, the tests and the changeset level are untouched.
Changeset: the superseded #13856 reading is named; an object the engine
cannot return a schema for is `enabled: false` by `getPolicy`'s definition
and refuses at redemption (fail-closed, as `createLink`); the refusal logs
one `warn` per refused hit, not latched. `permissive` TSDoc: during a late
schema scan the option helps minting, not serving. The four added TSDoc
lines moved the five row-37 anchors, re-anchored by the census gate's own
`--fix`; the row-37 "What you get" cell is unchanged.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
…gin/main (round 1)
os-regen-merge.sh step 4 on the merged tree: main's side of the page taken
at merge, the row-37 "What you get" cell re-applied (a hand edit no
generator reproduces), then `pnpm gen:system-context-census`. No read site
arrived or vanished.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
@os-sales
os-sales marked this pull request as ready for review September 2, 2026 15:12
@os-salesClaude

Copy link
Copy Markdown
Collaborator

Landing provenance — ready + auto-merge at head c55d849d7


Generated by Claude Code

@os-sales
os-sales deleted the claude/issue-14033-public-sharing-enabled-standing-policy branch September 2, 2026 16:12
baozhoutao pushed a commit that referenced this pull request Sep 2, 2026
Item 2 of #14582 (follow-up to #14033 / PR #14580, split from #14033's
docs surfaces that PR #14580 could not touch). The "Public Share Links"
section documented the child predicate (eligibility, #13608) as a
standing policy held at every redemption, but not the parent switch
publicSharing.enabled — #14033 made that switch the same shape: held at
every redemption, not only at mint. Adds the sibling paragraph plus its
upgrade-note callout, placed after the eligibility paragraph so a reader
who has already understood the child predicate recognises the parent's
paragraph as the same shape one level up.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WLJQhde67SeTccsmnBVarV
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

2 participants

@os-sales@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(plugin-sharing): hold publicSharing.enabled at redemption, not only at mint (#14033) - #14580

Merged
os-sales merged 10 commits into
mainfrom
claude/issue-14033-public-sharing-enabled-standing-policy
Sep 2, 2026
Merged

fix(plugin-sharing): hold publicSharing.enabled at redemption, not only at mint (#14033)#14580
os-sales merged 10 commits into
mainfrom
claude/issue-14033-public-sharing-enabled-standing-policy

Conversation

@claude

@claudeclaudeBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Fixes#14033

Ruling of record (verbatim, untranslated)

Maintainer ruling of 2026-09-01, recorded by the director seat in 14033#issuecomment-5491054105 (maintainer's verbatim reply to the batch: 「其他同意」). The recorded four points, verbatim:

  1. A:publicSharing.enabled 为持续策略,resolveToken() 每次兑付重查;关掉 ⇒ 存量链接停止兑付。
  2. 追溯即时生效(沿 sharing: publicSharing.eligibility is evaluated only at mint — a link keeps serving a record after it stops being eligible #13608 先例):部署一落地,禁用块上的所有既有 token 即停止解析。changeset 必须标注 breaking runtime change,与 Hold publicSharing.eligibility at redemption, not only at mint #13857 同型。
  3. 系统/宽容旁路铸造的链接同样受管:兑付是匿名动作,不因铸造方式豁免 —— enabled 关则一律拒付(含 system-context.mdx ledger row 37 的旁路铸造路径)。
  4. 兄弟键统一律入册:enabled 关 ⇒ 整块不生效 ⇒ 拒付(子键求值 moot);enabled 开 ⇒ 块内各策略键按持续策略在兑付期求值(eligibility 已然,redactFieldssharing: turning publicSharing off leaves already-minted links serving — and silently drops the object's redactFields #13856 修复)。后续兄弟键⛔ 不再单独立卡。

And its implementation notes, verbatim:

Premise verification on origin/main (BASE 53d368921) — all five hold

  • P1 — measured, not read. The first pin (THE REPRO — minted while enabled, the SAME token stops resolving the moment the block is turned off) was run against the UNMODIFIED service before any edit: AssertionError: expected { link: { …(16) }, …(1) } to be null — the token kept serving with the block off. 15 such reds (every new/reversed pin), 61 existing pins green. Log: pre-fix run, Tests 15 failed | 61 passed (76). Premise valid.
  • P2 — PR fix(plugin-sharing): declared publicSharing.redactFields survive the object opting out #14171 (sharing: turning publicSharing off leaves already-minted links serving — and silently drops the object's redactFields #13856) pins. Two pins asserted a link on a DISABLED block still serves with redactions applied (THE REPRO — opting out keeps the declared redactions applying, boundary — the per-link redact_fields union is unchanged when the switch is off), and two reverse controls asserted a bypass-seeded link on a block-LESS object serves. All four are REVERSED in place and registered (docblock naming what each pinned, which ruling point moved it, the ruling verbatim in the block header); none deleted. The pins are agent-authored test content in share-link-service.test.ts, not governed content. The enabled: true control and the mint-gate pin are untouched.
  • P3 — PR Hold publicSharing.eligibility at redemption, not only at mint #13857 form. Its changeset (.changeset/share-link-eligibility-at-redemption.md at fc9ba76a5) is minor + the **BREAKING** runtime behaviour change on a published package … Shipped as minor under the repo's launch-window convention banner + an ADR-0087 not-required (no-migration-prescription) marker. This changeset copies that form. Hold publicSharing.eligibility at redemption, not only at mint #13857 answers the refusal with the undifferentiated null (over HTTP the generic 404 INVALID_OR_EXPIRED), reason to the server-side log at warn — the same family this refusal joins.
  • P4 — no new code. The refusal reuses the answer resolveToken already gives for revoked / expired / unknown / ineligible: null. No error code is minted, nothing under packages/spec/** is touched. check:dispatcher-error-vocabulary and check:error-code-casing green (below); a check:error-code-provenance script does not exist in package.json — reported as such, not as measured.
  • P5.content/docs/releases/**, rule-hooks.ts, sharing-plugin.ts, skills/**, packages/spec/** untouched; the redactFields logic from fix(plugin-sharing): declared publicSharing.redactFields survive the object opting out #14171 is untouched (getPolicy unchanged).

PM mechanism hypotheses — verdicts

  • H1 holds. One gate in resolveToken, at the existing policy read (after the cheap revoked / expired / audience / password gates, BEFORE loadRecordForServing, BEFORE the usage stamp, BEFORE the redaction set): if (!policy.enabled) { log; return null; }. It reads the object's CURRENT block through the same getPolicy / getSchema source and nothing about how the row was minted. Measured: on a refused redemption the only find the engine sees is the sys_share_link token lookup; use_count / last_used_at are unchanged (fake engine and real SqlDriver alike).
  • H2 holds. The code is the family the anonymous caller already receives for a link that does not serve: null at the service seam, 404 INVALID_OR_EXPIRED over HTTP, byte-for-byte equal to an unknown token (pinned at the route). SHARING_NOT_ENABLED stays the authoring-side mint refusal (422, pinned unchanged) and appears at redemption only as the reason field of the server-side log line, never on the wire (pinned: the body contains no enabled, publicsharing or sharing_not_enabled).
  • H3 holds. With the block off, eligibility is not evaluated and the redaction set is not computed (the gate returns first; pinned: a record the predicate WOULD refuse is refused by the switch with no record read and a SHARING_NOT_ENABLED log line, and switching back on the same token is refused by the predicate — RECORD_NOT_ELIGIBLE — then serves once it qualifies). With the block on, Hold publicSharing.eligibility at redemption, not only at mint #13857's 12 redemption pins and fix(plugin-sharing): declared publicSharing.redactFields survive the object opting out #14171's enabled: true control are unchanged and green. No new code path for any sibling key.
  • H4 holds. The gate reads the current block on every call; no migration, no data change; the changeset's breaking-runtime-change banner is the only artefact.
  • H5 holds.share-link-routes.ts untouched: null is already mapped (401 for a live password / signed-in row exactly as today, 410 for revoked / expired, else 404 INVALID_OR_EXPIRED). The dispatcher twin (runtime/src/domains/share-links.ts) maps null the same way.

What changed

packages/plugins/plugin-sharing/src/share-link-service.ts

  • resolveToken(): the standing-policy gate described under H1, with a docblock carrying the why, the placement argument, ruling point 3 (no mark of how a row was minted is read — an absent block is the same switch at its default and refuses too) and point 4.
  • ShareLinkServiceOptions.permissive TSDoc: states the bypass is MINT only and that a link minted under it while the block is off does not resolve until the block is enabled. This TSDoc grew five lines above the five isSystem read sites row 37 anchors — see the adjacent mechanical fix below.

packages/plugins/plugin-sharing/src/share-link-service.test.ts — the reversal register for the #13856 block (four pins reversed in place, the ruling quoted verbatim once in the block header), the two harness helpers hoisted to module scope so both blocks share them, and the [#14033] block: the repro, the standing-policy restore, the shared-null shape, and ruling point 3 on three mint paths (permissive, system context, never-declared block).

packages/plugins/plugin-sharing/src/share-link-eligibility.test.ts — the [#14033] block on the real SqlDriver and the real public route: no record read / no usage stamp, the HTTP-seam equality with an unknown token, the log line, ruling point 4 both halves on one token, and the permissive-minted link refused by the bypassing service and the ordinary one alike.

.changeset/share-link-enabled-at-redemption.md@objectstack/plugin-sharing: minor, in PR #13857's form (see "Changeset level" below).

content/docs/permissions/system-context.mdx — row 37 re-anchored by check:system-context-census --fix (adjacent mechanical fix, declared here; see below) AND its "What you get" cell hand-corrected (one cell, seat-approved in round 0): the bypass buys link creation only; resolution is not bypassed since #14033.

Clause-② declaration (from the actual diff)

git diff -U0 origin/main...HEAD | grep export on the branch: zero lines. No export is added, removed or re-shaped. The accept-set narrowing is behavioural, on the anonymous redemption door: tokens on an object whose publicSharing.enabled is not true — accepted today, refused after, retroactively on deploy, for every mint path. needs:contract-review is hung on this PR and on #14033 per the claim. Hand-edited prose in this PR beyond the code and tests: exactly one cell — the "What you get" cell of content/docs/permissions/system-context.mdx row 37 (seat-approved widening of the claimed file surface, round 0); every other change to that page is the gate's own re-anchoring.

Reading the contract review may want to judge: the gate treats an object with NO publicSharing block as switched off (it is the same switch at its default — enabled defaults to false; createLink already refuses both identically), so a bypass-minted link on a never-opted-in object refuses too. Ruling point 3 is read as requiring exactly this; exempting block-less objects would leave the row-37 path open on every object that never opted in.

Log level

One new log site, warn, through the sink's GUARANTEED member (ShareLinkServiceOptions.logger.warn), spelled this.logger?.warn?.(…) exactly like stillEligible's refusal line. Why a log at all: by ruling the wire answer is deliberately undifferentiated, so the server-side log is the ONLY place the reason a link died exists — #13857's ruling put it there and the same argument holds for the parent switch. Why not error: the caller was answered (null), so this is a response, not a durability degradation; no error? growth on the published sink shape (#13398 class ruling respected). check:durability-log-level and check:optional-error-sink green.

Adjacent mechanical fix — system-context row 37 re-anchor (+ the merge regeneration)

check:system-context-census went red on this branch with exactly the 10 problems the permissive TSDoc edit predicts: 5 [site-without-a-row] reads at :445 :499 :503 :576 :606 plus the 5 [anchor-is-not-a-read-site] citations they rotted off (:440 :494 :498 :571 :601). Remedy: the gate's own --fix (5 anchors rewritten, all +5; 109 read sites / 145 anchors unchanged — no site arrived or vanished). The pre-PR merge-tree check then listed the same page as driver-deferred against a moved origin/main, so scripts/pm/os-regen-merge.sh was run and the page regenerated as the driver instructs; see the commit list.

Commits on the branch (squash-merged as one): 1925bfafc the gate + pins + changeset · a4f128f6b census re-anchor (--fix) · 1162f24df merge of origin/main (7286dd58e) via os-regen-merge.sh · a52cfa363 census regeneration on the merged tree (pnpm gen:system-context-census). Final head: a52cfa363. The union of gates, both pin files (Tests 76 passed (76)) and the package typecheck were re-run on that head; the readings above are from those runs.

Tests and ablation (ran on the head named in each line)

  • Pre-fix measurement on BASE 53d368921 + the new pins: Tests 15 failed | 61 passed (76) — all 15 expected { link: … } to be null.
  • Post-fix, head a52cfa363: pnpm --filter @objectstack/plugin-sharing exec vitest run --maxWorkers=2 src/share-link-service.test.ts src/share-link-eligibility.test.tsTest Files 2 passed (2) / Tests 76 passed (76).
  • pnpm --filter @objectstack/plugin-sharing typecheck (tsc + tsconfig.scripts + check:test-typecheck) → exit 0; check:test-typecheck: OK — … 2 file(s) / 3 error(s) / 3 pinned signature(s) held (ledger unchanged; the edited test files carry no debt entry, so they are compiled and clean).
  • Ablation on the committed tree (a4f128f6b, before the main merge; the gate is byte-identical after it): the gate's condition replaced by false with a marker, proven on disk by anchored counts (if (!policy.enabled) { 1→0, marker 0→1, git diff --stat 1 file 1+/1−). The subject is imported from source (./share-link-service.js, same package — not a dependency's exports), so no dist is involved and no rebuild leg applies. Ablated run: Tests 15 failed | 61 passed (76) — exactly the 15 pins the gate protects went red; the 61 others (all Hold publicSharing.eligibility at redemption, not only at mint #13857 eligibility pins, the mint-gate pin, the enabled: true control) stayed green. Restore: git checkout HEAD -- ABSOLUTE_PATH, proven by empty git diff HEAD + clean status + equal blob hashes (d00a6e53… both sides). Direction note vs the dispatch template: pin (b) (turn the block back ON restores the token) went RED under ablation, not green — it asserts the OFF refusal first, which the ablated tree cannot produce; the ON half is unreachable there. (d) and (e) stayed green as predicted.

Gates (derived on the final head with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands, run unlocked in four batches, exit captured before any pipe)

Green (exit 0): 61 commands — node scripts/check-adr-0087-registration.mjs · node scripts/check-changeset-no-major.mjs · node scripts/check-ci-filter-parity.mjs · node scripts/check-comment-mask-adoption.mjs · node scripts/check-cross-package-test-inputs.mjs · node scripts/check-doc-frontmatter.mjs · node scripts/check-doc-route-spelling.mjs · node scripts/check-docs-section-name.mjs · node scripts/check-empty-changeset.mjs · node scripts/check-keyed-text-bounds.mjs · node scripts/check-plugin-teardown-shape.mjs · node scripts/check-section-landing-index.mjs · node scripts/check-shard-attestation.mjs · node scripts/check-system-context-census.mjs · node scripts/check-tenant-audit-census.mjs · node scripts/check-undeclared-dep-imports.mjs · node scripts/docs-audit/check-affected-docs.mjs · node scripts/docs-audit/check-drift-comment.mjs · node scripts/pm/check-half-states.mjs · node scripts/pm/release-rehearsal-clone.mjs --self-test · pnpm --filter @objectstack/lint run check:doc-formula-expressions · pnpm --filter @objectstack/lint run check:doc-security-posture · pnpm --filter @objectstack/spec run check:docs · pnpm --filter @objectstack/spec run check:empty-state · pnpm --filter @objectstack/spec run check:liveness · pnpm --filter @objectstack/spec run check:strictness-ledger · pnpm --filter @objectstack/spec run check:variant-docs · pnpm --filter @objectstack/spec run check:yaml-examples · pnpm check:changeset-gate-self-tests · pnpm check:corpus-claim-drift · pnpm check:cross-package-test-inputs · pnpm check:doc-anchors · pnpm check:doc-authoring · pnpm check:docs-audit-scope · pnpm check:docs-redirects · pnpm check:docs-single-h1 · pnpm check:engine-double-contract · pnpm check:i18n-stale-fill · pnpm check:logger-receiver-detach · pnpm check:merge-driver · pnpm check:objectql-double-limit · pnpm check:objectui-changeset · pnpm check:page-declaration-shape · pnpm check:pm-half-states · pnpm check:published-files · pnpm check:published-readme-links · pnpm check:query-options-erasure · pnpm check:react-page-adapter-contract · pnpm check:role-word · pnpm check:skill-identifier-liveness · pnpm check:slot-lookup · pnpm check:test-source-alias · pnpm check:type-check-coverage · pnpm check:type-source-resolution · pnpm check:vendor-version-stamps · pnpm check:where-matcher · pnpm check:dispatcher-error-vocabulary · pnpm check:error-code-casing · pnpm check:durability-log-level · pnpm check:optional-error-sink · pnpm check:nul-bytes

NOT MEASURED — each exited on its own stated prerequisite, none is a red:

  • node scripts/check-test-completeness.mjs — exit 3: needs a saved test-run log; the gate's own text says the local reading is NOT MEASURED.
  • pnpm check:dual-build-cjs-loads — exit 3 PREREQUISITE NOT MET: reads built output of 54 packages that have no dist/ here.
  • pnpm check:i18n — exit 1 PREREQUISITE NOT MET: the workspace CLI is not built. This diff touches no translation bundle or object metadata.
  • pnpm --filter @objectstack/spec run check:skill-examples — exit 1: packages/client-react/dist holds no .d.ts (not built). This diff touches no skill, doc example or client SDK.
  • pnpm check:type-check-debt — exit 3 PREREQUISITE NOT MET: 40 workspace dependencies of the ledgered packages are unbuilt; no ledger number was touched.
  • check:error-code-provenance — no such script in package.json.

Repo-wide pnpm lint not run locally (CI-owned); the diff is three TypeScript files in one package plus a changeset and a regenerated page.

Changeset level

minor, not patch, deliberately: scripts/check-changeset-no-major.mjs records the launch-window convention (a breaking change ships as minor; the BREAKING banner and the ADR-0087 disposition are the carriers), and the ruling says 「与 #13857 同型」 — #13857 is minor with that banner. No export changed; the level carries the breaking banner, not a surface change. ADR-0087 disposition: not-required (no-migration-prescription) — nothing authorable is removed, renamed or re-shaped; check:adr-0087-registration and check:changeset-no-major green.

Known stale prose deliberately NOT edited here (file surface per the claim) — flagged for the PM

  1. DONE in this PR (patch round 0, seat answer A on open question 1).content/docs/permissions/system-context.mdx row 37, "What you get" column used to read "link creation/resolution while the policy is off". After this change the bypass buys creation only; resolution is refused. The page is in this PR only through the census gate's own regeneration (the claim's constraint), so the one-cell correction is proposed, not made: "Get: link creation while the policy is off — resolution is NOT bypassed since [Decision] What does turning publicSharing.enabled off mean for an ALREADY-MINTED share link? — the parent switch is mint-only while its own child predicate is now a standing policy #14033: a link minted this way does not resolve until the block is enabled".
  2. content/docs/protocol/objectql/security.mdx "When eligibility is enforced" — the enabled switch now deserves the sibling paragraph and upgrade note Hold publicSharing.eligibility at redemption, not only at mint #13857 wrote for eligibility.
  3. packages/spec/src/contracts/share-link-service.tsIShareLinkService.resolveToken TSDoc lists the null causes and should name the switched-off block; packages/spec/src/data/object.zod.tspublicSharing.enabled TSDoc says "no share links can be issued" and should add "and none resolves". TSDoc only, no schema change — outside this card's surface (packages/spec/** is a stop condition for schema changes; these are prose).

Generated by Claude Code


Patch round 0 (after the seat's collection comment 14033#issuecomment-5511082583)

Generated by Claude Code


Patch round 1 (after contract review round 1 PASS, 14033#issuecomment-5511376156 — the four prose-only notes taken)

Generated by Claude Code


Generated by Claude Code

…ly at mint
`ShareLinkService.resolveToken()` now reads the object's CURRENT
`publicSharing` block on every redemption and refuses when `enabled` is
not `true` — before the record probe, before the usage stamp, before any
sibling key inside the block is evaluated. Re-enabling the block restores
the same tokens (a standing policy, not a revocation). How a link was
minted buys it nothing at redemption: rows minted under a system context
or the `permissive` bypass on a switched-off object refuse the same way,
and an object with no block at all is the same switch at its default.
The refusal reuses the undifferentiated `null` a revoked / expired /
unknown / ineligible token already gets — no new error code, no new
response branch, no usage stamp (over HTTP the generic 404). The readable
reason goes to the server-side log at `warn`, where the eligibility
refusal already writes its own.
Tests: the standing-policy pins (fake engine and real driver), the
HTTP-seam shape, the log line, the bypass-minted path, and the reversal
register for the #13856 pins that asserted serving on a disabled block.
Changeset marked as a breaking runtime change in PR #13857's form.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
…nk hunk shifted it
`check:system-context-census --fix` — pure line rot: the `permissive`
option's TSDoc grew five lines above the five `isSystem` read sites row 37
anchors in `share-link-service.ts`, so every anchor moved by exactly +5.
No site arrived or vanished (109 read sites, 145 anchors, unchanged).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
…gin/main
`pnpm gen:system-context-census` on the merged tree (os-regen-merge.sh step 4):
main's side of the page was taken at merge, then row 37's anchors re-derived
from the merged `share-link-service.ts` (+5, the `permissive` TSDoc). No read
site arrived or vanished.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
@github-actionsgithub-actionsBot added size/l documentation Improvements or additions to documentation tests tooling labels Sep 2, 2026
@github-actions

github-actionsBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/plugin-sharing, touching 3 documentable anchor(s).

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/kernel/runtime-services/sharing-service.mdx(via SHARING_NOT_ENABLED (literal, a string literal in resolveToken))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17.mdx(via SHARING_NOT_ENABLED (literal, a string literal in resolveToken))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 8 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json a5b95ee6edd1e21e04cc43200f41fb730538b197packageMentionDocs.

Which tree this was computed on

This run read content/docs from 114b3f22a35cc3c3d766b6dd3a6b69e3b9b98810 — the merge of head c55d849d768e09bb6d6a48973995b044bdc38861 into base a5b95ee6edd1e21e04cc43200f41fb730538b197, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 114b3f22a35cc3c3d766b6dd3a6b69e3b9b98810 && git checkout 114b3f22a35cc3c3d766b6dd3a6b69e3b9b98810
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin a5b95ee6edd1e21e04cc43200f41fb730538b197 c55d849d768e09bb6d6a48973995b044bdc38861 && git checkout -B drift-repro a5b95ee6edd1e21e04cc43200f41fb730538b197 && git merge --no-ff c55d849d768e09bb6d6a48973995b044bdc38861
node scripts/docs-audit/affected-docs.mjs --json a5b95ee6edd1e21e04cc43200f41fb730538b197

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs a5b95ee6edd1e21e04cc43200f41fb730538b197 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

… resolution is a standing policy
The "What you get" cell of the system-context ledger's row 37 still said
"link creation/resolution while the policy is off". Since the redemption
gate on `publicSharing.enabled`, a link minted under the system /
`permissive` bypass while the block is off does not resolve until the block
is enabled. Prose only; the five anchors are untouched (seat-approved
one-cell widening of the claim's file surface, 14033#issuecomment-5511082583).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
…gin/main (round 0)
os-regen-merge.sh step 4 on the merged tree: main's side of the page was
taken at merge, the row-37 "What you get" cell re-applied (a hand edit no
generator reproduces), then `pnpm gen:system-context-census`. Anchors
unchanged; no read site arrived or vanished.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
…, one permissive TSDoc clause, row 37 re-anchored
Prose only; the gate, the tests and the changeset level are untouched.
Changeset: the superseded #13856 reading is named; an object the engine
cannot return a schema for is `enabled: false` by `getPolicy`'s definition
and refuses at redemption (fail-closed, as `createLink`); the refusal logs
one `warn` per refused hit, not latched. `permissive` TSDoc: during a late
schema scan the option helps minting, not serving. The four added TSDoc
lines moved the five row-37 anchors, re-anchored by the census gate's own
`--fix`; the row-37 "What you get" cell is unchanged.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
…gin/main (round 1)
os-regen-merge.sh step 4 on the merged tree: main's side of the page taken
at merge, the row-37 "What you get" cell re-applied (a hand edit no
generator reproduces), then `pnpm gen:system-context-census`. No read site
arrived or vanished.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
@os-sales
os-sales marked this pull request as ready for review September 2, 2026 15:12
@os-salesClaude

Copy link
Copy Markdown
Collaborator

Landing provenance — ready + auto-merge at head c55d849d7


Generated by Claude Code

@os-sales
os-sales deleted the claude/issue-14033-public-sharing-enabled-standing-policy branch September 2, 2026 16:12
baozhoutao pushed a commit that referenced this pull request Sep 2, 2026
Item 2 of #14582 (follow-up to #14033 / PR #14580, split from #14033's
docs surfaces that PR #14580 could not touch). The "Public Share Links"
section documented the child predicate (eligibility, #13608) as a
standing policy held at every redemption, but not the parent switch
publicSharing.enabled — #14033 made that switch the same shape: held at
every redemption, not only at mint. Adds the sibling paragraph plus its
upgrade-note callout, placed after the eligibility paragraph so a reader
who has already understood the child predicate recognises the parent's
paragraph as the same shape one level up.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WLJQhde67SeTccsmnBVarV
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

2 participants

@os-sales@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix(plugin-sharing): hold publicSharing.enabled at redemption, not only at mint (#14033) - #14580

Merged
os-sales merged 10 commits into
mainfrom
claude/issue-14033-public-sharing-enabled-standing-policy
Sep 2, 2026
Merged

fix(plugin-sharing): hold publicSharing.enabled at redemption, not only at mint (#14033)#14580
os-sales merged 10 commits into
mainfrom
claude/issue-14033-public-sharing-enabled-standing-policy

Conversation

@claude

@claudeclaudeBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Fixes#14033

Ruling of record (verbatim, untranslated)

Maintainer ruling of 2026-09-01, recorded by the director seat in 14033#issuecomment-5491054105 (maintainer's verbatim reply to the batch: 「其他同意」). The recorded four points, verbatim:

  1. A:publicSharing.enabled 为持续策略,resolveToken() 每次兑付重查;关掉 ⇒ 存量链接停止兑付。
  2. 追溯即时生效(沿 sharing: publicSharing.eligibility is evaluated only at mint — a link keeps serving a record after it stops being eligible #13608 先例):部署一落地,禁用块上的所有既有 token 即停止解析。changeset 必须标注 breaking runtime change,与 Hold publicSharing.eligibility at redemption, not only at mint #13857 同型。
  3. 系统/宽容旁路铸造的链接同样受管:兑付是匿名动作,不因铸造方式豁免 —— enabled 关则一律拒付(含 system-context.mdx ledger row 37 的旁路铸造路径)。
  4. 兄弟键统一律入册:enabled 关 ⇒ 整块不生效 ⇒ 拒付(子键求值 moot);enabled 开 ⇒ 块内各策略键按持续策略在兑付期求值(eligibility 已然,redactFieldssharing: turning publicSharing off leaves already-minted links serving — and silently drops the object's redactFields #13856 修复)。后续兄弟键⛔ 不再单独立卡。

And its implementation notes, verbatim:

Premise verification on origin/main (BASE 53d368921) — all five hold

  • P1 — measured, not read. The first pin (THE REPRO — minted while enabled, the SAME token stops resolving the moment the block is turned off) was run against the UNMODIFIED service before any edit: AssertionError: expected { link: { …(16) }, …(1) } to be null — the token kept serving with the block off. 15 such reds (every new/reversed pin), 61 existing pins green. Log: pre-fix run, Tests 15 failed | 61 passed (76). Premise valid.
  • P2 — PR fix(plugin-sharing): declared publicSharing.redactFields survive the object opting out #14171 (sharing: turning publicSharing off leaves already-minted links serving — and silently drops the object's redactFields #13856) pins. Two pins asserted a link on a DISABLED block still serves with redactions applied (THE REPRO — opting out keeps the declared redactions applying, boundary — the per-link redact_fields union is unchanged when the switch is off), and two reverse controls asserted a bypass-seeded link on a block-LESS object serves. All four are REVERSED in place and registered (docblock naming what each pinned, which ruling point moved it, the ruling verbatim in the block header); none deleted. The pins are agent-authored test content in share-link-service.test.ts, not governed content. The enabled: true control and the mint-gate pin are untouched.
  • P3 — PR Hold publicSharing.eligibility at redemption, not only at mint #13857 form. Its changeset (.changeset/share-link-eligibility-at-redemption.md at fc9ba76a5) is minor + the **BREAKING** runtime behaviour change on a published package … Shipped as minor under the repo's launch-window convention banner + an ADR-0087 not-required (no-migration-prescription) marker. This changeset copies that form. Hold publicSharing.eligibility at redemption, not only at mint #13857 answers the refusal with the undifferentiated null (over HTTP the generic 404 INVALID_OR_EXPIRED), reason to the server-side log at warn — the same family this refusal joins.
  • P4 — no new code. The refusal reuses the answer resolveToken already gives for revoked / expired / unknown / ineligible: null. No error code is minted, nothing under packages/spec/** is touched. check:dispatcher-error-vocabulary and check:error-code-casing green (below); a check:error-code-provenance script does not exist in package.json — reported as such, not as measured.
  • P5.content/docs/releases/**, rule-hooks.ts, sharing-plugin.ts, skills/**, packages/spec/** untouched; the redactFields logic from fix(plugin-sharing): declared publicSharing.redactFields survive the object opting out #14171 is untouched (getPolicy unchanged).

PM mechanism hypotheses — verdicts

  • H1 holds. One gate in resolveToken, at the existing policy read (after the cheap revoked / expired / audience / password gates, BEFORE loadRecordForServing, BEFORE the usage stamp, BEFORE the redaction set): if (!policy.enabled) { log; return null; }. It reads the object's CURRENT block through the same getPolicy / getSchema source and nothing about how the row was minted. Measured: on a refused redemption the only find the engine sees is the sys_share_link token lookup; use_count / last_used_at are unchanged (fake engine and real SqlDriver alike).
  • H2 holds. The code is the family the anonymous caller already receives for a link that does not serve: null at the service seam, 404 INVALID_OR_EXPIRED over HTTP, byte-for-byte equal to an unknown token (pinned at the route). SHARING_NOT_ENABLED stays the authoring-side mint refusal (422, pinned unchanged) and appears at redemption only as the reason field of the server-side log line, never on the wire (pinned: the body contains no enabled, publicsharing or sharing_not_enabled).
  • H3 holds. With the block off, eligibility is not evaluated and the redaction set is not computed (the gate returns first; pinned: a record the predicate WOULD refuse is refused by the switch with no record read and a SHARING_NOT_ENABLED log line, and switching back on the same token is refused by the predicate — RECORD_NOT_ELIGIBLE — then serves once it qualifies). With the block on, Hold publicSharing.eligibility at redemption, not only at mint #13857's 12 redemption pins and fix(plugin-sharing): declared publicSharing.redactFields survive the object opting out #14171's enabled: true control are unchanged and green. No new code path for any sibling key.
  • H4 holds. The gate reads the current block on every call; no migration, no data change; the changeset's breaking-runtime-change banner is the only artefact.
  • H5 holds.share-link-routes.ts untouched: null is already mapped (401 for a live password / signed-in row exactly as today, 410 for revoked / expired, else 404 INVALID_OR_EXPIRED). The dispatcher twin (runtime/src/domains/share-links.ts) maps null the same way.

What changed

packages/plugins/plugin-sharing/src/share-link-service.ts

  • resolveToken(): the standing-policy gate described under H1, with a docblock carrying the why, the placement argument, ruling point 3 (no mark of how a row was minted is read — an absent block is the same switch at its default and refuses too) and point 4.
  • ShareLinkServiceOptions.permissive TSDoc: states the bypass is MINT only and that a link minted under it while the block is off does not resolve until the block is enabled. This TSDoc grew five lines above the five isSystem read sites row 37 anchors — see the adjacent mechanical fix below.

packages/plugins/plugin-sharing/src/share-link-service.test.ts — the reversal register for the #13856 block (four pins reversed in place, the ruling quoted verbatim once in the block header), the two harness helpers hoisted to module scope so both blocks share them, and the [#14033] block: the repro, the standing-policy restore, the shared-null shape, and ruling point 3 on three mint paths (permissive, system context, never-declared block).

packages/plugins/plugin-sharing/src/share-link-eligibility.test.ts — the [#14033] block on the real SqlDriver and the real public route: no record read / no usage stamp, the HTTP-seam equality with an unknown token, the log line, ruling point 4 both halves on one token, and the permissive-minted link refused by the bypassing service and the ordinary one alike.

.changeset/share-link-enabled-at-redemption.md@objectstack/plugin-sharing: minor, in PR #13857's form (see "Changeset level" below).

content/docs/permissions/system-context.mdx — row 37 re-anchored by check:system-context-census --fix (adjacent mechanical fix, declared here; see below) AND its "What you get" cell hand-corrected (one cell, seat-approved in round 0): the bypass buys link creation only; resolution is not bypassed since #14033.

Clause-② declaration (from the actual diff)

git diff -U0 origin/main...HEAD | grep export on the branch: zero lines. No export is added, removed or re-shaped. The accept-set narrowing is behavioural, on the anonymous redemption door: tokens on an object whose publicSharing.enabled is not true — accepted today, refused after, retroactively on deploy, for every mint path. needs:contract-review is hung on this PR and on #14033 per the claim. Hand-edited prose in this PR beyond the code and tests: exactly one cell — the "What you get" cell of content/docs/permissions/system-context.mdx row 37 (seat-approved widening of the claimed file surface, round 0); every other change to that page is the gate's own re-anchoring.

Reading the contract review may want to judge: the gate treats an object with NO publicSharing block as switched off (it is the same switch at its default — enabled defaults to false; createLink already refuses both identically), so a bypass-minted link on a never-opted-in object refuses too. Ruling point 3 is read as requiring exactly this; exempting block-less objects would leave the row-37 path open on every object that never opted in.

Log level

One new log site, warn, through the sink's GUARANTEED member (ShareLinkServiceOptions.logger.warn), spelled this.logger?.warn?.(…) exactly like stillEligible's refusal line. Why a log at all: by ruling the wire answer is deliberately undifferentiated, so the server-side log is the ONLY place the reason a link died exists — #13857's ruling put it there and the same argument holds for the parent switch. Why not error: the caller was answered (null), so this is a response, not a durability degradation; no error? growth on the published sink shape (#13398 class ruling respected). check:durability-log-level and check:optional-error-sink green.

Adjacent mechanical fix — system-context row 37 re-anchor (+ the merge regeneration)

check:system-context-census went red on this branch with exactly the 10 problems the permissive TSDoc edit predicts: 5 [site-without-a-row] reads at :445 :499 :503 :576 :606 plus the 5 [anchor-is-not-a-read-site] citations they rotted off (:440 :494 :498 :571 :601). Remedy: the gate's own --fix (5 anchors rewritten, all +5; 109 read sites / 145 anchors unchanged — no site arrived or vanished). The pre-PR merge-tree check then listed the same page as driver-deferred against a moved origin/main, so scripts/pm/os-regen-merge.sh was run and the page regenerated as the driver instructs; see the commit list.

Commits on the branch (squash-merged as one): 1925bfafc the gate + pins + changeset · a4f128f6b census re-anchor (--fix) · 1162f24df merge of origin/main (7286dd58e) via os-regen-merge.sh · a52cfa363 census regeneration on the merged tree (pnpm gen:system-context-census). Final head: a52cfa363. The union of gates, both pin files (Tests 76 passed (76)) and the package typecheck were re-run on that head; the readings above are from those runs.

Tests and ablation (ran on the head named in each line)

  • Pre-fix measurement on BASE 53d368921 + the new pins: Tests 15 failed | 61 passed (76) — all 15 expected { link: … } to be null.
  • Post-fix, head a52cfa363: pnpm --filter @objectstack/plugin-sharing exec vitest run --maxWorkers=2 src/share-link-service.test.ts src/share-link-eligibility.test.tsTest Files 2 passed (2) / Tests 76 passed (76).
  • pnpm --filter @objectstack/plugin-sharing typecheck (tsc + tsconfig.scripts + check:test-typecheck) → exit 0; check:test-typecheck: OK — … 2 file(s) / 3 error(s) / 3 pinned signature(s) held (ledger unchanged; the edited test files carry no debt entry, so they are compiled and clean).
  • Ablation on the committed tree (a4f128f6b, before the main merge; the gate is byte-identical after it): the gate's condition replaced by false with a marker, proven on disk by anchored counts (if (!policy.enabled) { 1→0, marker 0→1, git diff --stat 1 file 1+/1−). The subject is imported from source (./share-link-service.js, same package — not a dependency's exports), so no dist is involved and no rebuild leg applies. Ablated run: Tests 15 failed | 61 passed (76) — exactly the 15 pins the gate protects went red; the 61 others (all Hold publicSharing.eligibility at redemption, not only at mint #13857 eligibility pins, the mint-gate pin, the enabled: true control) stayed green. Restore: git checkout HEAD -- ABSOLUTE_PATH, proven by empty git diff HEAD + clean status + equal blob hashes (d00a6e53… both sides). Direction note vs the dispatch template: pin (b) (turn the block back ON restores the token) went RED under ablation, not green — it asserts the OFF refusal first, which the ablated tree cannot produce; the ON half is unreachable there. (d) and (e) stayed green as predicted.

Gates (derived on the final head with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands, run unlocked in four batches, exit captured before any pipe)

Green (exit 0): 61 commands — node scripts/check-adr-0087-registration.mjs · node scripts/check-changeset-no-major.mjs · node scripts/check-ci-filter-parity.mjs · node scripts/check-comment-mask-adoption.mjs · node scripts/check-cross-package-test-inputs.mjs · node scripts/check-doc-frontmatter.mjs · node scripts/check-doc-route-spelling.mjs · node scripts/check-docs-section-name.mjs · node scripts/check-empty-changeset.mjs · node scripts/check-keyed-text-bounds.mjs · node scripts/check-plugin-teardown-shape.mjs · node scripts/check-section-landing-index.mjs · node scripts/check-shard-attestation.mjs · node scripts/check-system-context-census.mjs · node scripts/check-tenant-audit-census.mjs · node scripts/check-undeclared-dep-imports.mjs · node scripts/docs-audit/check-affected-docs.mjs · node scripts/docs-audit/check-drift-comment.mjs · node scripts/pm/check-half-states.mjs · node scripts/pm/release-rehearsal-clone.mjs --self-test · pnpm --filter @objectstack/lint run check:doc-formula-expressions · pnpm --filter @objectstack/lint run check:doc-security-posture · pnpm --filter @objectstack/spec run check:docs · pnpm --filter @objectstack/spec run check:empty-state · pnpm --filter @objectstack/spec run check:liveness · pnpm --filter @objectstack/spec run check:strictness-ledger · pnpm --filter @objectstack/spec run check:variant-docs · pnpm --filter @objectstack/spec run check:yaml-examples · pnpm check:changeset-gate-self-tests · pnpm check:corpus-claim-drift · pnpm check:cross-package-test-inputs · pnpm check:doc-anchors · pnpm check:doc-authoring · pnpm check:docs-audit-scope · pnpm check:docs-redirects · pnpm check:docs-single-h1 · pnpm check:engine-double-contract · pnpm check:i18n-stale-fill · pnpm check:logger-receiver-detach · pnpm check:merge-driver · pnpm check:objectql-double-limit · pnpm check:objectui-changeset · pnpm check:page-declaration-shape · pnpm check:pm-half-states · pnpm check:published-files · pnpm check:published-readme-links · pnpm check:query-options-erasure · pnpm check:react-page-adapter-contract · pnpm check:role-word · pnpm check:skill-identifier-liveness · pnpm check:slot-lookup · pnpm check:test-source-alias · pnpm check:type-check-coverage · pnpm check:type-source-resolution · pnpm check:vendor-version-stamps · pnpm check:where-matcher · pnpm check:dispatcher-error-vocabulary · pnpm check:error-code-casing · pnpm check:durability-log-level · pnpm check:optional-error-sink · pnpm check:nul-bytes

NOT MEASURED — each exited on its own stated prerequisite, none is a red:

  • node scripts/check-test-completeness.mjs — exit 3: needs a saved test-run log; the gate's own text says the local reading is NOT MEASURED.
  • pnpm check:dual-build-cjs-loads — exit 3 PREREQUISITE NOT MET: reads built output of 54 packages that have no dist/ here.
  • pnpm check:i18n — exit 1 PREREQUISITE NOT MET: the workspace CLI is not built. This diff touches no translation bundle or object metadata.
  • pnpm --filter @objectstack/spec run check:skill-examples — exit 1: packages/client-react/dist holds no .d.ts (not built). This diff touches no skill, doc example or client SDK.
  • pnpm check:type-check-debt — exit 3 PREREQUISITE NOT MET: 40 workspace dependencies of the ledgered packages are unbuilt; no ledger number was touched.
  • check:error-code-provenance — no such script in package.json.

Repo-wide pnpm lint not run locally (CI-owned); the diff is three TypeScript files in one package plus a changeset and a regenerated page.

Changeset level

minor, not patch, deliberately: scripts/check-changeset-no-major.mjs records the launch-window convention (a breaking change ships as minor; the BREAKING banner and the ADR-0087 disposition are the carriers), and the ruling says 「与 #13857 同型」 — #13857 is minor with that banner. No export changed; the level carries the breaking banner, not a surface change. ADR-0087 disposition: not-required (no-migration-prescription) — nothing authorable is removed, renamed or re-shaped; check:adr-0087-registration and check:changeset-no-major green.

Known stale prose deliberately NOT edited here (file surface per the claim) — flagged for the PM

  1. DONE in this PR (patch round 0, seat answer A on open question 1).content/docs/permissions/system-context.mdx row 37, "What you get" column used to read "link creation/resolution while the policy is off". After this change the bypass buys creation only; resolution is refused. The page is in this PR only through the census gate's own regeneration (the claim's constraint), so the one-cell correction is proposed, not made: "Get: link creation while the policy is off — resolution is NOT bypassed since [Decision] What does turning publicSharing.enabled off mean for an ALREADY-MINTED share link? — the parent switch is mint-only while its own child predicate is now a standing policy #14033: a link minted this way does not resolve until the block is enabled".
  2. content/docs/protocol/objectql/security.mdx "When eligibility is enforced" — the enabled switch now deserves the sibling paragraph and upgrade note Hold publicSharing.eligibility at redemption, not only at mint #13857 wrote for eligibility.
  3. packages/spec/src/contracts/share-link-service.tsIShareLinkService.resolveToken TSDoc lists the null causes and should name the switched-off block; packages/spec/src/data/object.zod.tspublicSharing.enabled TSDoc says "no share links can be issued" and should add "and none resolves". TSDoc only, no schema change — outside this card's surface (packages/spec/** is a stop condition for schema changes; these are prose).

Generated by Claude Code


Patch round 0 (after the seat's collection comment 14033#issuecomment-5511082583)

Generated by Claude Code


Patch round 1 (after contract review round 1 PASS, 14033#issuecomment-5511376156 — the four prose-only notes taken)

Generated by Claude Code


Generated by Claude Code

…ly at mint
`ShareLinkService.resolveToken()` now reads the object's CURRENT
`publicSharing` block on every redemption and refuses when `enabled` is
not `true` — before the record probe, before the usage stamp, before any
sibling key inside the block is evaluated. Re-enabling the block restores
the same tokens (a standing policy, not a revocation). How a link was
minted buys it nothing at redemption: rows minted under a system context
or the `permissive` bypass on a switched-off object refuse the same way,
and an object with no block at all is the same switch at its default.
The refusal reuses the undifferentiated `null` a revoked / expired /
unknown / ineligible token already gets — no new error code, no new
response branch, no usage stamp (over HTTP the generic 404). The readable
reason goes to the server-side log at `warn`, where the eligibility
refusal already writes its own.
Tests: the standing-policy pins (fake engine and real driver), the
HTTP-seam shape, the log line, the bypass-minted path, and the reversal
register for the #13856 pins that asserted serving on a disabled block.
Changeset marked as a breaking runtime change in PR #13857's form.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
…nk hunk shifted it
`check:system-context-census --fix` — pure line rot: the `permissive`
option's TSDoc grew five lines above the five `isSystem` read sites row 37
anchors in `share-link-service.ts`, so every anchor moved by exactly +5.
No site arrived or vanished (109 read sites, 145 anchors, unchanged).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
…gin/main
`pnpm gen:system-context-census` on the merged tree (os-regen-merge.sh step 4):
main's side of the page was taken at merge, then row 37's anchors re-derived
from the merged `share-link-service.ts` (+5, the `permissive` TSDoc). No read
site arrived or vanished.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
@github-actionsgithub-actionsBot added size/l documentation Improvements or additions to documentation tests tooling labels Sep 2, 2026
@github-actions

github-actionsBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/plugin-sharing, touching 3 documentable anchor(s).

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/kernel/runtime-services/sharing-service.mdx(via SHARING_NOT_ENABLED (literal, a string literal in resolveToken))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17.mdx(via SHARING_NOT_ENABLED (literal, a string literal in resolveToken))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 8 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json a5b95ee6edd1e21e04cc43200f41fb730538b197packageMentionDocs.

Which tree this was computed on

This run read content/docs from 114b3f22a35cc3c3d766b6dd3a6b69e3b9b98810 — the merge of head c55d849d768e09bb6d6a48973995b044bdc38861 into base a5b95ee6edd1e21e04cc43200f41fb730538b197, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 114b3f22a35cc3c3d766b6dd3a6b69e3b9b98810 && git checkout 114b3f22a35cc3c3d766b6dd3a6b69e3b9b98810
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin a5b95ee6edd1e21e04cc43200f41fb730538b197 c55d849d768e09bb6d6a48973995b044bdc38861 && git checkout -B drift-repro a5b95ee6edd1e21e04cc43200f41fb730538b197 && git merge --no-ff c55d849d768e09bb6d6a48973995b044bdc38861
node scripts/docs-audit/affected-docs.mjs --json a5b95ee6edd1e21e04cc43200f41fb730538b197

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs a5b95ee6edd1e21e04cc43200f41fb730538b197 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

… resolution is a standing policy
The "What you get" cell of the system-context ledger's row 37 still said
"link creation/resolution while the policy is off". Since the redemption
gate on `publicSharing.enabled`, a link minted under the system /
`permissive` bypass while the block is off does not resolve until the block
is enabled. Prose only; the five anchors are untouched (seat-approved
one-cell widening of the claim's file surface, 14033#issuecomment-5511082583).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
…gin/main (round 0)
os-regen-merge.sh step 4 on the merged tree: main's side of the page was
taken at merge, the row-37 "What you get" cell re-applied (a hand edit no
generator reproduces), then `pnpm gen:system-context-census`. Anchors
unchanged; no read site arrived or vanished.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
…, one permissive TSDoc clause, row 37 re-anchored
Prose only; the gate, the tests and the changeset level are untouched.
Changeset: the superseded #13856 reading is named; an object the engine
cannot return a schema for is `enabled: false` by `getPolicy`'s definition
and refuses at redemption (fail-closed, as `createLink`); the refusal logs
one `warn` per refused hit, not latched. `permissive` TSDoc: during a late
schema scan the option helps minting, not serving. The four added TSDoc
lines moved the five row-37 anchors, re-anchored by the census gate's own
`--fix`; the row-37 "What you get" cell is unchanged.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
…gin/main (round 1)
os-regen-merge.sh step 4 on the merged tree: main's side of the page taken
at merge, the row-37 "What you get" cell re-applied (a hand edit no
generator reproduces), then `pnpm gen:system-context-census`. No read site
arrived or vanished.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
@os-sales
os-sales marked this pull request as ready for review September 2, 2026 15:12
@os-salesClaude

Copy link
Copy Markdown
Collaborator

Landing provenance — ready + auto-merge at head c55d849d7


Generated by Claude Code

@os-sales
os-sales deleted the claude/issue-14033-public-sharing-enabled-standing-policy branch September 2, 2026 16:12
baozhoutao pushed a commit that referenced this pull request Sep 2, 2026
Item 2 of #14582 (follow-up to #14033 / PR #14580, split from #14033's
docs surfaces that PR #14580 could not touch). The "Public Share Links"
section documented the child predicate (eligibility, #13608) as a
standing policy held at every redemption, but not the parent switch
publicSharing.enabled — #14033 made that switch the same shape: held at
every redemption, not only at mint. Adds the sibling paragraph plus its
upgrade-note callout, placed after the eligibility paragraph so a reader
who has already understood the child predicate recognises the parent's
paragraph as the same shape one level up.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WLJQhde67SeTccsmnBVarV
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

2 participants

@os-sales@claude