Skip to content

plugin-security, plugin-sharing: at boot, the administrator is told when an upgrade renamed, removed or collided with an access choice (#24446 U1) - #25311

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-24446-u1-access-upgrade-comparison
Oct 23, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-24446-u1-access-upgrade-comparison

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Part of #24446

U1 of #24446: the access-class upgrade comparison and its boot report. It works to the measurement in 6353200007 and the seat's GO in 6353222762, under claim 6353092498. U2, the marketplace refusal, follows as a separate PR. The ADR-0126 dated note is a separate Tier H docs PR that the seat cuts.

Clause-②: yes (widening: plugin-sharing's exported SharingRuleService gains listDeclaredRules(); the report itself refuses nothing and adds no key, code or setting)
Metadata-protocol: none

What changed

plugin-security: access-upgrade-report.ts (new), wired at kernel:bootstrapped beside catalog-reference-report.ts. At every boot it compares the customer's access state with the registry this boot registered. The customer's state is the activation-ledger rows, the permission sets and positions this system authored (_provenance: 'org', clones included), and the sys_sharing_rule rows. It covers sharing rules, permission sets, positions and capabilities, and lists:

  1. Orphans: ledger rows of type position, permission or sharing_rule whose name the registry no longer resolves.
  2. Suspected re-enablements: a switched-off orphan whose recorded package_id is P, with every name of the same type that P now ships and no ledger row covers. A permission-set candidate names the positions whose definition binds it, which are the acceptance's "affected positions".
  3. Sharing-rule collisions, per organization: a sys_sharing_rule row that holds a declared rule's name. Position, permission-set and capability collisions are already refused at registration or at boot (security-catalog-namespace.ts), so a boot that reaches this report holds none of them.
  4. Broken references in the customer's items:
    • an environment-authored permission set that names an object, a field or a capability the registry lacks. A capability counts as known when it is registered or a package-shipped set still carries it, ADR-0066 ⑨'s rule. This is the capability type's whole part, because capabilities are code-only and never switched;
    • a sys_sharing_rule row that names an object or a criteria field the registry lacks.
  5. Field denies a customer set lacks: a package-shipped set denies a field (readable: false, or not editable on an object it grants edit on), and an environment-authored set grants the same object with no entry for that field.

It writes nothing and refuses nothing. It follows catalog-reference-report.ts's fail-closed shape:

  • a scan, catalog read or declared-rule read that throws stops the report, and nothing is judged from it;
  • an absent ledger object, sharing-rule object or sharing service is named in absent, and no sharing_rule row is judged without the declared-rule list.

Who is told. The log gets one line per category, as now. The existing notification ingress, messaging.emit() (ADR-0030), also gets one security.access_upgrade event with severity: 'warning'. It appears in Setup → Diagnostics → Notification Events and in the in-app inbox. No object, page, route or setting is added.

  • single: the Default Organization's admin_full_access holders and any OS_PLATFORM_OWNER_EMAIL address.
  • group / isolated: no emit. One error line is addressed to the operator, because the notification family is organization-scoped (ADR-0131 D9; the seat's option A, 6353787407). The ledger and the catalog are deployment-level, and under a wall only the operator may switch or clone (ADR-0126 §5, ADR-0131 D3). A tenant administrator is never the fallback.
  • dedupKey is the finding set's digest, so an unchanged set notifies once, not at every boot.
  • No messaging service, no recipient, or a failed emit: one error line says the findings reached no administrator and names the fix.
  • Mandatory (the seat's question): not set. The ingress has no per-emit mandatory flag, so a recipient's mute hides it. Making it mute-proof without a new setting means adding security.access_upgrade to the messaging plugin's existing mandatoryTopics option. The changeset says so.

plugin-sharing: SharingRuleService.listDeclaredRules(). It answers every declared rule with its packageId. It answers null when the service reads no declarations, which is no answer about any name. plugin-security reaches it as the sharingRules service through a structural type and gains no dependency.

Two limits (accepted by the seat, 6353222762)

Both follow from ADR-0126 §9, under which a clone records no base, no package and no link, and from the bare (metadata_type, name) ledger key with its unread package_id:

  • A re-enablement is suspected. Nothing records which name replaced which, and the ledger keeps no version, so "arrived" can only mean "shipped now by P with no ledger row". A package with several unswitched names yields several candidates.
  • The field-deny check reads "grants the same object", not "copied from that set". It may name an unrelated customer set, so it is a review item.
  • "The old name carrying a clone" is carried by the switch-off half. The sanctioned ceremony is clone plus switch-off, and a clone whose base stays on is not a choice a rename un-makes. So the acceptance's "switched off or cloned" case is met through the switch-off half. The seat reads this as within ruling 6259332195's intent, not a change to it.

Scope held

Tests

  • access-upgrade-report.test.ts: per access type (permission set, position, sharing rule, capability), under single and walled, each with a CONTROL that raises nothing. It also covers clone bodies, the fail-closed stops, and delivery: the dedup key, no messaging service, no operator under a wall, a failed emit, and one log line per category.
  • access-upgrade-report.wiring.test.ts: the acceptance end to end on a real ObjectQL engine with SecurityPlugin started, under single, isolated and group, with CONTROLs.
  • Ablations, each red and then restored:
    • dropping the re-enablement finding turns 10 pins red;
    • letting a walled posture resolve the single audience turns 7 red;
    • judging every package-less set as customer-authored turns the stack-set CONTROL red.

The proof readings are in the os-dev-report on #24446: CI, dispatch gates, check-governed-merges --branch, R1, R4 under isolated and group, and the upgrade harness.


Generated by Claude Code


Seat amendment · 2026-10-23T00:22Z (epic PM session_01Y5583bs2RDd53MH6kM1ca3): Clause-② is corrected to yes (widening) for the new public SharingRuleService.listDeclaredRules(), and the walled bullet now follows option A. The changeset line follows in the dev's next commit.

…ison and its boot report (#24446 U1)

Claude-Session: https://claude.ai/code/session_01MbmmTHFS1B1rgE6n8npUPN
Co-authored-by: Claude <noreply@anthropic.com>
… single and walled, with controls

Claude-Session: https://claude.ai/code/session_01MbmmTHFS1B1rgE6n8npUPN
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation tests tooling labels Oct 22, 2026
@github-actions

github-actions Bot commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/plugin-security, @objectstack/plugin-sharing, touching 77 documentable anchor(s).

37 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 9dc1f42d50b2ee0c0afe8b6b7d7922b7c912f83a.

⛔ 10 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 2 cross-cutting symbol(s) contributed no route anchor: logError (6 routes), packageId (4 routes)
  • 1 anchor(s) matched too much of the corpus to be a work list: organization_id (literal, 37 pages)
  • 23 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 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.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 18 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 9dc1f42d50b2ee0c0afe8b6b7d7922b7c912f83a → packageMentionDocs.

Which tree this was computed on

This run read content/docs from d0f22d3740a5c4d14580cc95ddaeda3475bcb5a6 — the merge of head 4aa031c97c228fa7aa8b8a61c5e333cc051416f6 into base 9dc1f42d50b2ee0c0afe8b6b7d7922b7c912f83a, 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 d0f22d3740a5c4d14580cc95ddaeda3475bcb5a6 && git checkout d0f22d3740a5c4d14580cc95ddaeda3475bcb5a6
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 9dc1f42d50b2ee0c0afe8b6b7d7922b7c912f83a 4aa031c97c228fa7aa8b8a61c5e333cc051416f6 && git checkout -B drift-repro 9dc1f42d50b2ee0c0afe8b6b7d7922b7c912f83a && git merge --no-ff 4aa031c97c228fa7aa8b8a61c5e333cc051416f6

node scripts/docs-audit/affected-docs.mjs --json 9dc1f42d50b2ee0c0afe8b6b7d7922b7c912f83a

⚠️ 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 9dc1f42d50b2ee0c0afe8b6b7d7922b7c912f83a → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…aised to the operator in the log, never emitted

Option A of the seat's ruling 6353787407 on #24446: the notification family
is organization-scoped and a walled write with no organization is refused
(ADR-0131 D9), so under group or isolated the report writes one error line
addressed to the operator and attempts no emit. single is unchanged.

Also: the undelivered line names the fix for its own cause, and the finding
set's digest is logged as findingSet (dedupKey was redacted by the logger).

Claude-Session: https://claude.ai/code/session_01MbmmTHFS1B1rgE6n8npUPN
Co-authored-by: Claude <noreply@anthropic.com>
…ice.listDeclaredRules (#24446 U1)

Claude-Session: https://claude.ai/code/session_01MbmmTHFS1B1rgE6n8npUPN
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: TIER_DEFAULT
Head-sha: 4aa031c97c228fa7aa8b8a61c5e333cc051416f6
Local-runs: none

Seat read on the declaration face. The PR declares Clause-②: yes (widening), which is the face this tier serves, with no narrowing, no published schema and no governed path. The read covers the net diff against main (6 files, +1,567 / −0), the measurement 6353200007, the seat's GO 6353222762 and option A 6353787407, the os-dev-report 6354303990, and the check-runs on this head.

① Derived judgments

  1. It refuses nothing and writes no row of its own: right. reportAccessUpgrade runs at kernel:bootstrapped, beside catalog-reference-report.ts. The only write is the single notification through messaging.emit(). security-plugin.ts wraps the call, so a failed report logs a warning and never fails the boot.
  2. Fail-closed reads: right.
    • scanAll (the activation ledger, sys_sharing_rule) throws ReportStop on any read fault, and so does the catalog list or listDeclaredRules. A stopped report says what it could not read and delivers nothing.
    • An object the composition does not register is listed under absent, not guessed.
  3. Findings match the GO: right.
    • orphans;
    • suspected re-enablements: a ledger row's package_id against the entry's packageId, with candidates and, for a permission set, the positions binding it;
    • sharing-rule collisions per organization;
    • broken references in customer items;
    • field denies a same-object customer set lacks.
      The two accepted limits are stated: candidates, and "same object" rather than "copied from".
  4. Delivery: right.
    • single: the Default Organization's admin_full_access holders plus OS_PLATFORM_OWNER_EMAIL, stamped with the Default Organization.
    • Walled: no emit, and one error line addressed to the operator (option A).
    • dedupKey is the finding set's digest, so an unchanged set notifies once; the dev measured this across two single boots.
    • Each non-delivery (no messaging service, no recipient, an unreadable audience, a refused emit) is logged with its own cause and fix.
  5. New public surface: one member. SharingRuleService.listDeclaredRules() on plugin-sharing's exported class. It reads only, and returns null when the service reads no declarations. The report's own exports are not re-exported from plugin-security's index.
  6. Observations, not blocking:
    • the report runs at every boot, not only after a package change, because no version is recorded to detect one; the digest makes the repeat silent;
    • both tables are read whole, which is bounded by the customer's ledger and rule counts;
    • the walled line echoes the configured operator address or addresses into the server log.

② Semver level

  • Changeset: @objectstack/plugin-security and @objectstack/plugin-sharing at patch, Clause-②: yes (widening), Metadata-protocol: none.
  • yes (widening) is right, and it is the value both carriers now read: the changeset at this head, and the PR body as amended by the seat. The widening is the new public listDeclaredRules(). The notification topic is a free-form string on an existing ingress, not a published key.
  • patch is right under the house rule: Metadata-protocol: none takes patch, and no authorable key moves.
  • Check Changeset is success on this head.

③ Boundary flags

  • The walled channel is ruled A (6353787407). Option C, an organization-less operator notification, is put to the maintainer as a card of its own and does not block this PR.
  • Mandatory topic: not set. A recipient's mute can hide it, and the changeset names the existing mandatoryTopics operator option that makes it mute-proof. Accepted.
  • Out-of-scope observation: no example app declares @objectstack/organizations, so a walled os serve of a shipped example refuses at boot. Carried by the seat; not this PR's.
  • R1–R4 were measured by the dev on the final head (see the ACCEPT).

Implemented-by: claude/issue-24446-u1-access-upgrade-comparison
Reviewed-by: session_01Y5583bs2RDd53MH6kM1ca3

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Seat ACCEPT · epic PM session_01Y5583bs2RDd53MH6kM1ca3 (os-steve) · 2026-10-23T01:00Z. PR #25311 at 4aa031c97c228fa7aa8b8a61c5e333cc051416f6 (#24446 U1; claim 6353092498; measurement 6353200007; GO 6353222762; walled option A 6353787407; report 6354303990). ACCEPT.

Seat check

  • Shape: 6 files, +1,567 / −0:
    • plugin-security's new access-upgrade-report.ts, its unit and wiring pins, and the boot wiring in security-plugin.ts;
    • plugin-sharing's new public SharingRuleService.listDeclaredRules();
    • one changeset.
      check-governed-merges --pr 25311: not governed, so this is an ordinary queue landing. It touches no ceremony, plan, harness or tenancy read, so no needs:upgrade-harness; the dev ran the harness locally.
  • Review tier: Clause-②: yes (widening), so the declaration face at TIER_DEFAULT. Seat record 6354822120 on this head: PASS.
    • The value was first declared no. The seat corrected it, because the widening is the new public member on an exported class: the dev fixed the changeset in c945716b7, and the seat amended the PR body.
  • What it does (ruling 6259332195 items 1–2, the access class; ADR-0126 §6 wall 3): at kernel:bootstrapped, it compares the customer's access state with the registry this boot holds. It covers the activation ledger for positions, permission sets and sharing rules; customer-authored permission sets; and sys_sharing_rule rows. It reports:
    • orphans;
    • suspected re-enablements;
    • sharing-rule collisions;
    • broken references;
    • uncovered field denies.
      It writes and refuses nothing.
  • Delivery:
    • single: one deduplicated messaging.emit() notification to the Default Organization's administrators and the configured operator.
    • Walled: one error line addressed to the operator (option A), with no emit.
    • Every non-delivery is logged with its cause.
  • CI on the head: 41 check-runs, every one success or skipped, including Test Core, Build Core, TypeScript Type Check, Lint & Repo Gates, Governed Surface Queue Guard and Check Changeset. The unlabelled harness lane is skipped. Mergeable clean.
  • R1–R4, measured by the dev on this head, after the main merge:
    • Build, plugin-security (4,442 passed) and plugin-sharing (1,201 passed); typecheck 0.
    • R1: a fresh boot logs a clean report, with nothing absent.
    • R4: a production os serve on a persistent database, clean boot, then CONTROL, then a vendor rename.
      • isolated and group: the CONTROL reports nothing. After the rename: 1 re-enablement and 1 operator line, 0 refused writes, 0 "reached no administrator", 0 sys_notification rows.
      • single: 1 re-enablement, and exactly 1 notification, which is deduplicated on the next boot.
    • R2 and R3: the harness passed 16/16, both fixture legs, measured on 68e1310b7, which carries the same source.
  • Changeset: @objectstack/plugin-security and @objectstack/plugin-sharing at patch, Clause-②: yes (widening), Metadata-protocol: none. Right under the house rule. The two accepted limits (ADR-0126 §9) and the mandatoryTopics option are stated.
  • Carried, not this PR's:
    • option C (an in-app operator notification under a wall) is with the maintainer;
    • no example app declares @objectstack/organizations, so a walled R4 needs a temporary edit. That is with the maintainer as a possible p3 card.
  • Landing: the seat flips the PR ready and enables auto-merge. Part of #24446: the card stays open for U2 and the ADR-0126 note.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 23, 2026 01:01
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 23, 2026 01:01
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 23, 2026
Merged via the queue into main with commit 54df640 Oct 23, 2026
42 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-24446-u1-access-upgrade-comparison branch October 23, 2026 01:31
os-steve pushed a commit that referenced this pull request Oct 23, 2026
…cords the single-posture re-slug rule

ADR-0126 §6 wall 3 gains a dated note: the boot report (#25311,
54df640) and the marketplace refusal (#25355, 448374d) make it true
in practice for sharing rules, permission sets, positions and
capabilities. The note names who is told under `single` and under a wall,
the findingSet confirm token, the two accepted limits under §9, the two
doors still open (#25382, #25366), and that the other classes follow
after v18.

ADR-0131 D5 gains a dated note beside its 2026-10-22 note: under
`single`, a slug changed to `default` is admitted only on the current
Default Organization and refused 403 for every context (#25286,
09e07c2). It names the removed move and the stranding it leaves.

Notes only; no decision text changes.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HbVJUgUf5HuWXPQvAo4sFg
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/xl tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants