Skip to content

Commit 1cc6e2c

Browse files
os-zhuangclaude
andcommitted
fix(verify): isolate the RLS-proof stack; document write-transform fidelity gaps
Surfaced by running `objectstack verify` against the real third-party hotcrm app: - The CLI ran data-fidelity and RLS proofs against the SAME in-process stack, so the RLS phase's admin-creates collided with rows the fidelity phase had already written on unique-constrained fields (e.g. a unique `sku`/`account_number`) → a 409 that silently skipped those objects' authorization check. Boot a separate pristine stack for the RLS phase. hotcrm `--rls --multi-tenant` goes from 5-consistent/3-skipped(409) to 8-consistent/0-holes. - Document the known limitation that fields normalized on write (uppercase/trim hooks, canonicalizing formulas) read back as `fidelity-gaps` even when working as designed (hotcrm's `sku`/`account_number` uppercase hooks). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent ff23956 commit 1cc6e2c

2 files changed

Lines changed: 38 additions & 11 deletions

File tree

packages/cli/src/commands/verify.ts

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -58,20 +58,33 @@ export default class Verify extends Command {
5858
String(readEnvWithDeprecation('OS_MULTI_ORG_ENABLED', 'OS_MULTI_TENANT') ?? 'false').toLowerCase() !==
5959
'false';
6060

61-
const stack = await bootStack(config, { multiTenant });
62-
61+
// Data fidelity runs on its own pristine stack.
6362
let crud: VerifyReport;
64-
let rls: RlsReport | undefined;
65-
try {
66-
const adminToken = await stack.signIn();
67-
crud = await runCrudVerification(stack, adminToken, config);
63+
{
64+
const stack = await bootStack(config, { multiTenant });
65+
try {
66+
const adminToken = await stack.signIn();
67+
crud = await runCrudVerification(stack, adminToken, config);
68+
} finally {
69+
await stack.stop();
70+
}
71+
}
6872

69-
if (flags.rls) {
70-
const memberToken = await stack.signUp('verify-member@objectstack.test');
71-
rls = await runRlsProofs(stack, adminToken, memberToken, config);
73+
// The RLS proofs run on a SEPARATE, fresh stack. Reusing the fidelity stack
74+
// would let the RLS phase's admin-creates collide with the rows the fidelity
75+
// phase already wrote on unique-constrained fields (e.g. a unique `sku` or
76+
// `account_number`) — a 409 that silently skips the object instead of
77+
// proving its authorization.
78+
let rls: RlsReport | undefined;
79+
if (flags.rls) {
80+
const rlsStack = await bootStack(config, { multiTenant });
81+
try {
82+
const adminToken = await rlsStack.signIn();
83+
const memberToken = await rlsStack.signUp('verify-member@objectstack.test');
84+
rls = await runRlsProofs(rlsStack, adminToken, memberToken, config);
85+
} finally {
86+
await rlsStack.stop();
7287
}
73-
} finally {
74-
await stack.stop();
7588
}
7689

7790
// Failure contract: a "real" runtime break the app's author must see.

packages/verify/README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,3 +100,17 @@ isolation policies actually apply.
100100

101101
`bootStack` options: `admin`, `authSecret`, `security` (a custom `SecurityPlugin`
102102
for owner-scoped fixtures), `multiTenant`.
103+
104+
## Known limitations
105+
106+
- **Intentional write-transforms read back as fidelity gaps.** The fidelity
107+
check asserts an *exact* round-trip, so a field normalized on write — an
108+
`uppercase`/`trim` hook, a canonicalizing formula — is reported as a
109+
`fidelity-gaps` mismatch (e.g. `sku`: wrote `"abc-1"` → read `"ABC-1"`) and
110+
fails the run, even though the app behaves as designed. The report shows the
111+
exact `wrote → read` diff so it's diagnosable; letting an app declare such
112+
fields so the verifier can allow them is a planned enhancement.
113+
- **The auto-derived sweep is coarser than a hand-written matrix.** It exercises
114+
one synthesized record per object and skips fields it can't synthesize
115+
(required lookups / master-detail, media, computed). It's a broad runtime
116+
smoke test, not a substitute for targeted golden tests of specific behavior.

0 commit comments

Comments
 (0)