Skip to content

Commit e3ad492

Browse files
fix(security): runtime strings state each decision in words instead of a tracker number (stage 5) (#21518)
Part of #20751 Clause-②: no **Stage 5 of the `domain:services` lane under the maintainer's A / A ruling (5902360492): the `plugin-security` strings.** The card stays open for the later stages, so this PR carries no closing keyword. Text only: no status, error `code`, field, route, export or control flow moves (the AST skeleton reads SAME for 17 of 17 changed sources, below). ## What this does Some of the security plugin's strings sent the reader to a tracker number for the reason behind them: refusals, explain details, boot warnings, log lines, and the `managed_by` field help on `sys_permission_set` / `sys_position` (with their `es-ES`, `ja-JP` and `zh-CN` variants). In form D, as stages 1 to 4 applied it, the number goes. Where the sentence already said what was decided, only the citation goes. Where it leaned on the number, it now says the decision in words. The stage covers all 43 ledgered occurrences in `plugin-security` (claim `5960178324`). They were re-derived from the ledger on `origin/main` at `94a8761a8`, and again on the merged tree at `cb919dc03` (with `origin/main` `f9a8eb889` merged in): 43 occurrences, 27 (file, id) pairs and 11 files, in 38 string sites. The card's table reads 43, so it matches. None of them sits in the excluded `share-link-tenant-wall.test.ts` (a test file, which the ledger does not read), and that file is not touched. The two PRs that landed in `plugin-security/src/` after this branch was cut (21488 and 21503) add no tracker-number string: the census of the merged tree finds 0 sites in the package. ### Rewritten in words Author- and administrator-visible text first, log lines last. Line numbers are at the head. | Where (head line) | Cited | The text now says | Decision read from | |---|---|---|---| | `security-plugin.ts:7056`, the curated capability-name refusal (`403 PERMISSION_DENIED`) | 8552 | "A curated name is refused at authoring so that no admin-authored row can collide with the row the platform seeds for it." | The maintainer ruling on the card: option 4 (Setup can no longer create a `sys_capability` row with a name in `PLATFORM_CAPABILITY_NAMES`, or rename one to it), plus option 1 for installations that already collide. | | `delegated-admin-gate.ts:624`, `:625`, the two delegation-anchor refusals | cloud 830 | "because the anchor roots the delegate's business-unit visibility and may only narrow yours" / "a delegation may only narrow visibility, never widen it, because the anchor roots the delegate's business-unit visibility" | **Not read.** `objectstack-ai/cloud` answers 403 to this session, and `add_repo` was refused. The text states the behaviour the code enforces and what its own docblock records (`delegated-admin-gate.ts:600`: that card made `business_unit_id` the read-scope depth anchor, so "anchoring only narrows, never widens"). | | `objects/sys-permission-set.object.ts:336`, `objects/sys-position.object.ts:288`, the `managed_by` field help, and the generated `en` leaves | 2920 (item A4) | "Record provenance, on the one platform / package / admin vocabulary that capabilities, permission sets and positions all share" | Item A4 of the tracking card: the three RBAC catalogs' `managed_by` is unified on `sys_capability`'s select tri-state, and legacy values are mapped (system to platform, config to package, user to admin). | | `bootstrap-system-capabilities.ts:758`, the derived-capability boot warning | 5876 | "left as their author wrote them (the derivation refreshes them only on a row it can prove is the platform's own)" | The card's fix as accepted: the derived pass guards its label/description refresh by `managed_by`, and a row that is "not provably ours" is left alone. | | `bootstrap-system-capabilities.ts:749`, `:753`, the two remediation tails of that warning | 8552 | "when a row the seeder cannot prove is its own already holds the name, the seeder declines rather than adopting that row or backfilling a stamp" / "the seeder does not adopt a row it cannot prove is its own, and does not backfill provenance on the operator's behalf" | The same ruling rejected options 2 (adopt) and 3 (backfill). Adopting would reverse "not provably ours, leave it alone"; backfilling would decide provenance on the operator's behalf. | | `security-plugin.ts:2216`, the public-form strip warning | 3022 | "stripped: an anonymous form submission cannot set ownership, tenancy or audit columns" | The card's expectation, as landed: a public form has no transfer authority, so a supplied `owner_id`, `organization_id` or audit column is stripped at the data layer. | | `security-plugin.ts:5148`, `:8279`, the `controlled_by_parent` write-gate and master read-scope failures | 5386 | "a child is writable only where its master is, so a master check that cannot be resolved refuses" / "a child is readable only where its master is, so a master scope that cannot be resolved admits no child" | The card's acceptance: a `controlled_by_parent` child follows its master's ownership and share grants, on both reads and writes, and a sharing failure denies on both. | | `security-plugin.ts:8252`, `:8261`, the two chain guards | 11082 | "a chain the derivation cannot resolve admits no child rather than leaving it unrestricted" | The triage direction the fix implemented: compose across the chain, with cycle and depth guards that fail closed, and never a blanket deny for every chain. The card's issue endpoint answers 404 today; its comments were read through the timeline. | | `security-plugin.ts:5248`, the row-level write gate's sharing-verdict failure | 5492 | "only a resolved sharing allow, from Modify All Data or an edit-level share, may replace that floor" | The maintainer ruling: enforce both declared write wideners. The gate composes the sharing verdict, so `allow` replaces only the platform's own ownership floor. | | `security-plugin.ts:5448`, the authored-policy verdict failure | 5493 | "a verdict that cannot be resolved never lifts the sharing refusal" | The maintainer ruling Q1 = A: `admit` only when an app-authored policy matches, and `abstain` in every other case, a throw included. | | `security-plugin.ts:5506`, the `getReadFilter` sharing-scope failure | 4467 | "a path that bypasses the engine middleware never runs without the owner and share scope a direct read applies" | The card's expectation, as landed: the analytics read scope carries owner scope and share grants, and fails closed when it cannot resolve them. | | `security-plugin.ts:5544`, the on-behalf-of read-scope refusal | 2852 | "a delegated read is never scoped wider than its delegator's own" | The card's remaining scope (Gap 1): this path has no delegator intersection yet, so a delegated read denies rather than returning the agent's wider scope. | | `security-plugin.ts:7672`, the platform-owner wall-bypass audit line | 12974 | "crossed the Layer 0 organization wall on a read ... (only the declared platform owner's reads cross it; writes stay walled for everyone)" | The card has no comments. The ruling is quoted verbatim in the code at that site: READS only, for the verified `OS_PLATFORM_OWNER_EMAIL` account; writes keep the ADR-0123 D2 refusal. | | `cleanup-package-permissions.ts:195`, the uninstall line | 2747 | "removed by package_id, so no grant outlives the package (ADR-0090 D5)" | The card's expectation (delete by `package_id` so authorization lapses at once), and ADR-0090 D5's "No ghost grants". | | `unresolved-posture.ts:229`, `:233`, `:242`, the three fail-closed log lines | 3545, 10401, 10424 | "fail-closed: an unreadable posture never defaults to public or uncontracted" | 3545's second assessment: an unresolvable posture fails closed, and the exposure gate stays fail-open. Each line already stated the draft and outage halves (10401, 10424) in its own words. | | `normalize-managed-by.ts:190`, the normalizer line | 2920 (item A4) | "normalized to platform/package/admin, the one vocabulary every RBAC catalog shares" | As for the field help above. | ### Citation only (the sentence already stated the decision) - `explain-engine.ts:1409`, `:1868`, `:1874` (4647): "the same bypass the write path consults", "the write path consults this SAME bypass", "a write bypass requires Modify All Data (modifyAllRecords), so ownership and sharing still decide". These already say what 4647 ruled (option A: explain and the write path share one bypass predicate, and View All Data alone never bypasses a write). - `unresolved-posture.ts:197`, `:202`, `:207` (3545): each already says the access "fails CLOSED rather than defaulting to public/uncontracted". - `security-plugin.ts:1037`, `:1641`, `:1648`, the org-scoping entitlement refusal and the two arming lines (12699). The refusal's `problem` text already says the key is refused and resolves to "never declared" (fail closed). The arming lines already say "Layer 0 does not wall them on THIS deployment" and "membership-driven grants hand out organization_admin_no_bypass". Their `[security/` + number prefix becomes `[security]`. - `security-plugin.ts:8915`, the invalid `maskingRule` warning (8993): it already says "applying a full mask (fail-closed)" and names the closed preset set and `{keepHead, keepTail}`. The prefix changes as above. - `security-plugin.ts:2104`, the "registered security service" line (3544, 3547, 5493, 7616): the method list in the same line names what each card added (`canExport`, `getReadableFields`, `checkAuthoredRowWrite`, `resolvePermissionSetsForContext`). The ADR references stay. - `permission-evaluator.ts:511`, `:558` (2565): "falling back to bootstrap/db sources" and "unresolved sets grant nothing this request" are the card's fix (warn, and stay fail-closed). Every cited card that could be read was read through REST, body and every comment, before its string was rewritten. That is 22 cards: 8552, 5876, 2747, 4647, 2920, 2565, 12699, 3544, 3547, 5493, 7616, 3022, 5386, 5492, 4467, 2852, 12974, 11082, 8993, 3545, 10401 and 10424. 11082's issue endpoint answers 404 today, so its comments were read from the timeline endpoint. cloud 830 could not be read (see the table). ### Translations - `managed_by` help on both objects: the `es-ES`, `ja-JP` and `zh-CN` leaves are hand-written translations that never carried the number or the decision (they translate the value list only). Each now carries the same decision, using each locale's own object labels: "en el único vocabulario platform / package / admin que comparten capacidades, conjuntos de permisos y puestos", "ケイパビリティ・権限セット・ポジションで共通の platform / package / admin の語彙" and "能力、权限集与岗位共用同一套 platform / package / admin 取值". No key was added or dropped. - `node scripts/check-i18n-bundles.mjs --write --filter=plugin-security` rewrote the two `en` leaves from the object source. No `en` leaf and no source-hash digest was hand-edited. The four `managed_by` locale leaves have no digest entry (they are legacy-trusted), and the merge kept every hand-written value. - Tracker numbers left in the package's locale bundles: 0. ## Tests Four assertions in three test files held the old text. Each now holds the new substance, and no `code` or `status` assertion is touched. - `metadata-outage-unresolved-cause.test.ts`: the assertion that the outage explain detail contains the 3545 citation now asserts `toContain('rather than defaulting to public/uncontracted')`. The assertion that all three log lines contain the fail-closed suffix with the 3545 citation now asserts `toContain('fail-closed: an unreadable posture never defaults to public')`. - `default-report-sink.test.ts`: the assertion on the fail-closed suffix with the 2852 citation now asserts the new delegated-read clause. - `deployment-platform-global-exemption.test.ts`: the "nothing to refuse means nothing to warn about" filter matched the number in the refusal warn's prefix. It now matches `'org-scoping entitlement key'`, the refusal warn's own words. The junk-declaration cases in the same file are its positive control. Reverse check at the committed head: both sources were put back to `origin/main`, then restored with `git checkout HEAD`; the restore was proven by blob hash and an empty `git diff HEAD`. I predicted 3 red and measured 2: the log-line pin and the delegated-read pin. The outage explain-detail pin stayed green because that sentence's substance was already there; only its citation went. At the merged head `cb919dc03`, through `scripts/pm/os-verify-lock.sh` after a full build (`turbo build`, 71/71): - `pnpm --filter @objectstack/plugin-security test`: `Test Files 164 passed (164)`, `Tests 3527 passed | 45 skipped (3572)`, exit 0. - `pnpm --filter @objectstack/plugin-security typecheck`: exit 0. This includes `check:test-typecheck: OK` over `tsconfig.test.json`, so the re-pinned test files are compiled. In the published surface, `dist/index.js` and `dist/index.mjs` carry the new sentences. No dist file carries the old 3545 fail-closed suffix, the 12699 log prefix or the cloud 830 anchoring parenthesis. ## Ledger (`scripts/doc-authoring-prose-id.baseline.json`) - This PR opened after stage 4's PR 21472 merged (`cc0786223`). `origin/main` (`f9a8eb889`) was merged in, with no rebase. Only this file conflicted, and the conflict was resolved by taking main's copy and regenerating it, never by hand. - The ledger was recomputed with `node scripts/check-doc-authoring.mjs --census-ledger` on the merged tree. Against main's copy: 49 lines deleted, 0 added. The 11 `plugin-security` files leave the ledger (43 occurrences, 27 pairs), and no other entry moves. - Totals: 85 → 42 occurrences, 62 → 35 pairs, 21 → 10 files. `--census-ledger` refuses growth, and it refused nothing. - `check:doc-authoring`: "sibling-package prose ids hold the baseline — 34 pinned site(s) across 10 file(s), 86188 string(s) read in 1252 parsed source(s), no growth, no burn-down unrecorded". ## Text only The AST skeleton compares `origin/main` (`f9a8eb889`) with the head. Strings become placeholders, a `+` chain of strings counts as one string, template expressions are kept, identifiers and numbers are kept, and comments are not read. It reads SAME for all 17 changed `.ts` files. Controls on a scratch copy of `security-plugin.ts`, with each mutation counted once on disk: an identifier rename reads DIFF, a text-only change SAME, and a template re-split SAME. The changed lines are byte-identical before and after the merge (the `-U0` +/- line sets against `94a8761a8` and against `f9a8eb889` are equal). ## Gates - `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` at `cb919dc03` derives 74 commands. All 74 were run one at a time from the worktree, with exit codes recorded before any pipe, and all 74 exit 0. - `--ran`: "✓ dispatch-gates --ran: 74 derived famil(ies) accounted for — 74 run, 0 NOT-MEASURED (a DERIVED zero — all 74 recorded an exit code and none of them is 3)." - Among them: `check:i18n` ("all bundles in sync, no undeclared authoring keys"), `check:i18n-stale-fill` ("no new stale fills, 0 baselined"), `check:nul-bytes`, `check:published-files`, `check:dts-closure` (169/169), and `check:dual-build-cjs-loads` (106 require entry points across 66 packages load). - Lint, narrowed as a measurement: `eslint --no-inline-config --format json` over the 17 changed `.ts` files at `cb919dc03` reports 17 files linted (none ignored), 0 errors and 0 warnings. `eslint.config.mjs` enables no type-aware linting (no `parserOptions.project`), so this diff cannot change the verdict on any untouched file. The repo-wide `pnpm lint` is CI's. ## Acceptance notes - cloud 830 cannot be read from this session (403). The two anchor refusals state the behaviour the code enforces, not a reading of that card. - The issue endpoint for 11082 answers 404, while its timeline is readable. - `plugin-sharing`'s `sharing-service.ts` carries the same 5493 citation in its own abstain line. That is the `plugin-sharing` stage of this card, and this PR leaves it alone. - Test titles in this package still name card numbers. Those are test files, outside the ledger and the ruling's runtime scope, and this PR leaves them alone. - The locale `managed_by` leaves changed although they never carried the number, so that each locale carries the same decision as `en`. --- _Generated by [Claude Code](https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent abe8f28 commit e3ad492

19 files changed

Lines changed: 84 additions & 98 deletions
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
'@objectstack/plugin-security': patch
3+
---
4+
5+
Security refusals, explain details, field help and log lines no longer cite tracker numbers; each one states the decision behind it in words
6+
7+
Clause-②: no
8+
9+
Some strings the security plugin shows to administrators, authors and operators pointed at an issue-tracker number for the reason behind them. The number goes; where the sentence did not already say what was decided, it now does.
10+
11+
- The curated capability-name refusal says a curated name is refused at authoring so that no admin-authored row can collide with the row the platform seeds for it.
12+
- The two delegation anchor refusals say the business-unit anchor roots the delegate's business-unit visibility, so a delegation may only narrow it.
13+
- The `managed_by` field help on `sys_permission_set` and `sys_position`, in every shipped locale, says capabilities, permission sets and positions all share one platform / package / admin vocabulary.
14+
- The explain details for an unresolvable security posture and for the View/Modify All Data bypass drop their citations; those sentences already said that access fails closed and that the write path consults the same bypass.
15+
- The derived-capability boot warning says the derivation refreshes a row's label and description only when it can prove the row is the platform's own, and that the seeder neither adopts a row it cannot prove is its own nor backfills provenance on the operator's behalf.
16+
- The fail-closed log lines say what each denial protects: a `controlled_by_parent` child is readable and writable only where its master is, and a chain the derivation cannot resolve admits no child; only a resolved sharing allow (Modify All Data or an edit-level share) may replace the platform ownership floor; an authored-policy verdict that cannot be resolved never lifts the sharing refusal; a path that bypasses the engine middleware never runs without the owner and share scope a direct read applies; a delegated read is never scoped wider than its delegator's own; an unreadable posture never defaults to public or uncontracted.
17+
- The public-form line says an anonymous submission cannot set ownership, tenancy or audit columns; the uninstall line says a package's permission rows are removed by `package_id`, so no grant outlives the package; the platform-owner wall-bypass line says only the declared platform owner's reads cross the wall and writes stay walled for everyone. The org-scoping entitlement, masking-rule, permission-set resolution, vocabulary-normalization and service-registration lines drop their citations, and the log lines that carried a tracker number in their `[security/…]` prefix now open with `[security]`.
18+
19+
Text only: no status, error code, field, route or control flow moves. A client or log filter that matches the old text (for example a tracker-number suffix or prefix) needs the new spelling.

‎packages/plugins/plugin-security/src/bootstrap-system-capabilities.ts‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -746,14 +746,17 @@ export async function bootstrapSystemCapabilities(
746746
'at all), so it most likely arrived as app seed data replayed per organization, or a legacy ' +
747747
'import. Fix it AT ITS SOURCE: Setup cannot, because ADR-0066 asset ownership refuses every ' +
748748
'admin-door edit and delete on a platform-stamped row. Note the platform bucket stays empty ' +
749-
"either way — that is the #8552 posture for an occupied name, not a consequence of the stamp."
749+
'either way, and not because of the stamp: when a row the seeder cannot prove is its own ' +
750+
'already holds the name, the seeder declines rather than adopting that row or backfilling a stamp.'
750751
: " The organization's row is a supported extension (ADR-0066 D1 — admins EXTEND the " +
751752
'registry), so there is nothing for an operator to remove; the platform bucket is left empty ' +
752-
'deliberately, and adopting or backfilling it was rejected in #8552.';
753+
'deliberately: the seeder does not adopt a row it cannot prove is its own, and does not ' +
754+
"backfill provenance on the operator's behalf.";
753755
options.logger?.warn?.(
754756
`[security] derived capability "${def.name}" has no platform placeholder and none was seeded. ` +
755757
`The row this pass found for the name is ${provenance} ${locality}, and its label and ` +
756-
'description were left as their author wrote them (#5876 — unchanged). In the platform ' +
758+
'description were left as their author wrote them (the derivation refreshes them only on a row ' +
759+
"it can prove is the platform's own). In the platform " +
757760
`(NULL-organization) bucket, where the declared unique key admits one row per name: ${bucket}. ` +
758761
"The platform's own derived placeholder is therefore missing from sys_capability " +
759762
'installation-wide. Grants and requiredPermissions referencing the name are unaffected — ' +

‎packages/plugins/plugin-security/src/cleanup-package-permissions.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ export async function cleanupPackagePermissions(
192192
);
193193
}
194194
if (out.sets + out.positionBindings + out.userGrants + out.suggestions > 0) {
195-
logger?.info?.('[security] package permission rows revoked on uninstall (#2747)', {
195+
logger?.info?.('[security] package permission rows revoked on uninstall — removed by package_id, so no grant outlives the package (ADR-0090 D5)', {
196196
packageId, ...out,
197197
});
198198
}

‎packages/plugins/plugin-security/src/default-report-sink.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ describe('[#10556 (a)] SecurityPlugin default report sink', () => {
7777
expect(filter).toBeDefined();
7878

7979
expect(seen).toHaveLength(1);
80-
expect(String(seen[0]?.[0])).toContain('denying (fail-closed, #2852)');
80+
expect(String(seen[0]?.[0])).toContain("denying (fail-closed: a delegated read is never scoped wider than its delegator's own)");
8181
});
8282

8383
it('guarantees a `warn` channel on the default sink, and routes it to the console', () => {

‎packages/plugins/plugin-security/src/delegated-admin-gate.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -621,8 +621,8 @@ export class DelegatedAdminGate {
621621
if (!allowed.has(rowAnchor)) {
622622
deny(
623623
allowed.size === 0
624-
? `business unit anchor '${rowAnchor}' cannot be validated against your own '${positionName}' anchor — an anchor that cannot be proven within your own range is refused (cloud#830: anchoring only narrows)`
625-
: `business unit anchor '${rowAnchor}' is outside your own effective anchor for '${positionName}' — a delegation may only narrow visibility, never widen it (cloud#830: anchoring only narrows)`,
624+
? `business unit anchor '${rowAnchor}' cannot be validated against your own '${positionName}' anchor — an anchor that cannot be proven within your own range is refused, because the anchor roots the delegate's business-unit visibility and may only narrow yours`
625+
: `business unit anchor '${rowAnchor}' is outside your own effective anchor for '${positionName}' — a delegation may only narrow visibility, never widen it, because the anchor roots the delegate's business-unit visibility`,
626626
{ position: positionName, businessUnitId: rowAnchor },
627627
);
628628
}

‎packages/plugins/plugin-security/src/deployment-platform-global-exemption.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ describe('[#12699] platformGlobalObjects — the deployment carve-out', () => {
160160
});
161161
// And nothing to refuse means nothing to warn about.
162162
const warned = logger.warn.mock.calls.map((c) => String(c[0]));
163-
expect(warned.filter((m) => m.includes('#12699'))).toEqual([]);
163+
expect(warned.filter((m) => m.includes('org-scoping entitlement key'))).toEqual([]);
164164
});
165165

166166
it('composes with, never replaces, the object-level channel: `tenancy.enabled:false` stays exempt with no deployment declaration', async () => {

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1406,7 +1406,7 @@ async function applyRecordAttribution(
14061406
rowFilter: null,
14071407
rules: [],
14081408
detail: `View/Modify All Data via [${vamaSets.join(', ')}] admits this record regardless of ownership — ` +
1409-
'the same bypass the write path consults (#4647).',
1409+
'the same bypass the write path consults.',
14101410
}
14111411
: { outcome: 'not_evaluated', rules: [], detail: 'No View/Modify All Data bypass applies to this record.' };
14121412
}
@@ -1865,13 +1865,13 @@ export async function explainAccess(deps: ExplainEngineDeps, input: ExplainInput
18651865
(delegatorVama ? ` AND by the delegator [${delegatorVama.join(', ')}]` : '') +
18661866
` — ownership and sharing checks are skipped` +
18671867
(vamaBit === 'modify'
1868-
? ` (Modify All Data: the write path consults this SAME bypass, #4647).`
1868+
? ` (Modify All Data: the write path consults this SAME bypass).`
18691869
: `.`)
18701870
: agentVama.length > 0 && delegatorVama !== null && delegatorVama.length === 0
18711871
? `Agent holds View/Modify All Data via [${agentVama.join(', ')}] but the DELEGATOR does not — D10 intersection strips the bypass.`
18721872
: viewOnlySets.length > 0
18731873
? `View All Data held via [${viewOnlySets.join(', ')}] does NOT bypass ownership for ${operation} — ` +
1874-
`a write bypass requires Modify All Data (modifyAllRecords), so ownership and sharing still decide (#4647).`
1874+
`a write bypass requires Modify All Data (modifyAllRecords), so ownership and sharing still decide.`
18751875
: 'No View/Modify All Data bypass.',
18761876
contributors: vamaEffective ? vamaSets.map((n) => ({ kind: 'permission_set' as const, name: n, via: viaOf(n) })) : [],
18771877
});

‎packages/plugins/plugin-security/src/metadata-outage-unresolved-cause.test.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,7 @@ describe('[#10424] the wording module states three distinct things', () => {
347347
expect(remedy).toContain('Do NOT change the declaration');
348348
expect(remedy).toContain('NOT a permissions problem');
349349
expect(unresolvedPostureExplainDetail('task', 'metadata_unavailable')).toContain('OUTAGE');
350-
expect(unresolvedPostureExplainDetail('task', 'metadata_unavailable')).toContain('#3545');
350+
expect(unresolvedPostureExplainDetail('task', 'metadata_unavailable')).toContain('rather than defaulting to public/uncontracted');
351351
});
352352

353353
it('the outage denial keeps the pinned opening clause verbatim', () => {
@@ -362,7 +362,7 @@ describe('[#10424] the wording module states three distinct things', () => {
362362
for (const c of causes) {
363363
expect(unresolvedPostureDenialMessage('task', 'find', c)).toContain('[Security] Access denied:');
364364
expect(unresolvedPostureExplainDetail('task', c)).toContain('fails CLOSED');
365-
expect(unresolvedPostureLogLine('task', 'find', 'u1', c)).toContain('fail-closed, #3545');
365+
expect(unresolvedPostureLogLine('task', 'find', 'u1', c)).toContain('fail-closed: an unreadable posture never defaults to public');
366366
}
367367
});
368368
});

‎packages/plugins/plugin-security/src/normalize-managed-by.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ export async function normalizeManagedByVocab(
187187
const permissionSets = await normalizeObject(ql, 'sys_permission_set', PERMISSION_SET_MAP, options.logger);
188188
const total = positions + permissionSets;
189189
if (total > 0) {
190-
options.logger?.info?.('[security] managed_by vocab normalized to platform/package/admin (A4 #2920)', {
190+
options.logger?.info?.('[security] managed_by vocab normalized to platform/package/admin, the one vocabulary every RBAC catalog shares', {
191191
positions,
192192
permissionSets,
193193
});

‎packages/plugins/plugin-security/src/objects/sys-permission-set.object.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -333,7 +333,8 @@ export const SysPermissionSet = ObjectSchema.create({
333333
readonly: true,
334334
defaultValue: 'admin',
335335
description:
336-
"Record provenance (unified tri-state, A4 #2920): 'platform' = shipped by the " +
336+
"Record provenance, on the one platform / package / admin vocabulary that capabilities, " +
337+
"permission sets and positions all share: 'platform' = shipped by the " +
337338
"platform; 'package' = versioned package metadata (re-seeded on upgrade, read-mostly " +
338339
"for admins); 'admin' = created/owned in this environment by an administrator " +
339340
"(live-edited, never touched by package seeding). Legacy rows may carry 'user' (== admin).",

0 commit comments

Comments
 (0)