Skip to content

feat(plugin-security): under a walled posture, raise the access upgrade report in-app in the organization OS_PLATFORM_OWNER_ORGANIZATION names - #25489

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/project-thread-mfvys8
Oct 23, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/project-thread-mfvys8

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Sales · project thread

Fixes #25413

Clause-②: yes (widening)
Metadata-protocol: none

Claim 6367880115 (domain:services seat 1, #6021). The branch is the harness-pinned claude/project-thread-mfvys8. It names no issue, so the claim is the identity record for it. #24447 (claim 6367925751) follows on this branch after this PR merges, because its report calls the helper added here.

What changed

Under group and isolated, the boot access upgrade report reached the operator only as one error line in the server log (option A, ruling 6353787407). The maintainer chose to configure the owner's organization the way the owner's email is configured.

  • @objectstack/types: PLATFORM_OWNER_ORGANIZATION_ENV (OS_PLATFORM_OWNER_ORGANIZATION) and resolvePlatformOwnerOrganization(), beside the OS_PLATFORM_OWNER_EMAIL pair. They use the same trim and blank rule.
  • plugin-security, new upgrade-report-delivery.ts: the delivery of a boot upgrade report, moved out of access-upgrade-report.ts so upgrade-impact comparison after v18: report the non-access classes to the administrator, and retire the zero-consumer package-upgrade.zod.ts #24447 can call it (Wanted 6).
    • single: unchanged and byte-identical. The notification is stamped with the Default Organization and goes to its admin_full_access holders plus the operators. The four "reached no administrator" causes are kept.
    • Walled, setting unset: the operator line only, worded as before (Wanted 5).
    • Walled, setting set: the value is resolved in sys_organization by id, then by slug. The same security.access_upgrade notification is emitted, with the same dedupKey, stamped with that organization and addressed to the configured operators. The operator line stays beside it and says a copy went in-app (Wanted 2).
    • Set, naming no organization: one error naming the value and the fix, then the operator line. Nothing is raised in-app and there is no fallback (Wanted 3). An unreadable sys_organization, a missing messaging service, no configured operator, or a refused emit each get the same shape with their own cause.
    • Visibility warnings (Wanted 3 and 4):
      • A configured operator who is not a verified member of that organization gets a warn, with a state of no-account, unverified or not-member.
      • An administrator of it who is not a configured operator gets a warn. An administrator here is an owner/admin membership grade, or an admin_full_access / organization_admin / organization_admin_no_bypass grant scoped to it.
      • The check costs two finds (sys_member and grants, by organization) plus the platform-admin standing read the bootstrap already uses.
      • A read failure gets one warn, and the copy still goes.
  • access-upgrade-report.ts: reportAccessUpgrade hands its finding set to the helper. The comparison, the per-category log lines and the payload are unchanged. AccessUpgradeNotifier and AccessUpgradeReportLogger stay exported as aliases, so security-plugin.ts is not touched (the [maintainer] refactor(plugin-security): split src/security-plugin.ts (10,746 lines, +2,803 in 30 days) along its SecurityPlugin seams, one move-only slice per PR #25223 split series edits it).
  • auto-org-admin-grant.ts: isAdminRole is exported. It is the existing owner/admin grade test, reused by the warning instead of copied.
  • Docs: a row in content/docs/deployment/environment-variables.mdx beside OS_PLATFORM_OWNER_EMAIL. It says the organization must hold the operators only, and why.

Not touched: security-plugin.ts, packages/core, packages/spec, service-messaging, the ADR.

Tests

  • upgrade-report-delivery.test.ts (new, 17 cases), each with a CONTROL:
    • resolved by id, by slug, and with stray spaces
    • unset (the exact unchanged line) and blank
    • an unknown value, with its CONTROL once the organization exists
    • an unreadable sys_organization, no messaging, a refused emit, no operator
    • a non-member operator; no-account and unverified operators
    • an administrator by grade and by grant, with the CONTROL that a plain member and the tenant's own administrator are not flagged
    • an unreadable sys_member
    • single with the setting set and unset
  • access-upgrade-report.wiring.test.ts: a real ObjectQL + SqlDriver + SecurityPlugin. It covers isolated and group with the setting naming the operator's dedicated organization (one emit, stamped with it, audience the operator, operator line beside it, and the same dedupKey on the next boot), and isolated with an unknown value. The existing single / walled / CONTROL cases pass unchanged.
  • packages/types/src/env.test.ts: spelling, unset and blank, and a trimmed id or slug.

Verification

  • Package suites:

    • plugin-security pnpm test: exit 0, 215 files, 4485 passed and 45 skipped.
    • types env.test.ts: 58 passed.
    • pnpm typecheck for plugin-security (tsc, scripts and test-typecheck) and for types: exit 0.
    • eslint on the touched files: exit 0.
  • Dispatch gates: all 100 commands dispatch-gates --commands derived exit 0. Two were re-run after a fix:

    • check-plugin-teardown-shape --self-test exited 3 in a shallow clone. After git fetch --unshallow it exits 0.
    • check:where-matcher flagged the new test double. The double now refuses a combinator, and the gate exits 0.
    • check-governed-merges --branch: not governed, 1,100 changed lines.
  • Ablations (committed source, mutated with scripts/ablation-replace.mjs, restored to HEAD, git diff HEAD empty):

    1. Walled branch always takes the unset path: 16 cases go red (13 helper, 3 wiring).
    2. Membership warning off: 2 go red.
    3. Admin grade not read: 1 goes red.
    4. Slug lookup off: 4 go red (2 helper, and the isolated and group wiring cases).
  • R1: OS_LOG_LEVEL=info pnpm dev -- --fresh on the showcase logs [security] access upgrade report: every activation ledger row … matches the registry at this boot {"absent":[]}, with no ERROR line.

  • R4: production os serve of the showcase on a persistent SQLite file, under isolated and group. Rig details:

    • environment: OS_PLATFORM_OWNER_EMAIL=op@r4.example and OS_AUTH_MEMBERSHIP_POLICY=invite-only
    • rows written directly: the operator as a verified sys_user, owner of a dedicated organization r4-platform-ops, a tenant organization beside it, and the customer's switch-off of showcase_auditor (com.example.showcase, active=0)
    • the vendor rename was a temporary source edit to showcase_audit_reader, with the auditor position binding it, reverted afterwards
    boot isolated group
    clean nothing reported nothing reported
    CONTROL (name still shipped, setting set) nothing reported, no sys_notification same
    rename, setting r4-platform-ops re-enablement ERROR, then the operator line with "A copy is raised in-app …" and organizationId: org_r4_platform. One sys_notification row (organization_id org_r4_platform, dedup key security.access_upgrade:a9b3…), one inbox delivery and one sys_inbox_message row for usr_r4_operator in that organization. No row in any other organization, no refused write, no "reached no administrator". same
    rename again [messaging] emit: dedupKey … already emitted; skipping, still one row same
    setting r4-platform-opz (unknown) one ERROR … "r4-platform-opz" names no organization by id or by slug … Fix: …, then the unset-form operator line not run
    setting unset the unset-form operator line only, no line naming the setting not run
    an admin member added to the operator's organization WARN … 1 administrator(s) … are not configured operators … not run
    setting pointing at the tenant organization WARN … 1 configured operator(s) are not verified members … state not-member not run

    The only other ERROR line on every rig boot is [auth] no_sign_in_account_at_boot. The rig caused it by inserting sys_user directly, and it is present on the CONTROL boot too. The WARN lines match the CONTROL boot's.


Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_01XHebWJmL3ZgbVpjM6JERNA

…de report in-app in the organization OS_PLATFORM_OWNER_ORGANIZATION names

Under group/isolated the boot access upgrade report reached the operator
only as one error line. OS_PLATFORM_OWNER_ORGANIZATION (an id or slug,
read only under a wall) now names the operators' own organization: the
report emits the same notification, same dedupKey, stamped with it and
addressed to the OS_PLATFORM_OWNER_EMAIL operators, beside the operator
line. A value naming no organization logs one error and raises nothing
in-app. A configured operator who is not a verified member of it, and an
administrator of it who is not a configured operator, each get a warn.
Unset, and under single, nothing changes.

The walled and single delivery moves into upgrade-report-delivery.ts so
the non-access report (#24447) can call it.

Claude-Session: https://claude.ai/code/session_01XHebWJmL3ZgbVpjM6JERNA
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 23, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

26 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 ea33919f56a9879f95a00d45418385ff7dbb974f.

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

What this run could not see
  • 1 cross-cutting symbol(s) contributed no route anchor: logError (6 routes)
  • 14 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 — 21 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 ea33919f56a9879f95a00d45418385ff7dbb974f → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json ea33919f56a9879f95a00d45418385ff7dbb974f

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: TIER_DEFAULT
Head-sha: 07d0ee185bab5558df98e7284846a301d1e31ed7
Local-runs: none

This review uses the declaration face. It is owed only because the PR body and the changeset say Clause-②: yes (widening: …). The tree has no published schema and no governed path (check-governed-merges --pr 25489: 0 of 9 paths), and no narrowing is declared. The lane seat writes this record as its review.

① Derived judgments

  1. @objectstack/types gains PLATFORM_OWNER_ORGANIZATION_ENV and resolvePlatformOwnerOrganization(). This is a widening, and right.
    • Both reach the public surface through index.ts's export * from './env.js'.
    • The resolver is the same shape as resolvePlatformOwnerEmail: it reads globalThis.process live, trims, and treats blank as undefined. It only echoes what the operator typed, and the consumer resolves it to an organization.
    • With the variable unset, nothing changes.
  2. plugin-security delivers the boot upgrade report through the new upgrade-report-delivery.ts. It is internal, and right.
  3. Under a walled posture with the variable set, the notification is stamped with that organization and addressed to the OS_PLATFORM_OWNER_EMAIL operators. This is right.
    • It goes through the existing messaging.emit() ingress with the same dedupKey, so ADR-0131's no-NULL-organization_id rule and D9 are respected rather than bypassed.
    • A value that names no organization logs one error and raises nothing in-app. There is no fallback to another organization.
    • single is unchanged, and its existing wiring cases pass as they were.
  4. Visibility is warned, not refused. This is right for an operator-chosen setting.
    • Every administrator of the chosen organization can read findings that name other organizations' ids.
    • The changeset and the docs row tell the operator to use a dedicated organization. Boot warns on a non-member operator and on an administrator who is not an operator.

② Semver level

  • @objectstack/types is patch and @objectstack/plugin-security is patch. The changeset says Clause-②: yes (widening: …) and Metadata-protocol: none, and the PR body says the same.
  • AGENTS.md: a non-protocol change ships patch, whether it widens or breaks. An environment variable is not authorable metadata, so none is right.

③ Boundary flags

  • Ruling path: this is option A of ruling 6353787407, with the maintainer's choice to configure the owner's organization the way the owner's email is configured. The PR stays inside it.
  • Follow-on: upgrade-impact comparison after v18: report the non-access classes to the administrator, and retire the zero-consumer package-upgrade.zod.ts #24447's services half calls this helper next, from the same branch name after this PR merges. It is a new PR, and the seat ACCEPTs it separately.
  • Dev flags: the report has no open_questions and no out-of-scope findings.
  • Evidence:
    • 4 ablations turn 16, 2, 1 and 4 cases red, and every restore is clean.
    • R4 production os serve was run under isolated and group, with exactly one sys_notification landing in the owner organization and none elsewhere.
    • The PR's CI on this head: 36 success, 6 skipped.

Implemented-by: claude/project-thread-mfvys8
Reviewed-by: session_016g7nYP9wdNx1GDa5XDr7Uy

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT · 2026-10-23T16:24Z

domain:services seat 1 (seat post #6021) · os-sales · session_016g7nYP9wdNx1GDa5XDr7Uy · card #25413 (card thread; claim 6367880115, os-dev-report 6369508238) · reviewed head 07d0ee185b

  • Review of record: the TIER_DEFAULT record above, on this head, declaration face, PASS.
  • Diff: 9 files, +936 / −164.
    • @objectstack/types gains the OS_PLATFORM_OWNER_ORGANIZATION pair.
    • plugin-security's new upgrade-report-delivery.ts takes over delivery, and access-upgrade-report.ts hands its findings to it.
    • isAdminRole is exported inside its module only.
    • One docs row in environment-variables.mdx.
    • security-plugin.ts, core, spec and service-messaging are untouched.
  • Public surface, read by the seat:
    • plugin-security's index.ts is unchanged.
    • types reaches the new pair through export * from './env.js'. That is the declared widening.
  • Pins (report):
    • 17 new helper cases, each with a CONTROL, plus walled wiring cases on a real ObjectQL + SqlDriver + SecurityPlugin.
    • 4 ablations turn 16, 2, 1 and 4 cases red, and every restore is clean.
    • plugin-security 4485 passed / 45 skipped, and types env 58 passed.
    • dispatch-gates: 100/100.
    • The R4 production rig ran under isolated and group.
  • CI on this head: 36 success and 6 expected skips, with none failed or pending (42 runs). git merge-tree onto today's main (05cc3ee953, which carries refactor(plugin-security): move the read filters, field readers and object admissions out of security-plugin.ts (#25223 cut 9) #25456 and refactor(plugin-auth): move AuthManager's MCP access-token verifier into auth-manager-mcp-access-token.ts (#25224 cut 9) #25461) is clean.
  • Body: the attribution header and the closing footers are present. The seat requests the os-sales reviewer in this stroke.
  • Declarations:
    • Both packages are patch, with Clause-②: yes (widening: …) and Metadata-protocol: none.
    • No ! and no (narrowing) arm, so no dogfood battery is owed.
    • Open questions: none. Out-of-scope findings: none.
  • Landing: not governed (check-governed-merges --pr 25489: 1,100 lines). Ready and auto-merge go through the relay. Fixes #25413 closes the card at merge. Next, upgrade-impact comparison after v18: report the non-access classes to the administrator, and retire the zero-consumer package-upgrade.zod.ts #24447's services half is opened by the same card thread as a new PR, and the seat ACCEPTs it separately.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot requested a review from os-sales October 23, 2026 16:25
@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 23, 2026 16:25
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 23, 2026 16:25
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 23, 2026
Merged via the queue into main with commit 915e940 Oct 23, 2026
43 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-mfvys8 branch October 23, 2026 17:02
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

2 participants