fix(security): runtime strings state each decision in words instead of a tracker number (stage 5) - #21518
Conversation
…f a tracker number (stage 5, wip) Rewrites the plugin-security refusals, explain details, field help and log lines that cited a tracker number so each one says what was decided. Text only. Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
… help carries the shared-vocabulary decision (stage 5, wip) Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
…ld help (stage 5, wip) Written by node scripts/check-i18n-bundles.mjs --write --filter=plugin-security. Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
…s (stage 5, wip) Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
…ger; plugin-security goes to zero (stage 5, wip) Shrink only: 49 lines deleted, 0 added; no other entry moves. Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
Brings in stage 4 of the services lane and the plugin-security changes landed since the branch was cut. Only the prose-id ledger conflicted; it is recomputed with --census-ledger on the merged tree (shrink only: the plugin-security entries go to zero, no other entry moves). Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 11 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 10 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 16 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 7ed7a07f92edeaacbd12aa74ed2c203f06128ee4 && git checkout 7ed7a07f92edeaacbd12aa74ed2c203f06128ee4
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 529d9711fb1403221c2d5d641b018dda21aec5e1 cb919dc030a456c96986cfb0cb72f59dfbeb5bc3 && git checkout -B drift-repro 529d9711fb1403221c2d5d641b018dda21aec5e1 && git merge --no-ff cb919dc030a456c96986cfb0cb72f59dfbeb5bc3
node scripts/docs-audit/affected-docs.mjs --json 529d9711fb1403221c2d5d641b018dda21aec5e1
|
Part of #20751
Clause-②: no
Stage 5 of the
domain:serviceslane under the maintainer's A / A ruling (5902360492): theplugin-securitystrings. The card stays open for the later stages, so this PR carries no closing keyword. Text only: no status, errorcode, 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_byfield help onsys_permission_set/sys_position(with theires-ES,ja-JPandzh-CNvariants). 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(claim5960178324). They were re-derived from the ledger onorigin/mainat94a8761a8, and again on the merged tree atcb919dc03(withorigin/mainf9a8eb889merged 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 excludedshare-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 inplugin-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.
security-plugin.ts:7056, the curated capability-name refusal (403 PERMISSION_DENIED)sys_capabilityrow with a name inPLATFORM_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 refusalsobjectstack-ai/cloudanswers 403 to this session, andadd_repowas refused. The text states the behaviour the code enforces and what its own docblock records (delegated-admin-gate.ts:600: that card madebusiness_unit_idthe read-scope depth anchor, so "anchoring only narrows, never widens").objects/sys-permission-set.object.ts:336,objects/sys-position.object.ts:288, themanaged_byfield help, and the generatedenleavesmanaged_byis unified onsys_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 warningmanaged_by, and a row that is "not provably ours" is left alone.bootstrap-system-capabilities.ts:749,:753, the two remediation tails of that warningsecurity-plugin.ts:2216, the public-form strip warningowner_id,organization_idor audit column is stripped at the data layer.security-plugin.ts:5148,:8279, thecontrolled_by_parentwrite-gate and master read-scope failurescontrolled_by_parentchild 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 guardssecurity-plugin.ts:5248, the row-level write gate's sharing-verdict failureallowreplaces only the platform's own ownership floor.security-plugin.ts:5448, the authored-policy verdict failureadmitonly when an app-authored policy matches, andabstainin every other case, a throw included.security-plugin.ts:5506, thegetReadFiltersharing-scope failuresecurity-plugin.ts:5544, the on-behalf-of read-scope refusalsecurity-plugin.ts:7672, the platform-owner wall-bypass audit lineOS_PLATFORM_OWNER_EMAILaccount; writes keep the ADR-0123 D2 refusal.cleanup-package-permissions.ts:195, the uninstall linepackage_idso authorization lapses at once), and ADR-0090 D5's "No ghost grants".unresolved-posture.ts:229,:233,:242, the three fail-closed log linesnormalize-managed-by.ts:190, the normalizer lineCitation 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'sproblemtext 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 invalidmaskingRulewarning (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_byhelp on both objects: thees-ES,ja-JPandzh-CNleaves 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-securityrewrote the twoenleaves from the object source. Noenleaf and no source-hash digest was hand-edited. The fourmanaged_bylocale leaves have no digest entry (they are legacy-trusted), and the merge kept every hand-written value.Tests
Four assertions in three test files held the old text. Each now holds the new substance, and no
codeorstatusassertion is touched.metadata-outage-unresolved-cause.test.ts: the assertion that the outage explain detail contains the 3545 citation now assertstoContain('rather than defaulting to public/uncontracted'). The assertion that all three log lines contain the fail-closed suffix with the 3545 citation now assertstoContain('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 withgit checkout HEAD; the restore was proven by blob hash and an emptygit 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, throughscripts/pm/os-verify-lock.shafter 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 includescheck:test-typecheck: OKovertsconfig.test.json, so the re-pinned test files are compiled.In the published surface,
dist/index.jsanddist/index.mjscarry 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)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.node scripts/check-doc-authoring.mjs --census-ledgeron the merged tree. Against main's copy: 49 lines deleted, 0 added. The 11plugin-securityfiles leave the ledger (43 occurrences, 27 pairs), and no other entry moves.--census-ledgerrefuses 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.tsfiles. Controls on a scratch copy ofsecurity-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 against94a8761a8and againstf9a8eb889are equal).Gates
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandsatcb919dc03derives 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)."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), andcheck:dual-build-cjs-loads(106 require entry points across 66 packages load).eslint --no-inline-config --format jsonover the 17 changed.tsfiles atcb919dc03reports 17 files linted (none ignored), 0 errors and 0 warnings.eslint.config.mjsenables no type-aware linting (noparserOptions.project), so this diff cannot change the verdict on any untouched file. The repo-widepnpm lintis CI's.Acceptance notes
plugin-sharing'ssharing-service.tscarries the same 5493 citation in its own abstain line. That is theplugin-sharingstage of this card, and this PR leaves it alone.managed_byleaves changed although they never carried the number, so that each locale carries the same decision asen.Generated by Claude Code