Repository navigation
Commit e3ad492
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
File tree
- .changeset
- packages/plugins/plugin-security/src
- objects
- translations
- scripts
Lines changed: 19 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
Lines changed: 6 additions & 3 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
746 | 746 | | |
747 | 747 | | |
748 | 748 | | |
749 | | - | |
| 749 | + | |
| 750 | + | |
750 | 751 | | |
751 | 752 | | |
752 | | - | |
| 753 | + | |
| 754 | + | |
753 | 755 | | |
754 | 756 | | |
755 | 757 | | |
756 | | - | |
| 758 | + | |
| 759 | + | |
757 | 760 | | |
758 | 761 | | |
759 | 762 | | |
| |||
Lines changed: 1 addition & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
192 | 192 | | |
193 | 193 | | |
194 | 194 | | |
195 | | - | |
| 195 | + | |
196 | 196 | | |
197 | 197 | | |
198 | 198 | | |
| |||
Lines changed: 1 addition & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
77 | 77 | | |
78 | 78 | | |
79 | 79 | | |
80 | | - | |
| 80 | + | |
81 | 81 | | |
82 | 82 | | |
83 | 83 | | |
| |||
Lines changed: 2 additions & 2 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
621 | 621 | | |
622 | 622 | | |
623 | 623 | | |
624 | | - | |
625 | | - | |
| 624 | + | |
| 625 | + | |
626 | 626 | | |
627 | 627 | | |
628 | 628 | | |
| |||
Lines changed: 1 addition & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
160 | 160 | | |
161 | 161 | | |
162 | 162 | | |
163 | | - | |
| 163 | + | |
164 | 164 | | |
165 | 165 | | |
166 | 166 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1406 | 1406 | | |
1407 | 1407 | | |
1408 | 1408 | | |
1409 | | - | |
| 1409 | + | |
1410 | 1410 | | |
1411 | 1411 | | |
1412 | 1412 | | |
| |||
1865 | 1865 | | |
1866 | 1866 | | |
1867 | 1867 | | |
1868 | | - | |
| 1868 | + | |
1869 | 1869 | | |
1870 | 1870 | | |
1871 | 1871 | | |
1872 | 1872 | | |
1873 | 1873 | | |
1874 | | - | |
| 1874 | + | |
1875 | 1875 | | |
1876 | 1876 | | |
1877 | 1877 | | |
| |||
Lines changed: 2 additions & 2 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
347 | 347 | | |
348 | 348 | | |
349 | 349 | | |
350 | | - | |
| 350 | + | |
351 | 351 | | |
352 | 352 | | |
353 | 353 | | |
| |||
362 | 362 | | |
363 | 363 | | |
364 | 364 | | |
365 | | - | |
| 365 | + | |
366 | 366 | | |
367 | 367 | | |
368 | 368 | | |
Lines changed: 1 addition & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
187 | 187 | | |
188 | 188 | | |
189 | 189 | | |
190 | | - | |
| 190 | + | |
191 | 191 | | |
192 | 192 | | |
193 | 193 | | |
| |||
Lines changed: 2 additions & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
333 | 333 | | |
334 | 334 | | |
335 | 335 | | |
336 | | - | |
| 336 | + | |
| 337 | + | |
337 | 338 | | |
338 | 339 | | |
339 | 340 | | |
| |||
0 commit comments