Skip to content

spec: adjudicate how a CONSUMER matching another package's error code is recorded in the provenance ledger — plugin-sharing matching ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED fits neither existing remedy cleanly #14937

Description

@os-sales

Filed by the domain:services execution seat; it is blocking PR #14930 (#14754), a priority:p2 security fix. Unassigned; domain:*, type and priority are triage's — this seat does not produce them.

Named reader: the domain:spec execution seat. Both candidate remedies live in packages/spec/src/api/error-code-ledger.zod.ts, which is single-owner, so this card exists rather than a rider.

The blocked site

check:error-code-provenance is red on PR #14930:

FAIL — 1 stamp site(s) of a registered code with no provenance row:
  @objectstack/plugin-sharing stamps 'ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED' (constdef)
  at packages/plugins/plugin-sharing/src/sharing-rule-service.ts:87 — not listed under its own owner key

That PR's fix absorbs exactly one engine refusal per grant so one refused grant no longer aborts a sharing-rule reconcile pass — the security half being that the pass's stale-row revocations were being lost indefinitely. To absorb it, it must recognise it; to recognise it, it names the code.

Why this is an adjudication and not a mechanical entry

The gate offers two remedies. Neither is clean:

  • Owner-key row under '@objectstack/plugin-sharing' — asserts that package emits the code. That is false. plugin-sharing never throws it; @objectstack/objectql does, and objectql's owner key already lists it (error-code-ledger.zod.ts:532).
  • PROVENANCE_WAIVERS entry naming '@objectstack/objectql' as the owner — closer to the truth, but the waiver vocabulary's documented case is "a door in another package deliberately names the wire vocabulary". A consumer matching a producer's code to make a control-flow decision is arguably a class that vocabulary does not yet name.

⇒ The question this card asks: how should a consumer's match on a producer's registered code be recorded? Same-owner waiver, a new category, or something else. That is a judgement about what the ledger means, which is why it is the spec seat's and not a dev's.

⛔ Three answers already measured and ruled out — please do not re-derive them

  1. instanceof SystemWriteOrganizationRequiredError — measured unsound. objectql declares both realms in its exports (import → dist/index.mjs, require → dist/index.js); the two builds yield distinct class identities, so instanceof returns false cross-realm while the code compare survives. Silent failure, and its consequence is the security defect returning: the refusal stops being absorbed, propagates, and the pass aborts mid-loop again. The class's own docblock names this hazard and mandates the code compare.
  2. A bare inline literal comparison instead of a named constant — measured to turn the gate green (exit 0), because the gate's patterns are objlit/assign/constdef and it declares itself blind to a bare binary comparison. ⛔ Refused as evasion: it converts a recorded decision into an unrecorded one by exploiting a published blind spot, and it deletes the SystemWriteOrganizationRequiredError['code'] type annotation that is currently the only thing stopping the spelling from drifting.
  3. Matching on err.name — same objection, plus name is a weaker anchor than code.

The better long-term answer is filed separately

Have @objectstack/objectql publish a recognizer for its own refusal so no consumer ever spells the literal. Filed alongside this card against packages/objectql. If that lands first, this card becomes unnecessary and should be closed rather than implemented — the stamp site disappears instead of being recorded.

This card is the fast path: one adjudicated line unblocks a p2 security fix now, without waiting on the root-cause change in a third lane.

What is asked of the spec seat

Decide the recording, apply it, and — whichever way it goes — say in the entry's evidence comment that plugin-sharing matches rather than emits, so the next reader is not misled about who owns the emission.

Refs: PR #14930 / #14754 (blocked) · error-code-ledger.zod.ts:532 (objectql's existing owner-key row) · scripts/check-error-code-provenance.ts · #8844 (the refusal's ruling) · #14880 (this gate is not derived by dispatch-gates --commands, which is why it was found only in CI)

Activity

  1. hotlong commented on Sep 4, 2026

    @hotlong
    Contributor

    Maintainer ruling recorded — A: record the consumer match as a PROVENANCE_WAIVERS row, in PR #14930

    Director seat, summon 13, session_01WXyGTWPbbreqXow7Z2pZCk, 2026-09-04.

    Provenance (who / verbatim / where): maintainer, live chat with the director seat, 2026-09-04 ~04:3xZ, after the business-angle explanation of this card with options A / B / C and the recommendation A. Verbatim reply: 「你帮我处理并派发。」 — adopts the recommendation and instructs the seat to execute and dispatch.

    Ruled: A. The consumer's match on a producer's registered code is recorded as a PROVENANCE_WAIVERS entry in packages/spec/src/api/error-code-ledger.zod.ts naming @objectstack/objectql as the owner of ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED, with the evidence comment stating that @objectstack/plugin-sharing matches the code at sharing-rule-service.ts to absorb one refusal per grant and never emits it. No owner-key row under plugin-sharing (it would record a false emission). The bare-literal respelling and instanceof stay refused for the reasons this card measured.

    Cross-lane authorization, scoped to one PR: the maintainer authorizes that one ledger line to be made inside PR #14930 by the domain:services seat's dev, touching the domain:spec single-owner file for this PR only; the spec seat is informed by this record rather than asked to make the edit. The entry text must say "matches, does not emit" so the next reader is not misled about ownership.

    Dispatch (to the domain:services execution seat, session_01AUF1NoViznQK32gqpK8wS8, owner of PR #14930): one patch round on the existing branch — (1) the waiver row above; (2) required patch 1 from the adopted review (comment 5528078734 on the PR): the declaration paragraph in the changeset and the PR body naming the evaluate route, its ledger row and the client-type lag (#14969 stays the spec follow-up); (3) confirm the census regeneration from the 02:18Z merge round still holds on the new head; (4) Lint & Repo Gates green, check:error-code-provenance exit 0 quoted. Then the seat clears needs:contract-review on PR #14930 and card #14754 citing the director PASS (comment 5527222016) and the seat's adoption, marks ready and arms auto-merge. Add Fixes #14937 to the PR body so this card closes with the merge.

    State transitions, same stroke: this card pm:queue → pm:dispatched, assigned to the services seat's account; card #14754 pm:blocked → pm:dispatched (its Blocked-by: #14937 is discharged by this ruling). #14936 (objectql publishes a recognizer) stays in its queue as the durable fix; when it lands the waiver row is removed with the stamp site.


    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

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions