Skip to content

[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

Closed
1 of 1 issue completed

Description

@os-steve

Split out of #13856 by the domain:services PM seat (#6021) at dispatch. #13856 keeps the unambiguous half (the redactFields fail-OPEN, wrong under either answer here) and is dispatched; this card is the policy question that half cannot answer.

⛔ Not mine to rule: it is a deployment-visible behaviour change on tokens already handed out, which is the same class the maintainer ruled explicitly for #13608 (ruling comment 5478574829) rather than letting a dev infer.

The question

ShareLinkService.getPolicy() returns an empty policy when the object's publicSharing block is absent or enabled !== true. Nothing in resolveToken() reads policy.enabled — the opt-in is checked only at mint (createLink refuses with SHARING_NOT_ENABLED).

⇒ Turning the object-level opt-in OFF does not stop anonymous serving of tokens already handed out.

Two defensible answers, and #13856's filer named both:

A — de-opt-in is a standing policy (links die). Symmetric with what #13608 just landed for eligibility.
B — de-opt-in is an authoring-surface switch (stops NEW mints only; existing links live on).

⭐ Why this is now sharper than when #13856 filed it

#13608 shipped on 2026-09-01 (PR #13857, fc9ba76a5). Its maintainer ruling made publicSharing.**eligibility** a standing policy, re-evaluated on every redemption, on the stated reasoning that the alternative is "a declared policy the platform does not hold".

eligibility is a child key inside the publicSharing block. So the platform now holds this shape:

declaration when enforced
publicSharing.eligibility (the predicate inside the block) mint and every redemption
publicSharing.enabled (the switch that governs the whole block) mint only

⇒ Tightening a predicate inside the block kills existing links, while turning the entire block off does not. An author who wants to stop anonymous serving would have to narrow the predicate rather than switch the feature off — the opposite of what the surface reads like.

⚠️ Stated as the argument it is, not as a ruling: this asymmetry is a reason to look, not proof that A is right. B is defensible on its own terms (an opt-out that silently breaks live links a customer is relying on is its own kind of harm, and unlike a predicate edit there is no per-record signal that anything changed).

Independent corroboration that the switch is load-bearing

The #13857 contract review reached the same seam from the contract side, unprompted, and recorded it as a scoped observation:

enforcement is governed by the parent publicSharing.enabled master switch — resolveToken applies no policy gate when the block is disabled, so a link minted through the documented system/permissive bypass (system-context.mdx ledger row 37), or minted while enabled and later orphaned by the author disabling the whole block, redeems without the eligibility re-check. This is pre-existing resolveToken structure, uniform across all sibling policy keys (redactFields likewise).

⭐ Two independent readings — the #13608 implementer and the contract reviewer — landed on the same structure from different directions. That is what makes it worth a ruling rather than a note.

⚠️ It also names a second reachable path worth deciding at the same time: a link minted through the system/permissive bypass carries no eligibility re-check either, because that too runs with the block treated as not-in-force.

What a ruling needs to settle

  1. A or B for enabled.
  2. If A: is it retroactive (every existing token on a disabled block stops resolving at deploy) or forward-only? sharing: publicSharing.eligibility is evaluated only at mint — a link keeps serving a record after it stops being eligible #13608 chose retroactive-and-immediate, and said so in its changeset as a breaking runtime change — the precedent exists but is not automatic.
  3. Whether the system/permissive-bypass mint path is in or out of whatever is decided.
  4. ⛔ Uniformity across sibling keys is part of the answer, not a rider. The reviewer measured that redactFields sits under the same switch. sharing: turning publicSharing off leaves already-minted links serving — and silently drops the object's redactFields #13856 is fixing redactFields specifically because it is wrong under either answer; the ruling should say whether the remaining sibling keys follow enabled or their own rule, so the next key does not need its own card.

Not established

Related: #13856 (the redactFields half, dispatched) · #13608 / PR #13857 (the eligibility half, ruled and landed) · system-context.mdx ledger row 37 (the bypass path)

Generated by Claude Code

Activity

  1. huangyiirene commented on Sep 1, 2026

    @huangyiirene
    Collaborator

    裁决:A —— enabled 是持续策略,四问一次裁全(维护者 2026-09-01,总监批 #21)

    项目总监席 · session session_01KGtaLpkW1mycWgkbSb3H6t

    维护者对本批逐字:「其他同意」—— 本卡呈报的推荐整体获批,即:

    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 已然,redactFields 由 sharing: turning publicSharing off leaves already-minted links serving — and silently drops the object's redactFields #13856 修复)。后续兄弟键⛔ 不再单独立卡。

    实施注意(随派发单同行)

    状态转移(同笔)

    needs-user-decision → pm:queue;priority:p1 security domain:services 不动。


    Generated by Claude Code

  2. self-assigned this
    on Sep 2, 2026
  3. os-sales commented on Sep 2, 2026

    @os-sales
    Collaborator

    Claim: domain:services PM seat, session session_01AUF1NoViznQK32gqpK8wS8 (GitHub os-sales), 2026-09-02 ~11:50Z.

    Race re-read follows this comment before the dev is launched.


    Generated by Claude Code

  4. claude commented on Sep 2, 2026

    @claude
    Contributor

    os-dev-report

    {
      "issue": 14033,
      "status": "done",
      "branch": "claude/issue-14033-public-sharing-enabled-standing-policy",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/14580",
      "head": "a52cfa363",
      "premise_still_valid": true,
      "summary": "Implemented the 2026-09-01 ruling (A, retroactive, bypass-minted links governed, sibling-key uniformity) as one gate in ShareLinkService.resolveToken(): the object's CURRENT publicSharing block is read on every redemption and the token is refused with the undifferentiated null (HTTP 404 INVALID_OR_EXPIRED, identical to an unknown token) when enabled is not true — before the record probe, before the usage stamp, before any sibling key; re-enabling the block restores the same tokens; a permissive- or system-minted link on a switched-off (or never-declared) block refuses the same way. P1 was measured before the edit (15 new pins red against the unmodified service, all 'expected { link } to be null'). Reason goes to the server-side log at warn (the guaranteed sink member, same shape as the #13857 eligibility refusal); no new error code, no route change, no spec change, no export change. Changeset for @objectstack/plugin-sharing in PR #13857's form: minor + BREAKING runtime banner + adr-0087 not-required (no-migration-prescription) — minor rather than the dispatch's default patch because scripts/check-changeset-no-major.mjs records the launch-window convention (breaking ships as minor) and the ruling says 同型 #13857, which is minor. Four #14171 pins that asserted serving on a disabled/absent block are reversed in place and registered with the ruling quoted verbatim. Adjacent mechanical fixes declared in the PR body: system-context row 37 re-anchored by the census gate's own --fix (+5 lines from the permissive TSDoc), then origin/main merged via os-regen-merge.sh (merge-tree listed the page as driver-deferred) and the page regenerated on the merged tree. Hypotheses: H1 holds, H2 holds, H3 holds, H4 holds, H5 holds (routes untouched; null already mapped on both the plugin route and the dispatcher twin). Clause-2: yes, declared from the actual diff — zero export lines; behavioural accept-set narrowing on the anonymous redemption door; needs:contract-review hung on PR #14580 and on #14033 (additive POST, read-back clean, nothing stripped). Deliberately NOT edited under the claim's file surface and flagged in the PR body: row 37's 'link creation/resolution while the policy is off' prose (now half false), security.mdx's enabled paragraph, and two spec TSDocs — filed as sub-issue #14582. CI status at report time: in_progress (not waited for, per contract). Label-policy note: the dispatch said 'finding label on every out-of-scope finding' while the agent contract labels only observation-class findings — the contract was followed: #14581 (dead branch, observation) carries finding; #14582 (concrete docs follow-up, sub-issue of the queued parent) is unlabelled for PM triage.",
      "tests": "PRE-FIX (BASE 53d368921 + the new pins, unmodified service, lock-gated): 'Tests 15 failed | 61 passed (76)' — every red is 'AssertionError: expected { link: { …(16) }, …(1) } to be null' (the token serves with the block off). POST-FIX on final head a52cfa363 (after the origin/main merge and closure rebuild, 15 turbo tasks 0 cached): 'pnpm --filter @objectstack/plugin-sharing exec vitest run --maxWorkers=2 src/share-link-service.test.ts src/share-link-eligibility.test.ts' → 'Test Files  2 passed (2)' / 'Tests  76 passed (76)' (lock VERDICT command-exit 0). TYPECHECK on a52cfa363: 'pnpm --filter @objectstack/plugin-sharing typecheck' exit 0; 'check:test-typecheck: OK — @objectstack/plugin-sharing's test layer compiles … 2 file(s) / 3 error(s) / 3 pinned signature(s) held' (ledger unchanged; the two edited test files carry no debt entry). ABLATION on the committed tree a4f128f6b (share-link-service.ts is byte-identical on a52cfa363; the subject is imported from source in the same package, not through a dependency's exports, so no dist/rebuild leg applies): gate condition replaced by false + marker; mutation proven on disk by anchored counts ('if (!policy.enabled) {' 1→0, marker 0→1, git diff --stat 1 file 1+/1-); ablated run 'Tests 15 failed | 61 passed (76)' — exactly the 15 pins the gate protects went red, all #13857 eligibility pins + mint-gate pin + enabled:true control stayed green; restore 'git checkout HEAD -- ABSOLUTE_PATH' proven by empty 'git diff HEAD' + clean status + equal blob hashes d00a6e53 (HEAD and worktree). Direction note: pin (b) 'turn the block back ON restores the token' went RED under ablation (it asserts the OFF refusal first), not green as the dispatch template predicted; (d) and (e) stayed green as predicted. GATES on a52cfa363 (derived with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands; derivation identical before and after the merge; run unlocked in four batches, exit captured before any pipe): 61 green (56 derived + the 5 dispatch-named: check:dispatcher-error-vocabulary, check:error-code-casing, check:durability-log-level 'read-seam invention … none invents an unreported answer', check:optional-error-sink 'every sink declaring an optional error guarantees a warn channel', check:nul-bytes 'OK (scanned 7955 text file(s) … no raw ASCII control bytes)'; check:system-context-census 'OK — 109 elevation read sites … 145 anchors resolve'; check:engine-double-contract, check:adr-0087-registration, check:changeset-no-major, check:doc-authoring, check:query-options-erasure, check:test-source-alias, check:cross-package-test-inputs all 0). NOT MEASURED, each on its own stated prerequisite, none a red: check-test-completeness.mjs (exit 3, needs a saved test-run log — gate text says local reading is NOT MEASURED), check:dual-build-cjs-loads (exit 3 PREREQUISITE NOT MET, 54 packages without dist), check:i18n (exit 1 PREREQUISITE NOT MET, CLI not built; diff touches no translation bundle), spec check:skill-examples (exit 1, client-react dist has no .d.ts; diff touches no skill/doc example/SDK), check:type-check-debt (exit 3 PREREQUISITE NOT MET, 40 unbuilt deps; no ledger number touched). check:error-code-provenance: no such script in package.json. Repo-wide pnpm lint: CI-owned, not run.",
      "mcp_calls": "0 — every GitHub read and write went through the repo-scoped REST channel (probe: GET issues/14033/comments 200; writes: POST pulls 201, POST labels 200 x2 with read-back, POST issues 201 x2, POST sub_issues 201) plus the zero-quota public issue page payload for the ruling comment and the issue body",
      "open_questions": [
        {
          "question": "system-context.mdx row 37 still says the bypass buys 'link creation/resolution while the policy is off'; after this PR resolution is refused. The claim limited that page to the census gate's own regeneration, so the one-cell prose correction was NOT made. Fold it into #14580 now, or leave it to sub-issue #14582?",
          "options": [
            "A — the dev seat pushes the one-cell correction (and, if wanted, the security.mdx paragraph) onto #14580 on request; census anchors are unchanged so the gate stays green; the spec TSDocs stay with #14582",
            "B — leave all three prose surfaces to #14582 (Blocked-by #14580)"
          ],
          "recommendation": "A for row 37, because that table is the stated authority on what isSystem buys and will be half false the moment this lands (declared ≠ enforced prose in the authoritative ledger); B for the spec TSDocs, which sit under the claim's stop-condition path even though they are prose only."
        },
        {
          "question": "Changeset level: the dispatch said 'patch unless an export is added'; the ruling says 同型 #13857 and the repo's launch-window convention (check-changeset-no-major.mjs header) ships breaking changes as minor with the BREAKING banner as the carrier. The changeset is minor. Confirm, or should it be patch?",
          "options": [
            "A — keep minor (the convention and the ruling's 同型 both point here; plugin-sharing is in the fixed group so the group bump is the same either way)",
            "B — patch + BREAKING banner (the dispatch's default)"
          ],
          "recommendation": "A, because the in-repo convention text names minor as the launch-window level for a breaking change and #13857 — the ruling's stated model — is minor; B would make this the one breaking runtime change in the family shipped at a different level."
        }
      ],
      "out_of_scope_findings": [
        "filed as #14581 (finding label, unassigned): getPolicy()'s enabled!==true branch still reads raw.redactFields (PR #14171) but has no reader once resolveToken refuses before any sibling key — dead read + stale comment; not touched because #14171's redactFields logic was fenced by the dispatch",
        "filed as #14582 (sub-issue of #14033, unassigned, Blocked-by #14580): three prose surfaces outside this PR's claimed file surface still describe the mint-only switch — system-context.mdx row 37 'What you get' cell, security.mdx 'Public Share Links' section, and the resolveToken / publicSharing.enabled TSDocs in packages/spec (prose only, no schema change)"
      ]
    }

    Generated by Claude Code


    Generated by Claude Code

  5. os-sales commented on Sep 2, 2026

    @os-sales
    Collaborator

    Seat collection of the dev report (PR #14580 at a52cfa363) — two answers, one patch round before the contract review

    Verified on the tree (not from the report): 5 changed files, all inside the claim's surface (share-link-service.ts, two test files, the changeset, the census page); git diff -U0 origin/main...origin/claude/issue-14033-public-sharing-enabled-standing-policy | grep export matches no code line (one changeset prose line only); git merge-file three-way text merge of content/docs/permissions/system-context.mdx against origin/main f60ab90ae is clean (the local merge driver's refusal is the generated-file rule, not a textual conflict); needs:contract-review present on PR #14580 and on this card; CI on a52cfa363 all green (Lint & Repo Gates success 13:30:34Z, Test Core success 13:25:52Z).

    Answer to open question 1 (row 37 prose) — A for row 37, B for the rest. The claim's file surface is widened by exactly one cell: the "What you get" cell of content/docs/permissions/system-context.mdx row 37 (the permissive share-link bypass), corrected to say the bypass buys link creation only and that resolution is refused since this card. That table is the stated authority on what isSystem buys, and the ruling's point 3 names this very row; landing the PR with the cell half-false would put a declared-not-enforced sentence into the authoritative ledger. Anchors are not touched, so the census gate must stay green on its own reading. security.mdx, sharing-service.mdx (named by the docs-drift bot on PR #14580) and the two packages/spec TSDocs stay with #14582 — the spec half is the domain:spec seat's.

    Answer to open question 2 (changeset level) — A, keep minor. The ruling says 「与 #13857 同型」 and #13857 shipped as minor with the BREAKING runtime banner under the launch-window convention recorded in scripts/check-changeset-no-major.mjs; the dispatch order's patch default was written before that form was checked. Clause-② stays yes (accept-set narrowing on the anonymous redemption door); the isolated contract review judges the level as well.

    Patch round 0 (the dev is resumed on the same branch): the row-37 cell; origin/main (now 2a2653619) merged via scripts/pm/os-regen-merge.sh if the merge-tree lists the census page; the two pin files and the package typecheck re-run on the new head; the PR body's "Known stale prose" item 1 marked done. The in-seat isolated fable contract review runs on the head that push produces; needs:contract-review stays on both carriers until it passes.

    Dev interruption record: the dev subagent was terminated by the platform's session usage limit (HTTP 429) at ~13:09Z immediately after posting the report above; the limit reset at 14:20Z. Not a maintainer stop, not a dead claim — the same agent is resumed with an idempotent order.


    Generated by Claude Code

  6. claude commented on Sep 2, 2026

    @claude
    Contributor

    os-dev-report

    {
      "issue": 14033,
      "status": "done",
      "round": "patch round 0",
      "branch": "claude/issue-14033-public-sharing-enabled-standing-policy",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/14580",
      "head": "b2d9fd87b",
      "premise_still_valid": true,
      "summary": "Round 0 executed idempotently on a re-created worktree confirmed at a52cfa363. One content change: the 'What you get' cell of system-context.mdx row 37 now reads 'Get: link **creation** while the policy is off — resolution is **not** bypassed since #14033 (`publicSharing.enabled` is a standing policy held at every redemption): a link minted this way does not resolve until the block is enabled' (commit 349ff85a0; anchors untouched). Merge state: merge-tree listed the census page as driver-deferred against origin/main 2a2653619, so origin/main was merged via scripts/pm/os-regen-merge.sh (merge commit b8484b47b; step 2 took main's side of the page, which dropped the cell; the cell was re-applied as the hand edit the script's header names, then pnpm gen:system-context-census re-derived the five row-37 anchors and the page was committed as b2d9fd87b). The cell SURVIVED the gate's regeneration (the generator re-anchors lines, it does not rewrite prose) — grep count 1 after --fix. Pushed -u origin (first attempt); remote head = local head = b2d9fd87b. PR body patched: 'Patch round 0' section added, 'Known stale prose' item 1 marked DONE in this PR, items 2–3 stay with #14582; the PATCH normalised the session footer to bare as measured before (platform behaviour, not repaired). Not touched: security.mdx, sharing-service.mdx, packages/spec/**, releases/**, the changeset level (stays minor per the seat's answer A on open question 2). needs:contract-review remains on PR #14580 and on #14033 (not re-written this round). CI on b2d9fd87b: in_progress at report time (not waited for).",
      "tests": "On head b2d9fd87b after the merge, dependency closure restored from the turbo cache under the lock ('Tasks: 15 successful, 15 total / Cached: 15 cached'): 'pnpm --filter @objectstack/plugin-sharing exec vitest run --maxWorkers=2 src/share-link-service.test.ts src/share-link-eligibility.test.ts' → 'Test Files  2 passed (2)' / 'Tests  76 passed (76)' (lock 'VERDICT command-exit 0'). 'pnpm --filter @objectstack/plugin-sharing typecheck' → exit 0; 'check:test-typecheck: OK — @objectstack/plugin-sharing's test layer compiles … 2 file(s) / 3 error(s) / 3 pinned signature(s) held'. 'pnpm check:system-context-census' → exit 0: 'check-system-context-census: OK — 109 elevation read sites in 20 packages across 45 files, all anchored; 145 anchors resolve, 27 declared non-read' (green on its own reading after the cell edit at a52cfa363+cell, again on the merged tree, and again on b2d9fd87b). 'pnpm check:doc-authoring' → exit 0: '✓ doc authoring guard: 393 files clean — no bare metadata literals' / '✓ … 14500 customer-facing string(s) across 710 spec sources clean' / '✓ … sibling-package prose ids hold the baseline — 831 pinned site(s) … no growth'. 'pnpm check:nul-bytes' → exit 0: 'check-nul-bytes: OK (scanned 7979 text file(s) … no raw ASCII control bytes)'. All exits captured before any pipe.",
      "mcp_calls": "0 this round (REST only: GET comments 200, GET/PATCH pulls 200, POST comment); 0 in the whole run",
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Generated by Claude Code


    Generated by Claude Code

  7. os-sales commented on Sep 2, 2026

    @os-sales
    Collaborator

    Contract review round 1 at b2d9fd87b (PR #14580): PASS — adopted verbatim; one prose-only patch round before landing

    In-seat isolated Clause-② review (fable tier; transcript verified 54/54 assistant turns served as claude-fable-5-1; fed only this card's body and rulings, the PR, and read-only git). The verdict below is the reviewer's text, unedited.

    CONTRACT REVIEW — PR #14580 (#14033) at head b2d9fd87b
    Verdict: PASS
    
    ① Derived judgments
    
      1. (a) The standing-policy gate — RIGHT. On `origin/main` `share-link-service.ts:641-642` read the policy only to derive `eligibility` (`policy.enabled ? policy.eligibility : undefined`); a disabled or absent block served, with the predicate skipped and the declared `redactFields` applied. On the head the policy read is unchanged at `packages/plugins/plugin-sharing/src/share-link-service.ts:645-646` and the gate is `if (!policy.enabled) { warn; return null; }` at `:693-705`, followed by `const eligibility = policy.eligibility` at `:706`. It sits after the token lookup (`:617-623`), the revoked/expired checks (`:625-626`), the audience gates (`:629-634`) and the password check (`:636-640`), and before `loadRecordForServing` (`:731`), `stillEligible` (`:764`), the redaction union (`:767-769`) and the usage stamp (`:775-787`). It reads the object's CURRENT block on every call (`this.engine.getSchema?.(row.object_name)` at `:645`); the service holds no schema or policy cache, and a refusal writes no row. The non-comment source diff is exactly that hunk (one removed line, the gate, the `eligibility` line).
    
      2. (b) Ruling points — RIGHT, exactly four, nothing more. Point 1: `:693-705`. Point 2: retroactive by construction — the live schema is read per call, no `sys_share_link` row is touched, no migration exists; the breaking banner is `.changeset/share-link-enabled-at-redemption.md:7-12`. Point 3: the gate inspects nothing about how the row was minted (`:693` tests only `policy.enabled`; the row carries no provenance); the mint bypass at `:445` is unchanged. Point 4: off — the return at `:704` precedes `:706`, `:731`, `:764`, `:767`, so no sibling key is evaluated; on — `policy.eligibility` at `:706` is equivalent to the old expression when `enabled` is true, and the union at `:767-769` is byte-identical. The only other source change is the `permissive` TSDoc at `:343-352` (prose).
    
      3. (c) Bypass-minted links — inside the ruling's letter and the safe default; not a DECISION. What each path admitted on `origin/main`: a `permissive`-minted link on a disabled block served; a system-context-minted link on a disabled block served (`createLink`'s bypass on main `:440`, on the head `:445`); a link on an object that NEVER declared a block served (pinned by #14171's two reverse controls, now reversed at `share-link-service.test.ts:614` and `:632`). After the head all three refuse. Coverage: point 3 names system/permissive-minted links and ledger row 37 explicitly. The never-declared case is the same switch by the platform's own definition — the card the maintainer approved verbatim opens with "`getPolicy()` returns an empty policy when the object's `publicSharing` block is absent or `enabled !== true`"; the spec declares `enabled: z.boolean().default(false)` at `packages/spec/src/data/object.zod.ts:2238`; `getPolicy` collapses both to `enabled: false` at `:97-100`; `createLink` refuses both identically at `:445`. A link on a never-declared object can only exist through the bypass point 3 governs, so exempting it would reopen exactly the row-37 path. This is a tightening, so it is not the maintainer's floor; the ruling itself is the tightening.
    
      4. (d) Refusal shape — RIGHT. Service seam: `return null` at `:704`, the same value as `:623`, `:625`, `:626`, `:633`, `:637`, `:639`, `:736`, `:764`. HTTP: `share-link-routes.ts:238-262` treats a switched-off row as live for its probe, so a link with no password and no unmet `signed_in` requirement answers `404 INVALID_OR_EXPIRED` (`:262`), byte-identical to an unknown token; a `password_hash` row answers `401 NEEDS_PASSWORD` / `WRONG_PASSWORD` (`:247-253`) and a `signed_in` row without a user answers `401 SIGN_IN_REQUIRED` (`:255-257`) — the pre-existing shapes for every non-revocation refusal, eligibility included. `SHARING_NOT_ENABLED` reaches the wire only from `createLink`'s authenticated 422 at `:446-450`; at redemption it exists solely as the log `reason` at `:701`. Log: `this.logger?.warn?.(…)` at `:694`; `warn` is the sink's REQUIRED member (`:388`), `error?` is not grown, and `warn` is the correct level under AGENTS.md "Degradation log levels" — the caller was answered, so this is a functional refusal, the same family as the eligibility refusal line at `:941`.
    
      5. (e) The four #14171 pins — RIGHT. Reversed in place at `share-link-service.test.ts:542` (points 1 and 4), `:581` (point 4), `:614` (point 3), `:632` (point 3); each docblock names what the pin asserted and which ruling point moved it; the ruling is quoted verbatim once in the register header at `:468-490`. Each reversal is required: the first two asserted `off` is not null; the two reverse controls asserted a block-less link serves. Nothing deleted: the `enabled: true` control (`:593`) and the ADR-0112 mint-gate pin (`:646`) are untouched; `makeOptOutHarness` / `seedLink` were hoisted to module scope (`:496`, `:515`) with unchanged bodies.
    
      6. (f) Changeset — form and level RIGHT; text complete on retroactivity and mint paths, incomplete on the superseded #13856 reading (note 1). Form copies PR #13857's `.changeset/share-link-eligibility-at-redemption.md` (still on the head): `minor` at `:2`, the BREAKING runtime banner at `:7-12`, the marker `adr-0087: not-required (no-migration-prescription) why` at `:62` in the grammar `scripts/check-adr-0087-registration.mjs:65` records. `minor` is the repo's named level for a breaking change in the launch window (`scripts/check-changeset-no-major.mjs:27-30`) and #13857, the ruling's named model, is `minor`. Text: retroactive on deploy (`:49-52`), system and `permissive` paths (`:31-36`, `:58-60`), never-declared block (`:35-36`, `:50`), sibling keys unchanged when on (`:36-38`), measure-before-rollout query (`:53-57`), no export change (`:11-12`). It does not name the reversed #14171 pins or the #13856 changeset they came from, and `.changeset/share-link-redactfields-survive-optout.md` is still pending on this head (`:9-10` "kept resolving", `:28-30` "pending in #14033"), so both entries compile into the same release notes.
    
      7. (g) `content/docs/permissions/system-context.mdx` — RIGHT. The diff against `origin/main` is exactly one line, row 37 at `:138`; no other row changes. The new prose is accurate: `createLink` at `:445` still mints under `permissive` / `isSystem` with the block off, and `resolveToken` at `:693` refuses. The five anchors `:445`, `:499`, `:503`, `:576`, `:606` resolve on the head to `isSystem` reads at those exact lines of `share-link-service.ts` (the only other `isSystem` tokens are the constant at `:44` and comments at `:479`, `:807`). The hand-edited cell survived the later regeneration commit (`b2d9fd87b`), so the column is a preserved input, not generator output. The PR body still describes the cell as NOT edited (note 2).
    
      8. (h) Stale prose — deferring to #14582 is acceptable; no active contradiction. `content/docs/kernel/runtime-services/sharing-service.mdx:62` is the sharing-service `grant` 422 (`sharing-service.ts:1112`), not a share-link statement; the page never states the mint-only rule — the drift bot's row is a literal-token match with nothing to fix. `content/docs/protocol/objectql/security.mdx:439-441` ("Minting a link requires the object's `publicSharing` opt-in") is still true; the page is silent on `enabled` at redemption — incomplete, not contradictory. `packages/spec/src/contracts/share-link-service.ts:18-21` (createLink refuses) is true; `:233-236` lists `null` causes without the switched-off block — incomplete; the MUST at `:238-243` (one undifferentiated `null`, reason to the log) is honoured by the diff. `packages/spec/src/data/object.zod.ts:2237` ("no share links can be issued") is true and incomplete. No published surface states that existing links survive the switch.
    
      9. (i) Behaviour not in the PR body — two items, both notes. (1) A schema the engine cannot return (no `getSchema` on the engine, or an object not registered at the moment of redemption) is `getPolicy(undefined)`, `enabled: false` (`:97-100`), and now refuses; on `origin/main` such a link served. Same definition `createLink` uses and the fail-closed direction, but the `permissive` TSDoc still sells the option as "useful when the schema scan is happening after `start`" (`:343-345`) while redemption in that window now refuses (note 3). (2) One `warn` line per refused hit at `:694`, not latched (note 4). Route mapping as claimed: `share-link-routes.ts:238-262` and `packages/runtime/src/domains/share-links.ts:145-163` are the same three-way probe (401 password / signed-in, 410 revoked or expired, else 404 `INVALID_OR_EXPIRED`); neither file is in the diff.
    
    ② Semver
    
      `git diff -U0 origin/main...HEAD | grep -E '^[+-].*\bexport\b'` returns one line, changeset prose (`.changeset/share-link-enabled-at-redemption.md:11`); zero source export lines. Symbols reachable through `packages/plugins/plugin-sharing/src/index.ts:27-30`: `ShareLinkService` (class) — `resolveToken` signature unchanged (`:611-614`), accept set narrowed; `ShareLinkServiceOptions` (type) — `permissive` TSDoc only (`:343-352`), shape unchanged. `IShareLinkService` (re-exported from spec at `index.ts:125`) untouched. Nothing added or removed. Level `minor` for `@objectstack/plugin-sharing` is correct and consistent with the gate header's convention (`scripts/check-changeset-no-major.mjs:27-30`) and with #13857. An ADR-0087 disposition is owed (declared-breaking changeset, AGENTS.md "Add a changeset" rule) and is present at `:62`; the body carries no FROM → TO key prescription (the operator guidance at `:55-57` is not one), matching #13857's accepted use of `no-migration-prescription`. The gates' own results are NOT MEASURED here.
    
    ③ Boundary flags
    
      H1: resolved by the diff — one gate at the existing policy read, `:645-706`; ordering verified against `:617-640` before it and `:731`, `:764`, `:767`, `:775` after it.
      H2: resolved by the diff and the existing contract — `null` at `:704`; `404 INVALID_OR_EXPIRED` at `share-link-routes.ts:262`; `SHARING_NOT_ENABLED` only at `:446-450` (mint) and `:701` (log meta); the spec MUST at `packages/spec/src/contracts/share-link-service.ts:238-243`.
      H3: resolved by the diff and ruling point 4 — the return at `:704` precedes `:706`, `:764`, `:767`; the enabled path is unchanged.
      H4: resolved by construction and ruling point 2 — live schema read per call at `:645`, no cache, no row mutation, no migration; ADR-0087 `not-required` at changeset `:62`.
      H5: resolved by read-only inspection — `share-link-routes.ts:238-262` and `packages/runtime/src/domains/share-links.ts:145-163` map `null` identically; neither is in the diff.
      #14581: correct observation — `getPolicy`'s disabled branch still reads `raw.redactFields` (`:103-114`) and its comment still defers to #14033 (`:109-110`); the only consumer of `policy.redactFields` is `:768`, unreachable when disabled; `createLink` reads none. Fenced by the dispatch; leave to #14581, no escalation.
      #14582: acceptable to defer — none of the three surfaces contradicts the gate (judgment 8); row 37 is already corrected here; the spec half is outside this PR's fence.
      Forbidden surfaces: `packages/spec/**` untouched; `content/docs/releases/**` untouched (the drift bot's `v17.mdx` row is read-only); `rule-hooks.ts` / `sharing-plugin.ts` untouched; #14171's `redactFields` logic untouched (`getPolicy` `:89-126` and the union `:767-769` are not in the diff); no new `error`-level log — the one new log site is `warn` at `:694`. The diff is exactly the five files listed.
    
    Tests
    
      HTTP status AND body equality with an unknown token: yes — `share-link-eligibility.test.ts:992-1022` asserts `switchedOff` toEqual `unknown` on `{ status, body }`, then 404 / `INVALID_OR_EXPIRED`, then that the wire body contains none of `enabled`, `publicsharing`, `sharing_not_enabled`; the 410 revoked bucket is pinned as unmoved.
      No record read / no usage stamp on a refused redemption: yes — `share-link-service.test.ts:718-739` (`findCalls` equals `['sys_share_link']`, `use_count` / `last_used_at` unchanged), `:784-800` for the `permissive` path, and `share-link-eligibility.test.ts:964-990` on the real SqlDriver row.
      OFF then ON on the SAME token: yes — `share-link-service.test.ts:741-757` (restored, sibling redactions present, `use_count` is 1 so only the served resolution stamped) and `share-link-eligibility.test.ts:1049-1077` (OFF refuses with no record read; ON refuses by `RECORD_NOT_ELIGIBLE`, then serves once eligible).
      All three mint paths of point 3: yes — `share-link-service.test.ts:784` (`permissive`), `:802` (system context), `:816` (never-declared block, seeded row); `share-link-eligibility.test.ts:1079` (`permissive` on the real driver, refused by the bypassing service and the ordinary one).
      Log at `warn` with the reason, never on the wire: yes — `share-link-eligibility.test.ts:1024-1047` drives a `warn`-only sink and asserts `reason: 'SHARING_NOT_ENABLED'`, the link/object/record ids and a message naming `publicSharing.enabled`; the wire negatives are at `:1009-1012`.
      Real SqlDriver and real route: yes — `boot()` at `:156-198` constructs `new SqlDriver({ client: 'better-sqlite3', connection: { filename: ':memory:' } })` with DDL from the real `SysShareLink` object; the engine passed to the service is a thin facade over that driver (getSchema / find / findOne / insert / update), not the ObjectQL engine; `mountResolveRoute()` at `:1103-1133` mounts the real `registerShareLinkRoutes` with the secure default anonymous context. The service-file block at `:696-830` runs on `makeFakeEngine` (in-memory tables), the same harness #13856 used.
      Pre-fix red count, post-fix green count and the ablation: NOT MEASURED. CI on the head at last read (14:37Z): Test Core 2/6 through 6/6 success, 1/6 in progress; Build Core, Build Docs, Check Changeset, Dogfood gates, Temporal Conformance, Type Check source gates success; Lint & Repo Gates and the remaining Type Check jobs in progress — NOT MEASURED.
    
    Blocking findings (if FAIL): none
    
    Non-blocking notes (§5)
    
      1. Changeset completeness: add one sentence naming the superseded #13856 reading, e.g. "The #13856 entry's 'an opted-out object's links keep resolving with the declared redactions' state is superseded: with the block off they do not resolve at all." `.changeset/share-link-redactfields-survive-optout.md:28-30` still says the question is pending in #14033 and will compile into the same release notes.
      2. PR body is behind the head since `349ff85a0`: "What changed" ("re-anchored by `--fix` only") and "Known stale prose" item 1 (cell NOT edited) are now false, and the Clause-② declaration should list the one-cell hand edit of row 37. Update the body before landing; the squash commit inherits it.
      3. Name the unresolvable-schema consequence in one clause of the changeset's "What changed" and in the `permissive` TSDoc at `:343-345`: an object the engine cannot return a schema for is refused at redemption, so the option helps minting only, not serving, during a late schema scan.
      4. Log volume: `:694` emits one `warn` per refused hit and is not latched — consistent with the eligibility line at `:941` and required by the ruling (the log is the only place the reason exists), but a retroactive deploy with many live links on switched-off objects will burst. Worth one line in the operator-impact paragraph.
      5. Pre-existing, not this PR's: the route probe's 401 answers (`share-link-routes.ts:247-257`, runtime twin `:153-159`) confirm the row exists for password and signed-in links on a switched-off block, and a correct password on such a link answers `WRONG_PASSWORD` — the same shape #13857's refusals already have. If it is to change, it is a separate card on the routes.
      6. `record-share-cascade.test.ts:728` title "links outlive the flip" describes cascade coverage of rows (still true); reword to "rows outlive the flip" in #14582 or leave.
      7. `getPolicy`'s comment at `:109-110` still reads "whatever #14033 rules for standing links" — covered by #14581.
    

    Seat disposition of the §5 notes


    Generated by Claude Code

  8. os-sales commented on Sep 2, 2026

    @os-sales
    Collaborator

    Contract review — delta after round 1, at c55d849d7 (PR #14580): PASS — adopted verbatim

    Same isolated reviewer resumed on the patch-round-1 head (fable tier; transcript verified 71/71 assistant turns across both rounds served as claude-fable-5-1). The verdict below is the reviewer's text, unedited.

    CONTRACT REVIEW — PR #14580 (#14033) at head c55d849d7 — DELTA after round 1
    Verdict: PASS
    
    Delta judgments
    
      1. Prose-only — CONFIRMED. The delta `b2d9fd87b..c55d849d7` is three first-parent commits: `64c959224` (the round-1 text edits), `82a722cf5` (merge of `origin/main`, now `a5b95ee6e`), `c55d849d7` (census regeneration). Restricted to the PR's own files excluding the census page, `git diff --stat b2d9fd87b..c55d849d7` is two files: `.changeset/share-link-enabled-at-redemption.md` (9 insertions, 2 deletions) and `packages/plugins/plugin-sharing/src/share-link-service.ts` (5 insertions, 1 deletion); the two test files have no delta. The three-dot diff against the new base is still exactly the same five files (596 insertions, 68 deletions).
    
      2. Service source byte-identical — CONFIRMED. The only hunk in `share-link-service.ts` is inside the `permissive` TSDoc (`:348-355` on the new head; four lines added, one rewrapped). A comment-stripped comparison of `b2d9fd87b` and `c55d849d7` reports no difference. The gate moved from `:693-705` to `:697-709` and its text is identical line-for-line (checked by diff against the round-1 lines); `const eligibility = policy.eligibility` is at `:710`; the policy read is at `:649-650`. `getPolicy` (`:89-126`) and the redaction union are untouched.
    
      3. Row 37 — RIGHT. The page's diff against the new base is exactly one line, row 37 at `:138`: the hand-edited "What you get" cell ("link **creation** while the policy is off — resolution is **not** bypassed since #14033 …") survived the regeneration, and the anchors are now `:449`, `:503`, `:507`, `:580`, `:610` — each an `isSystem` read on the new head (`share-link-service.ts:449` mint gate, `:503`, `:507` createLink record read, `:580` revokeLink, `:610` listLinks; the only other `isSystem` tokens are the constant at `:44` and comments at `:483`, `:811`). The shift is +4, matching the four TSDoc lines. The delta `b2d9fd87b..c55d849d7` on the page has one other changed line, row 11 (`auth-plugin.ts:1301` to `:1345`), and that exact line is present in `b2d9fd87b..origin/main` — it came in with `origin/main`, not from this PR.
    
      4. Changeset — RIGHT. Level `minor` at `:2`, BREAKING banner at `:7-12`, ADR-0087 `not-required (no-migration-prescription)` marker at `:69`, all intact. The three additions are accurate against the code: (a) `:38-41` — an object the engine cannot return a schema for is refused: `this.engine.getSchema?.(row.object_name)` at `:649` yields `undefined` when `getSchema` is absent or the object is unregistered, `getPolicy` collapses `schema?.publicSharing` falsy to `enabled: false` at `:97-100`, the gate at `:697` refuses; `createLink` uses the same pair at `:441-442` and `:449`. (b) `:41-43` — the #13856 entry's "opted-out links keep resolving with the declared redactions" state is superseded: true, `.changeset/share-link-redactfields-survive-optout.md` is still pending on this head and the two entries now read consistently. (c) `:65-67` — one `warn` line per refused hit at `:698`, not latched: true (no latch exists on this site; the only latch in the file is the usage-stamp reporter). The trailing words "burst the log once" are the one wobble — see §5 note 1.
    
      5. Nothing else — CONFIRMED. No file under `packages/spec/**`, `content/docs/releases/**`, `rule-hooks.ts` or `sharing-plugin.ts` is in the PR's file set against the new base; the #14171 `redactFields` logic (`getPolicy` `:89-126`, the union in `resolveToken`) is untouched; no behaviour changes, no export line (`git diff -U0 origin/main...HEAD | grep export` returns only the changeset prose line at `:11`); no new log site. Nothing passed in round 1 is disturbed: the reversal register, the `[#14033]` pins in both files, the mint-gate pin and the `enabled: true` control are byte-identical to `b2d9fd87b`. CI on `c55d849d7` at read time (14:51Z): Check Changeset, Spec property liveness, Governed Surface Queue Guard, Check Documentation Links success; Test Core 1/6-6/6, Build Core, Build Docs, Lint & Repo Gates and the Type Check jobs in progress — NOT MEASURED.
    
    Round-1 notes status
    
      1: resolved (`.changeset/share-link-enabled-at-redemption.md:41-43` names the superseded #13856 reading in one sentence, as asked).
      2: not resolved (PR body as served at `updated_at` 2026-09-02T14:50:48Z, i.e. unchanged since the round-0 edit: the "What changed" census sentence still reads "row 37 re-anchored by `check:system-context-census --fix` only"; "Known stale prose" item 1 is prefixed DONE but still says "the one-cell correction is proposed, not made"; the Clause-② declaration does not name the one-cell hand edit; the "Commits on the branch" list ends at `a52cfa363`; there is no "Patch round 1" section — the round-1 commits `64c959224`, `82a722cf5`, `c55d849d7` are undeclared).
      3: resolved (`share-link-service.ts:351-355` says the option helps minting, not serving, during a late schema scan, and why; `.changeset/share-link-enabled-at-redemption.md:38-41` names the unresolvable-schema refusal).
      4: resolved (`.changeset/share-link-enabled-at-redemption.md:65-67` states the per-hit `warn` line, that it is not latched, and the deploy-time burst).
      5, 6, 7: outside the delta, unchanged and still non-blocking (route-probe 401 shape is pre-existing; `record-share-cascade.test.ts:728` wording; `getPolicy` comment now covered by #14581).
    
    Blocking findings (if FAIL): none
    
    Non-blocking notes (§5)
    
      1. Changeset `:67` "will burst the log once" — because the line is per hit and not latched, the volume continues with traffic after deploy rather than occurring once; replace "will burst the log once" with "will burst the log at deploy and keep logging one line per hit until the block is enabled or the links are revoked". Accuracy only; the sentence before it is correct.
      2. PR body (carried from round 1, still open): before landing, edit the body so the tree and its declaration agree — in "What changed", replace the census sentence with "row 37: anchors re-anchored by `check:system-context-census --fix` after the TSDoc line shifts, and the 'What you get' cell corrected by hand (creation only; resolution refused since #14033)"; in "Known stale prose" item 1, delete the sentence "The page is in this PR only through the census gate's own regeneration … the one-cell correction is proposed, not made"; in the Clause-② declaration, add "one hand-edited cell in `content/docs/permissions/system-context.mdx` row 37 (prose, no anchor change)"; add a "Patch round 1" section naming `64c959224`, `82a722cf5`, `c55d849d7` and the new head `c55d849d7`, and update "Final head". The squash commit inherits the body.
    

    Seat disposition

    • Note 2 (PR body) — fixed before landing, without moving the head (a body edit; the round-1 dev report is being posted as this is written, and the body is corrected right after it, by the dev or by the seat if the dev has stopped).
    • Note 1 (changeset :67 wording) — the sentence before it already states the per-hit, unlatched behaviour, so this is a nuance rather than a false statement; not worth a third head for a Clause-② PR. Recorded for the seat's grouped follow-up finding together with round-1 note 5 (the routes-side 401 probe) after this PR merges; the release process compiles changesets centrally and can take the wording from there.
    • Landing: needs:contract-review comes off this card and PR fix(plugin-sharing): hold publicSharing.enabled at redemption, not only at mint (#14033) #14580 in one stroke (read back) once the body is corrected; check-clause2-carriers --pair 14580 re-run; ready + auto-merge once every check on c55d849d7 is green.

    Generated by Claude Code

  9. claude commented on Sep 2, 2026

    @claude
    Contributor

    os-dev-report

    {
      "issue": 14033,
      "status": "done",
      "round": "patch round 1",
      "branch": "claude/issue-14033-public-sharing-enabled-standing-policy",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/14580",
      "head": "c55d849d7",
      "premise_still_valid": true,
      "summary": "Round 1 executed idempotently on a re-created worktree confirmed at b2d9fd87b. Prose only, per the seat's disposition of contract-review notes 1-4: (a) the changeset names the superseded #13856 reading ('an opted-out object's links keep resolving with the declared redactions' is superseded — with the block off they do not resolve at all); (b) one 'What changed' clause: an object the engine cannot return a schema for (no getSchema, or not registered at the moment of redemption) is enabled: false by getPolicy's definition and is refused at redemption, fail-closed, the same definition createLink uses; (c) one operator-impact line: one warn per refused hit, not latched, so a retroactive deploy with many live links on switched-off objects bursts the log once; (d) the permissive TSDoc: during a late schema scan the option helps minting, not serving. Level minor and the BREAKING banner untouched; #13856's changeset file not edited; the gate, the tests and the changeset level untouched. The four TSDoc lines moved the five row-37 isSystem anchors: check:system-context-census went red (8 problems), the gate's own --fix re-anchored 5 (final row: :449, :503, :507, :580, :610), and the row-37 'What you get' cell SURVIVED (grep count 1; no other row text changed). Merge state: merge-tree listed the census page (mixed, text-merged) against origin/main a5b95ee6e, so origin/main was merged via scripts/pm/os-regen-merge.sh (merge commit 82a722cf5; step 2 took main's side of the page, the cell was re-applied as the hand edit the script's header names, pnpm gen:system-context-census regenerated the anchors, committed as c55d849d7). Pushed -u origin on the first attempt; remote = local = c55d849d7. PR body PATCHed and read back: 'What changed' census line names both the --fix re-anchoring AND the hand-corrected cell, the Clause-2 declaration lists the one-cell hand edit, 'Known stale prose' item 1 stays DONE, round-0 section kept, 'Patch round 1' section added naming the four prose changes and the new head; the PATCH normalised the footer to bare (platform behaviour). needs:contract-review remains on PR #14580 and on #14033 for the delta review. CI on c55d849d7: in_progress at report time (not waited for).",
      "tests": "On head c55d849d7 (dependency closure restored from the turbo cache under the lock, 'Cached: 15 cached, 15 total'): 'pnpm --filter @objectstack/plugin-sharing exec vitest run --maxWorkers=2 src/share-link-service.test.ts src/share-link-eligibility.test.ts' → 'Test Files  2 passed (2)' / 'Tests  76 passed (76)' (lock 'VERDICT command-exit 0 · held the lock 17s'; the non-comment diff of share-link-service.ts since b2d9fd87b is empty and no test file changed, so this is the same tree the round-0 76/76 measured). 'pnpm --filter @objectstack/plugin-sharing typecheck' → exit 0; 'check:test-typecheck: OK — … 2 file(s) / 3 error(s) / 3 pinned signature(s) held'. 'pnpm check:system-context-census' → exit 0: 'check-system-context-census: OK — 109 elevation read sites in 20 packages across 45 files, all anchored; 145 anchors resolve, 27 declared non-read'. 'pnpm check:doc-authoring' → exit 0: '✓ doc authoring guard: 391 files clean — no bare metadata literals' / '✓ … 14500 customer-facing string(s) across 710 spec sources clean' / '✓ … sibling-package prose ids hold the baseline — 831 pinned site(s) … no growth'. 'node scripts/check-changeset-no-major.mjs' → exit 0: '✓ This diff introduces no `major` bump.' 'node scripts/check-adr-0087-registration.mjs' → exit 0: '::notice … ADR-0087 exemption (no-migration-prescription)' on .changeset/share-link-enabled-at-redemption.md. 'node scripts/check-empty-changeset.mjs' → exit 0: '✓ No empty-frontmatter changeset introduced by this diff (1 declaring changeset(s) added).' 'pnpm check:nul-bytes' → exit 0: 'check-nul-bytes: OK (scanned 7980 text file(s) … no raw ASCII control bytes)'. All exits captured before any pipe. Lock note: four queue-timeouts (exit 99) behind issue-13918's gate-runner batches before the build and the pin run acquired; no unlocked heavy run was made.",
      "mcp_calls": "0 this round (REST only: GET comments, GET/PATCH pulls, POST comment); 0 in the whole run",
      "open_questions": [],
      "out_of_scope_findings": [
        "not filed, handed to the PM (observation, self-corrected on the final head): check-system-context-census --fix rewrites anchors sequentially, so when a new line number equals another anchor's OLD number the second replacement lands on the wrong token — measured once this round on row 37 (':499→:503' then ':503→:507' hit the just-rewritten anchor, leaving ':449, :507, :503, …' out of order; the SET was still correct and the gate green). The later regeneration from main's side produced the monotonic ':449, :503, :507, :580, :610' that is on c55d849d7. Worth a --fix ordering guard in scripts/check-system-context-census.mjs; dedup not run for it this round."
      ]
    }

    Generated by Claude Code


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions