Skip to content

Commit 49d2a24

Browse files
fix(verify, plugin-dev, plugin-hono-server): the verify --rls report, tenancy refusals and no-API warning state each decision in words instead of a tracker number (stage 4) (#21231)
Part of #20752 Clause-②: no **Stage 4 of 5 of the `domain:cli` lane under the maintainer's A / A ruling (5902360492): the `packages/verify`, `plugin-dev` and `plugin-hono-server` strings.** The card stays open for stage 5 (`qa`), so this PR carries no closing keyword. Text only: no status, error `code`, exit code, route, field, export, control flow, or `verify` verdict or count moves. ## What this does The `objectstack verify --rls` report, its persona-provisioning refusals and the records its probe writes, the verify harness's organizations remedy, the dev plugin's tenancy refusals and no-auth warning, and the Hono server's no-API warning sent the reader to a tracker number for the reason behind them. In form D, as stages 1 to 3 applied it (PR #21172, PR #21188, PR #21219), 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. All 14 ledgered occurrences in the claim's four files (claim `5938321786`), at 13 string sites: `rls.ts` 9 (1994 x3, 7685 x3, 7978 x3), `dev-plugin.ts` 3 (4818 x2, 3963 x1), `harness.ts` 1 (4719), `hono-plugin.ts` 1 (4073). Every cited card was read first; all seven answer, and each decision was cross-read against its landing commit. ### Rewritten in words | Where (head line) | Cited | The text now says | Decision read from | |---|---|---|---| | `rls.ts:908` report header | 1994 | `=== objectstack verify (RLS / cross-owner by-id-write invariant) — APP ===` | card 1994 (a by-id update or delete must pass the row-level write filter: a member can no longer change a record it cannot see), and this module's header, which names that invariant | | `rls.ts:768` `rls-hole` detail | 1994 | "...by-id write bypassed RLS, and a caller that cannot read a record must not be able to write it" | card 1994; the module header's "A user who CANNOT READ a record must not be able to WRITE it" | | `rls.ts:431` probe-persona refusal | 1994 | "...masked by the object gate and a by-id write that bypasses RLS is unreachable." | card 1994 for the class; the 7685 half is citation-only (below) | | `rls.ts:943` position-persona report line | 7978 | `── position personas (each holds one declared position and nothing else) — N of M declared position(s) probed` | card 7978 and landing commit `f5434b0ea4` (one persona per declared position, each holding that position and nothing else) | | `dev-plugin.ts:934-935` no-auth warning | 3963 | "...anonymous access to object data is always denied, with no setting that turns that off." | card 3963 decision A1 and landing commit `3c628ce647` (the `api.requireAuth` opt-out is retired; anonymous data access is denied unconditionally); stage 1's twin wording in `os serve` | ### Citation only (the sentence already stated the decision) - `rls.ts:357` probe RLS policy description and `rls.ts:464` probe `sys_permission_set` description (7685): each already says the probe holds object read+edit plus an owner-scoped narrowing so a refusal is the record gate's. Read from card 7685 item (i) and landing commit `be37f859bc`. The 7685 citation at `rls.ts:431` goes the same way. - `rls.ts:553` position-persona refusal and `rls.ts:575` `sys_user_position` reason (7978): each already says position-gated policies only apply to a persona holding the position. - `dev-plugin.ts:874` construct-stage refusal and `dev-plugin.ts:1000` init-stage refusal (4818): each already says `OS_ALLOW_DEGRADED_TENANCY` covers an absent multi-org runtime, not a present one that declined. Read from card 4818 and landing commit `29326f8eea`; stage 1 dropped the twin citation in `os serve` the same way. - `harness.ts:626` `bootStack` remedy (4719): it already says the app's declaration is what is checked and that a package reachable only through NODE_PATH or a hoisted store is not accepted. Read from card 4719 and landing commit `02dc076927`. - `hono-plugin.ts:683` no-API boot warning (4073): it already says the plugin is a transport adapter that serves neither API. Read from card 4073 and landing commit `e5a4d26901`. The `rls.ts` report header was on the ledger, so it is rewritten to name the invariant it proves rather than losing its anchor. ## Text only, proven on the AST A scratch script (not committed) parses each changed TypeScript file at BASE `7c5a311a58` and at head `36281b515a`, folds every `+` chain made only of string literals into one value, blanks every string value and template span, and compares the remaining node sequence (kinds, identifiers, numerals). Result: identical skeleton in all six files, string-value counts equal (dev-plugin 220, hono-plugin 126, harness 117, rls 212, the two tests 105 and 24), and 15 changed values, each of them prose: 2 `description:` values, 1 `reason:`, 1 `detail:`, 2 report lines, 4 `new Error` messages, 2 logger `warn` messages, 1 remedy string, and the 2 test assertions. The later merge of `origin/main` touches none of these files. The dev plugin's no-auth warning first gained a fifth literal; commit `03a490204b` re-wrapped the sentence across the original four so the skeleton stays equal. ## The ledger `node scripts/check-doc-authoring.mjs --census-ledger`, written to a scratch file first so its no-growth check reads the committed baseline, then installed: | | occurrences | (file, id) pairs | files | |---|--:|--:|--:| | the four rows before | 14 | 7 | 4 | | the four rows after | 0 | 0 | 0 | | whole ledger before (`7c5a311a58`, also `d6d6e872e5`) | 399 | 279 | 107 | | whole ledger after | 385 | 272 | 103 | 15 lines deleted, 0 added; every other row is byte-identical. After merging `origin/main` (`d6d6e872e5`) the recomputed ledger is byte-identical to the committed one. `pnpm check:doc-authoring`: before "338 pinned site(s) across 107 file(s) ... no growth, no burn-down unrecorded", after "325 pinned site(s) across 103 file(s) ... no growth, no burn-down unrecorded". ## Pins, and that they can fail Two tests asserted an id. Each now asserts the words that carry the decision, and each was ablated through `scripts/ablation-replace.mjs` on the committed fix (anchor must hit once, blob must change, restore proven by blob equal to HEAD and an empty `git diff HEAD`). Both tests import the source by relative path, so no build sits between the mutation and the run. | Pin | Asserts now | Ablation | Result | |---|---|---|---| | `plugin-dev/src/dev-plugin-optional-load-failure.test.ts:283` | "always denied, with no setting that turns that off" | "turns that off" to "turns that on" in `dev-plugin.ts` | 1 failed / 9 passed | | `plugin-hono-server/src/hono-transport-only.test.ts:92` | "transport adapter and serves neither" | "serves neither" to "serves nothing" in `hono-plugin.ts` | 1 failed / 4 passed | Restored, the two files run 10 passed and 5 passed. No other pin asserts any of the old strings: the whole repository was searched (`packages/**` including `qa` and dogfood, `examples`, `docs/qa/**`, snapshots and JSON). The dogfood RLS suites print `formatRlsReport` only as an assertion message, and `docs/qa/platform-checklist/RUNNER.md` quotes the summary lines and `N of M declared position(s) probed`, which are unchanged. ## What ships Measured on the built `dist/` of each package: every rewritten sentence is present (in both the ESM and CJS bundles) and no old spelling is. All three packages publish `dist`, so the changeset carries `patch` for `@objectstack/verify`, `@objectstack/plugin-dev` and `@objectstack/plugin-hono-server`. ## Verification (head `36281b515a`, after merging `origin/main` `d6d6e872e5`) Heavy runs went through `scripts/pm/os-verify-lock.sh` (slot `issue-20752-s4`); each verdict line reads `VERDICT command-exit 0` unless stated. - Build: `pnpm turbo run build` over the three packages and their dependency closures, 38/38 tasks. - Tests: `@objectstack/plugin-hono-server` 27 files / 324 tests, `@objectstack/plugin-dev` 9 / 86, `@objectstack/verify` 16 / 120, all passed (before and after the merge). The dogfood RLS runner oracle (`packages/qa/dogfood/test/rls-runner.test.ts`, which drives the `rls-hole` path against the rebuilt `@objectstack/verify` dist): 17 passed. - Typecheck: `typecheck` of all three packages, each `tsc --noEmit` plus `check:test-typecheck` (the test layer, the two pins included) OK. - Gates: `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` derived 71 commands; all 71 run with exit codes recorded; `--ran` reconciliation: "71 derived famil(ies) accounted for — 71 run, 0 NOT-MEASURED". `check:dual-build-cjs-loads` first answered PREREQUISITE NOT MET (exit 3, no dist for unrelated packages); after a full workspace build (72/72 tasks, 71 cached) it passed and that rerun is the recorded result. - Lint, a proven narrowing: eslint's own config places 6 of the 8 changed paths in its population (`isPathIgnored` false and a matching config object; the changeset and the JSON ledger are outside it). `--no-inline-config` with the json formatter: 6 file results, 0 errors, 0 warnings. Invariance: this repo's config enables no type-aware linting (`eslint.config.mjs`, the note at lines 326-328) and its only disk reads are two baselines this diff does not touch, so no untouched file's verdict can move. The full `pnpm lint` is CI's. ## Acceptance notes - The seat's staging comment (5930275362) listed a dead tracker number in the `plugin-dev` test title `dev-plugin-security-enforcement-warning.test.ts:121` for this stage. Claim `5938321786` names four files and not that one, and a test title is not on the ledger, so it is not touched here. Carrier: the seat, when it stages what remains. - Code comments in the same four files still cite these cards. They are not on the ledger, and the claim keeps them out of scope. - `packages/qa/dogfood/test/enterprise-organizations.ts:184` carries the 4719 twin of the harness remedy. It sits in the `qa` row, which stage 5 owns. --- _Generated by [Claude Code](https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 3a7b6eb commit 49d2a24

8 files changed

Lines changed: 39 additions & 32 deletions

File tree

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
'@objectstack/verify': patch
3+
'@objectstack/plugin-dev': patch
4+
'@objectstack/plugin-hono-server': patch
5+
---
6+
7+
The `verify --rls` report and messages, the dev plugin's tenancy and no-auth messages and the Hono server's no-API warning no longer cite tracker numbers; each one states the decision behind it in words
8+
9+
Clause-②: no
10+
11+
Strings these three packages show to operators, and print in verification reports, sent the reader to 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.
12+
13+
- `@objectstack/verify`, the `objectstack verify --rls` report: the header reads `=== objectstack verify (RLS / cross-owner by-id-write invariant) — <app> ===`, and the position-persona line reads `── position personas (each holds one declared position and nothing else) — N of M declared position(s) probed`.
14+
- `@objectstack/verify`, an `rls-hole` verdict's detail: it says the by-id write bypassed RLS, and that a caller that cannot read a record must not be able to write it.
15+
- `@objectstack/verify`, the refusal when the RLS probe persona cannot be provisioned (no ObjectQL engine): it says a by-id write that bypasses RLS is what becomes unreachable. The matching position-persona refusal drops its citation.
16+
- `@objectstack/verify`, the records the probe writes: the probe permission set's row-level-security policy description, the probe `sys_permission_set` row's description and the position persona's `sys_user_position` reason drop their citations. Each already said what it is for.
17+
- `@objectstack/verify`, the `bootStack` refusal for `multiTenant: true` when the app does not declare `@objectstack/organizations`: the citation beside "a package merely reachable through NODE_PATH or a hoisted workspace store is not accepted" goes.
18+
- `@objectstack/plugin-dev`, the `REST API NOT enabled` warning for a stack that mounts no auth: it says anonymous access to object data is always denied, with no setting that turns that off.
19+
- `@objectstack/plugin-dev`, the two refusals for an `OrganizationsPlugin` that refused to be constructed or failed to initialize: they drop their citations. Each already says `OS_ALLOW_DEGRADED_TENANCY` covers only an absent multi-org runtime, not a present one that declined.
20+
- `@objectstack/plugin-hono-server`, the boot warning for a server with no data or discovery API mounted: it drops its citation. It already says the plugin is a transport adapter that serves neither.
21+
22+
Text only: no status, error code, exit code, route, field, export, verdict or count moves. A log filter or script that matched the old text (for example the report header's `RLS / #NNNN` spelling) needs the new spelling.

‎packages/plugins/plugin-dev/src/dev-plugin-optional-load-failure.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,7 @@ describe('DevPlugin — an optional service that is installed and fails to const
280280
const line = allLines(ctx).find((l) => l.includes('REST API NOT enabled'));
281281
expect(line, 'the no-auth refusal is reported on its own terms').toBeDefined();
282282
expect(line).toContain('no auth is mounted');
283-
expect(line).toContain('#3963');
283+
expect(line).toContain('always denied, with no setting that turns that off');
284284
expect(line).toContain('NOT a missing-package problem');
285285
// And the false claim it used to emit instead is gone.
286286
expect(allLines(ctx).some((l) => l.includes('@objectstack/rest not installed'))).toBe(false);

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -871,7 +871,7 @@ export class DevPlugin implements Plugin {
871871
+ (mountCode !== undefined ? `code: ${String(mountCode)} — ` : '')
872872
+ `${mountMessage}. OS_ALLOW_DEGRADED_TENANCY does NOT apply to this failure and will `
873873
+ 'not get past it: it covers an ABSENT multi-org runtime the operator accepts doing '
874-
+ 'without, not a present one that declined. (#4818)',
874+
+ 'without, not a present one that declined.',
875875
);
876876
}
877877
ctx.logger.info(` ✔ Organizations plugin enabled (posture '${tenancyPosture}': organization_id auto-stamp, per-org seed)`);
@@ -931,9 +931,9 @@ export class DevPlugin implements Plugin {
931931
if (!authMounted) {
932932
ctx.logger.warn(
933933
' ✘ REST API NOT enabled: no auth is mounted in this stack, so no caller could ever '
934-
+ 'authenticate and anonymous access to object data is always denied (#3963). This is NOT a '
935-
+ 'missing-package problem — @objectstack/rest was never consulted. Install/enable '
936-
+ 'plugin-auth (or the `auth` tier), or drop the REST API from this dev stack.',
934+
+ 'authenticate and anonymous access to object data is always denied, with no setting that '
935+
+ 'turns that off. This is NOT a missing-package problem — @objectstack/rest was never '
936+
+ 'consulted. Install/enable plugin-auth (or the `auth` tier), or drop the REST API from this dev stack.',
937937
);
938938
} else {
939939
try {
@@ -997,7 +997,7 @@ export class DevPlugin implements Plugin {
997997
+ 'requested multi-organization isolation must not serve traffic without it (ADR-0093 D5). '
998998
+ 'The plugin reported (verbatim — the framework does not interpret it): '
999999
+ `${err?.message ?? String(err)}. OS_ALLOW_DEGRADED_TENANCY does NOT apply: it covers an `
1000-
+ 'ABSENT multi-org runtime, not a present one that declined. (#4818)',
1000+
+ 'ABSENT multi-org runtime, not a present one that declined.',
10011001
);
10021002
}
10031003
ctx.logger.error(`Failed to init child plugin ${plugin.name}: ${err.message}`);

‎packages/plugins/plugin-hono-server/src/hono-plugin.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -680,7 +680,7 @@ export class HonoServerPlugin implements Plugin {
680680
if (!hasPlugin(REST_API_PLUGIN) && !hasPlugin(RUNTIME_DISPATCHER_PLUGIN)) {
681681
ctx.logger.warn(
682682
'No data or discovery API is mounted on this server. HonoServerPlugin is a '
683-
+ 'transport adapter and serves neither (#4073). Mount `createRestApiPlugin` '
683+
+ 'transport adapter and serves neither. Mount `createRestApiPlugin` '
684684
+ 'from @objectstack/rest for full CRUD behind the gate stack, or '
685685
+ '`createDispatcherPlugin` from @objectstack/runtime.',
686686
);

‎packages/plugins/plugin-hono-server/src/hono-transport-only.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ describe('#4073 end state — the plugin serves transport and /me/* only', () =>
8989
// The message must carry the remedy, not just the diagnosis.
9090
expect(hit[0]).toContain('@objectstack/rest');
9191
expect(hit[0]).toContain('@objectstack/runtime');
92-
expect(hit[0]).toContain('#4073');
92+
expect(hit[0]).toContain('transport adapter and serves neither');
9393
});
9494

9595
it('a REST-composed boot stays quiet', async () => {

‎packages/verify/src/harness.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -623,7 +623,7 @@ export async function bootStack(
623623
'authority on what it has to publish.'
624624
: `Install/link it in THIS APP (${hostRoot}) — and DECLARE it in that app's ` +
625625
'package.json, which is what is actually checked: a package merely reachable through ' +
626-
'NODE_PATH or a hoisted workspace store is not accepted (#4719) — to run multi-org fixtures.';
626+
'NODE_PATH or a hoisted workspace store is not accepted — to run multi-org fixtures.';
627627
throw new Error(
628628
'verify: multiTenant=true requires the enterprise @objectstack/organizations package (migrated from plugin-org-scoping, ADR-0105 D12). ' +
629629
`${remedy} (${(e as Error).message})`,

‎packages/verify/src/rls.ts‎

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -354,7 +354,7 @@ export function rlsProbePermissionSet(config: any): PermissionSet {
354354
label: `RLS probe scope for ${o.name}`,
355355
description:
356356
'Verifier-authored owner narrowing (select only) — puts the probe persona outside the ' +
357-
'scope of every record it did not create, so the by-id-write class is reachable (#7685).',
357+
'scope of every record it did not create, so the by-id-write class is reachable.',
358358
object: o.name,
359359
operation: 'select',
360360
using: 'created_by == current_user.id',
@@ -428,7 +428,7 @@ export async function provisionRlsProbePersona(
428428
throw new Error(
429429
'verify: cannot provision the RLS probe persona — no ObjectQL engine on this stack. ' +
430430
'The probe needs object-level read+edit grants, without which every by-id probe is ' +
431-
'masked by the object gate and the #1994 class is unreachable (#7685).',
431+
'masked by the object gate and a by-id write that bypasses RLS is unreachable.',
432432
);
433433
}
434434
const sysCtx = { context: { isSystem: true } };
@@ -461,7 +461,7 @@ export async function provisionRlsProbePersona(
461461
description:
462462
'Ephemeral persona minted by `objectstack verify --rls`: object-level read+edit on every ' +
463463
'declared object plus an owner-scoped SELECT narrowing, so a by-id refusal is attributable ' +
464-
'to the record gate rather than the object gate (#7685).',
464+
'to the record gate rather than the object gate.',
465465
object_permissions: JSON.stringify(probeSet.objects ?? {}),
466466
field_permissions: '{}',
467467
system_permissions: '[]',
@@ -550,7 +550,7 @@ export async function provisionRlsPositionPersona(
550550
throw new Error(
551551
`verify: cannot provision the RLS position persona for '${position}' — no ObjectQL engine on ` +
552552
'this stack. Without the position assignment the app\'s position-gated policies are not ' +
553-
'applicable to the persona, so the app-authored narrowing is unreachable (#7978).',
553+
'applicable to the persona, so the app-authored narrowing is unreachable.',
554554
);
555555
}
556556
const sysCtx = { context: { isSystem: true } };
@@ -572,7 +572,7 @@ export async function provisionRlsPositionPersona(
572572
business_unit_id: null,
573573
organization_id: null,
574574
granted_by: null,
575-
reason: `Ephemeral persona minted by \`objectstack verify --rls\`: holds '${position}' and nothing else, so the app's own position-gated RLS narrowing is exercised (#7978).`,
575+
reason: `Ephemeral persona minted by \`objectstack verify --rls\`: holds '${position}' and nothing else, so the app's own position-gated RLS narrowing is exercised.`,
576576
},
577577
sysCtx,
578578
);
@@ -765,7 +765,7 @@ async function probeAsPersona(
765765
object,
766766
status: 'rls-hole',
767767
target: origin,
768-
detail: `the probe cannot read it (GET ${bRead.status}) yet MUTATED it by id (PATCH ${bWrite.status}) — by-id write bypassed RLS (#1994 class)${via}`,
768+
detail: `the probe cannot read it (GET ${bRead.status}) yet MUTATED it by id (PATCH ${bWrite.status}) — by-id write bypassed RLS, and a caller that cannot read a record must not be able to write it${via}`,
769769
});
770770
} else {
771771
results.push({
@@ -905,7 +905,7 @@ function summaryLine(s: RlsSummary): string {
905905
}
906906

907907
export function formatRlsReport(report: RlsReport): string {
908-
const lines: string[] = [`\n=== objectstack verify (RLS / #1994) — ${report.app} ===`];
908+
const lines: string[] = [`\n=== objectstack verify (RLS / cross-owner by-id-write invariant) — ${report.app} ===`];
909909
for (const r of report.results) {
910910
lines.push(` ${statusMark(r.status)} ${r.object} [${r.status}] ${r.detail ?? ''}`);
911911
}
@@ -940,7 +940,7 @@ export function formatRlsReport(report: RlsReport): string {
940940
// would claim N× the reach the fan-out actually has.
941941
const cov = report.positionCoverage;
942942
lines.push(
943-
`\n ── position personas (#7978) — ${cov.ran.length} of ${cov.declared.length} declared position(s) probed`,
943+
`\n ── position personas (each holds one declared position and nothing else) — ${cov.ran.length} of ${cov.declared.length} declared position(s) probed`,
944944
);
945945
if (cov.note) lines.push(` · ${cov.note}`);
946946
for (const run of report.positionRuns) {

‎scripts/doc-authoring-prose-id.baseline.json‎

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -176,10 +176,6 @@
176176
"packages/plugins/plugin-audit/src/translations/zh-CN.objects.generated.ts": {
177177
"#11507": 1
178178
},
179-
"packages/plugins/plugin-dev/src/dev-plugin.ts": {
180-
"#3963": 1,
181-
"#4818": 2
182-
},
183179
"packages/plugins/plugin-email/src/email-service.ts": {
184180
"#5172": 1
185181
},
@@ -189,9 +185,6 @@
189185
"packages/plugins/plugin-email/src/templates/auth-templates.ts": {
190186
"#8019": 4
191187
},
192-
"packages/plugins/plugin-hono-server/src/hono-plugin.ts": {
193-
"#4073": 1
194-
},
195188
"packages/plugins/plugin-security/src/bootstrap-system-capabilities.ts": {
196189
"#5876": 1,
197190
"#8552": 2
@@ -483,13 +476,5 @@
483476
},
484477
"packages/triggers/trigger-record-change/src/record-change-trigger.ts": {
485478
"#3457": 1
486-
},
487-
"packages/verify/src/harness.ts": {
488-
"#4719": 1
489-
},
490-
"packages/verify/src/rls.ts": {
491-
"#1994": 3,
492-
"#7685": 3,
493-
"#7978": 3
494479
}
495480
}

0 commit comments

Comments
 (0)