Skip to content

Commit e207b97

Browse files
feat(plugin-security): a boot upgrade impact report for the non-access classes, raised to the administrator like the access upgrade report (#25512)
<!-- claude-projects-attribution: {"github_login":"os-sales"} --> _Requested by **Sales** · [project thread](https://claude.ai/code/project/chan_01D5bWFY3rFzgJcytD5gD4Uu?thread=cmsg_01D5bWFY3rFzgJcytD5gD4UuUSKmaWjm1sW7kkQuwKewbx)_ Part of #24447 Clause-②: yes (widening) Metadata-protocol: none Claim 6367925751 (`domain:services` seat 1, #6021). This is the services half of the card: the non-access upgrade report (items 2 and 4 of ruling 6259332195). The spec half, retiring `packages/spec/src/kernel/package-upgrade.zod.ts` with its ADR-0087 conversion, belongs to the spec lane. It was asked for in comment 6367978063 and in pointer 6367986096 on #18549, and it has no card yet, so the card stays open for it. The two halves have no ordering between them. The branch is the harness-pinned `claude/project-thread-mfvys8`, the same one #25489 (#25413) landed from. This PR calls the delivery helper that PR added. ## What changed An upgrade that renames, removes or collides with a name could leave the customer's stored state meaning something else. Nothing said so beyond a scattered log line. At every boot, right after the access upgrade report, the security plugin now reads that state against the registry. It reports what it finds and changes nothing. - **New `plugin-security/src/upgrade-impact-report.ts`.** One finding list per class. - **Switches (flow, action, webhook ledger rows):** - a row whose package no longer ships the name; - a switched-off row whose package now ships names of that type that no switch covers (a suspected rename that makes the item active again); - a switched-off action whose name more than one object declares (the one row switches every one of them off). - **Stored rows a package now overrides:** - a stored flow whose name a package ships (the packaged flow runs, the stored one never does); - a `sys_webhook` row holding a declared webhook's name (the declaration is withheld for the whole deployment). - **Overlay orphans:** an active `sys_metadata` row of a view, dashboard, report, translation or email template, bound to an installed code or marketplace package that no longer ships the item. Bundled types are judged per member. - **Object extensions:** - a field an extension declares that the base layer (the overlay, else the owner) or another extension also declares, where the last declaration wins; - a stored object row bound to a package that does not own the object, which the boot refuses to serve; - a navigation contribution whose group its app no longer has (read from `getAppNavDiagnostics`). - **Settings:** a stored `sys_setting` / `sys_platform_setting` key that its registered namespace no longer declares. The value is never read, and the scan does not select the value column. - **Not judged, on purpose (named in the module header):** - a package-less object row over a packaged object, which is ADR-0005's sanctioned overlay and cannot be told from a stale one; - rows bound to an authored, unknown or disabled package; - an unregistered settings namespace; - a contribution to an app that is missing (the registry does not list those). - **Who is told.** The findings go through `upgrade-report-delivery.ts` from #25489, under their own topic `system.upgrade_impact` and a `dedupKey` over the sorted finding set. - Under `single`: the Default Organization's administrators and the operators. - Under a wall: the operator line. When `OS_PLATFORM_OWNER_ORGANIZATION` is set, a copy also goes in-app in that organization. - Each category logs one `error` with its fix. A clean boot logs one `info` line. - **Stops.** A store that cannot be read, or settings manifests that cannot be read, stop the report for that boot with one `warn` naming what was unreadable. An unread answer is never taken for "resolves nowhere", and nobody is told. - **`security-plugin.ts`:** one call in the `kernel:bootstrapped` hook after the access report. It has its own `try`/`catch`, and the `settings` and `messaging` services are looked up by name. - **Changeset:** `patch` for `@objectstack/plugin-security`. Not touched: `packages/spec`, the engine lane (objectql, core, metadata-*, platform-objects), service-automation, plugin-webhooks, service-settings, the ADR. ## Tests - **`upgrade-impact-report.test.ts` (39 cases), every class with a CONTROL.** - The engine double's `find` refuses a combinator or an object value. - The registry double answers as the real one does. `getArtifactItem` prefers the named package, else any package. `listItems` hides disabled packages. Contributors come back as own, overlay, extend. - Covered: the renamed flow under `single` and `isolated`, a disabled package, a tenant-authored flow, a row with no package, a switched-ON orphan, plural type spellings, inactive rows, another package shipping the same name, bundled members, the overlay as base layer, ADR-0005's package-less overlay, missing navigation diagnostics, a layout-only setting specifier, a missing settings service, four unreadable stores and unreadable manifests, absent stores, no registry, a stable `dedupKey`, and no write verb ever asked for. - **`upgrade-impact-report.wiring.test.ts` (5 cases).** It uses a real `ObjectQL`, `SqlDriver` and `SecurityPlugin`, with a vendor package before and after its upgrade, an add-on with an object extension and a navigation contribution, and real customer rows. - `single` upgraded: all 9 categories, and one emit under the topic to the operator and the Default Organization's administrator. - `isolated` upgraded: the operator line and no emit. - Both postures, CONTROL: the clean line. - The logged lines replayed through the core logger: no value is redacted, and the setting key is named. ## Verification - **Package suite:** plugin-security `pnpm test`: exit 0, 217 files, 4529 passed and 45 skipped. `pnpm typecheck` (tsc, scripts and test-typecheck): exit 0. eslint on the touched files: exit 0. - **Dispatch gates:** on this head (`7e12df4c01`, after a full `pnpm build`, 76 of 76 tasks), all 69 commands that `dispatch-gates --commands` derived exit 0. `--ran` reconciles 69 of 69, with exit codes recorded. Before #25489 merged, the same commit on a branch that also carried #25489 derived 100 commands, and all 100 exit 0. `check-governed-merges --pr 25512`: not governed. - **Ablations** (committed source mutated with `scripts/ablation-replace.mjs` and restored; `git diff HEAD` is empty after each): 1. The `security-plugin.ts` call skipped: 5 red (every wiring case). 2. The base layer left out of the collision check: 5 red. 3. The overlay's own-package check dropped: 1 red. 4. The undeclared-key test off: 5 red. 5. The shared-action threshold raised from more than 1 object to more than 99: 4 red. 6. A store read failure swallowed as an empty store: 4 red (the four stop cases). - **R5:** production `os serve` of the showcase on a persistent SQLite file, with `OS_PLATFORM_OWNER_EMAIL=op@r4.example` and `OS_AUTH_MEMBERSHIP_POLICY=invite-only`. - The impact seed writes these rows directly: - a switched-off ledger flow `showcase_recalc_estimate_old`; - a dashboard overlay `showcase_ops_dashboard_old` bound to `com.example.showcase`; - a package-less stored flow `showcase_recalc_task_estimate`; - a `sys_webhook` row `showcase_task_changed`; - an object row `showcase_task` bound to `app.r5_custom`; - a `sys_setting` `company.legacy_tagline`. - The CONTROL seed writes the same rows under names that are still shipped or not taken. - The showcase ships no add-on extension, nav contribution or second-object action, so those three categories are pinned by the wiring test only. | boot | single | isolated | |:--|:--|:--| | clean | the clean `info` line | same | | impact | 6 category `error` lines, one `sys_notification` (`system.upgrade_impact`) in the Default Organization | setting unset: the 6 lines, then the operator line ("this log is where they are raised"), with no notification. Setting `r4-platform-ops`: the 6 lines, then the operator line with "A copy is raised in-app …" and `organizationId: org_r4_platform`, one `sys_notification` in `org_r4_platform`, and one `sys_inbox_message` for `usr_r4_operator` | | impact again | `[messaging] emit: dedupKey … already emitted; skipping` | same, still one row | | CONTROL | the clean line, no new notification | same | The R5 isolated boot caught one defect, fixed here before this PR opened. The setting finding first carried the key as `key`. The core logger redacts any field of that name, so the line read `"key":"***REDACTED***"`. The field is now `name`, and the fifth wiring case pins it through the real logger. The rig's only other `ERROR` under `isolated` is `[auth] no_sign_in_account_at_boot`. The rig caused it by inserting `sys_user` directly, and it appears on the CONTROL boot too. --- _Generated by [Claude Code](https://claude.ai/code/session_01XHebWJmL3ZgbVpjM6JERNA)_ 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01XHebWJmL3ZgbVpjM6JERNA Co-authored-by: Claude <noreply@anthropic.com>
1 parent 7d3c013 commit e207b97

5 files changed

Lines changed: 1832 additions & 0 deletions

File tree

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
'@objectstack/plugin-security': patch
3+
---
4+
5+
feat: a boot upgrade impact report for the non-access classes (flows, actions, webhooks, overlays, object extensions, navigation contributions, settings), raised to the administrator like the access upgrade report
6+
7+
Clause-②: yes (widening: a new boot report and a new notification topic, `system.upgrade_impact`; nothing is written or refused, and no existing path changes)
8+
Metadata-protocol: none
9+
10+
A package upgrade that renames, removes or collides with a name could leave the customer's stored state meaning something else, with nothing said beyond a scattered log line. At every boot, beside the access upgrade report, the security plugin now reads that state against the registry and lists:
11+
12+
- **Flow, action and webhook switches:** an activation ledger row whose name no package ships any more, and a switched-off one whose package now ships unswitched names of the same type (a suspected rename that makes the item active again). Also a switched-off action whose name more than one object now declares, because the one row switches every one of them off.
13+
- **Stored rows a package now overrides:** a stored flow whose name a package ships (the packaged flow runs, the stored one never does), and a `sys_webhook` row holding the name of a webhook a package declares (the declaration is withheld for the whole deployment).
14+
- **Overlay orphans:** a customization of a view, dashboard, report, translation or email template bound to an installed package that no longer ships the item. It keeps serving as an item of its own.
15+
- **Object extensions:** a field an extension declares that the object it extends (or another package's extension) also declares, where the last declaration silently wins. Also a stored object row bound to a package that does not own the object, because another package now ships the name, which the boot refuses to serve. And a navigation contribution whose group its app no longer has.
16+
- **Settings:** a stored value whose namespace no longer declares its key. The value is never read, and the default applies. The report never reads a value.
17+
18+
Each category logs one `error` with its fix. The finding set is raised through the same delivery as the access report, under its own topic and `dedupKey`. Under `single` it goes to the Default Organization's administrators. Under a wall it goes to the operator alone, and in-app in the organization `OS_PLATFORM_OWNER_ORGANIZATION` names when that is set. A clean boot logs one `info` line. A store that cannot be read stops the report for that boot, and it says so.

‎packages/plugins/plugin-security/src/security-plugin.ts‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import {
4848
type DeclaredSharingRuleReader,
4949
} from './access-upgrade-report.js';
5050
import { ACCESS_UPGRADE_CHECK_SERVICE, createAccessUpgradeCheck } from './access-upgrade-check.js';
51+
import { reportUpgradeImpact, SETTINGS_SERVICE, type SettingsManifestReader } from './upgrade-impact-report.js';
5152
import {
5253
explainAccessForAsker,
5354
buildContextForUser,
@@ -4770,6 +4771,30 @@ export class SecurityPlugin implements Plugin {
47704771
{ error: (e as Error)?.message },
47714772
);
47724773
}
4774+
// [#24447] The upgrade impact report: the same comparison for the
4775+
// non-access classes — flow, action and webhook switches, stored
4776+
// overlays and object rows, object extensions, navigation
4777+
// contributions and stored settings — raised the same way. It writes
4778+
// and refuses nothing. See `upgrade-impact-report.ts`.
4779+
try {
4780+
const optional = <T,>(name: string): T | undefined => {
4781+
try { return ctx.getService<T>(name) ?? undefined; } catch { return undefined; }
4782+
};
4783+
const settings = optional<Partial<SettingsManifestReader>>(SETTINGS_SERVICE);
4784+
const messaging = optional<Partial<AccessUpgradeNotifier>>('messaging');
4785+
await reportUpgradeImpact(ql as any, {
4786+
settings: typeof settings?.listManifests === 'function' ? (settings as SettingsManifestReader) : undefined,
4787+
messaging: typeof messaging?.emit === 'function' ? (messaging as AccessUpgradeNotifier) : undefined,
4788+
walled: postureEnforcesWall(this.tenancyPosture),
4789+
logger: ctx.logger,
4790+
});
4791+
} catch (e) {
4792+
ctx.logger.warn(
4793+
'[security] the upgrade impact report did not run — a switch, stored row, extension or setting an ' +
4794+
'upgrade left behind goes unreported for this boot',
4795+
{ error: (e as Error)?.message },
4796+
);
4797+
}
47734798
});
47744799
}
47754800

0 commit comments

Comments
 (0)