From 38552c83687dcf1f5786c5bd7a9983ed20ac9e52 Mon Sep 17 00:00:00 2001 From: Hassan Abdel-Rahman Date: Tue, 29 Sep 2026 17:54:43 -0400 Subject: [PATCH 1/2] Resolve the non-grantable suite's carried policy through the gated entry `resolveOperation` refuses every write it leaves to the write lock, which is every write to a stored card, so the `carrying` helper's positive control (a granted `annotate`) was refused along with the writes the test means the gate to refuse, and those refusals could not fail. The helper resolves through `resolveGatedOperation`, as a caller that takes the lock's decision does, and returns the definition. Co-Authored-By: Claude Opus 5.5 --- .../tests/realm-policy-non-grantable-test.ts | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/packages/realm-server/tests/realm-policy-non-grantable-test.ts b/packages/realm-server/tests/realm-policy-non-grantable-test.ts index 5ff2c830b1d..2b8db51efe3 100644 --- a/packages/realm-server/tests/realm-policy-non-grantable-test.ts +++ b/packages/realm-server/tests/realm-policy-non-grantable-test.ts @@ -12,6 +12,7 @@ import type { } from '@cardstack/runtime-common'; import { newOperationScope, + resolveGatedOperation, resolveOperation, scopeCallerFor, type CompiledRealmPolicy, @@ -378,6 +379,11 @@ module(basename(import.meta.filename), function (hooks) { // a compiled policy holding `rules`, as the policy would be had compiling let // them through. It shows what the gate refuses of a grant that reached a // compiled policy however it came to be there. + // + // It resolves as a caller that takes the write lock's decision does. The + // plain resolution refuses every write it would leave to the lock, which is + // every write to a stored card, so through it a write the gate admits would + // be refused as well, and a refusal would say nothing about the gate. function carrying( rules: CompiledRealmPolicy['rules'], url: string, @@ -393,16 +399,18 @@ module(basename(import.meta.filename), function (hooks) { ...core, policy: { ...core.policy!, compiledPolicy: async () => policy }, }; - return (name) => - resolveOperation( - carried, - { kind: 'instance', url }, - name, - newOperationScope(carried, { - caller: scopeCallerFor(READER), - coarseDeclined: 'all', - }), - ); + return async (name) => + ( + await resolveGatedOperation( + carried, + { kind: 'instance', url }, + name, + newOperationScope(carried, { + caller: scopeCallerFor(READER), + coarseDeclined: 'all', + }), + ) + ).definition; } function assertNotPermitted( From e3409902280afc7134f65d3d0bcdcd9f445f084b Mon Sep 17 00:00:00 2001 From: Hassan Abdel-Rahman Date: Tue, 29 Sep 2026 18:09:47 -0400 Subject: [PATCH 2/2] Pin that the carried policy reaches the gate, and judge the reader as one The realm's own policy grants `annotate` too, so the positive control passed whether or not the carried policy was consulted; a carried policy that grants nothing now has to refuse it. The helper's scope declines the reader's writes rather than everything, as the realm does for a caller who may read it, so a resolution failure keeps its own code and only the gate's refusal matches `operation-not-permitted`. Co-Authored-By: Claude Opus 5.5 --- .../tests/realm-policy-non-grantable-test.ts | 29 ++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/packages/realm-server/tests/realm-policy-non-grantable-test.ts b/packages/realm-server/tests/realm-policy-non-grantable-test.ts index 2b8db51efe3..21e20f2fb05 100644 --- a/packages/realm-server/tests/realm-policy-non-grantable-test.ts +++ b/packages/realm-server/tests/realm-policy-non-grantable-test.ts @@ -375,15 +375,17 @@ module(basename(import.meta.filename), function (hooks) { }); } - // Resolves invocations on `url` for a reader the realm ACL declined, against - // a compiled policy holding `rules`, as the policy would be had compiling let - // them through. It shows what the gate refuses of a grant that reached a - // compiled policy however it came to be there. + // Resolves invocations on `url` for the reader, whom the realm ACL lets read + // and not write, against a compiled policy holding `rules`, as the policy + // would be had compiling let them through. It shows what the gate refuses of + // a grant that reached a compiled policy however it came to be there. // - // It resolves as a caller that takes the write lock's decision does. The - // plain resolution refuses every write it would leave to the lock, which is - // every write to a stored card, so through it a write the gate admits would - // be refused as well, and a refusal would say nothing about the gate. + // It resolves through the gated entry and keeps only the definition, so it + // refuses only what the gate refuses: a write the gate admits resolves, + // though the write lock would still judge its stored card. The plain + // resolution refuses every write it would leave to the lock, which is every + // write to a stored card, so through it an admitted write would be refused + // too, and a refusal would say nothing about the gate. function carrying( rules: CompiledRealmPolicy['rules'], url: string, @@ -407,7 +409,7 @@ module(basename(import.meta.filename), function (hooks) { name, newOperationScope(carried, { caller: scopeCallerFor(READER), - coarseDeclined: 'all', + coarseDeclined: 'writes', }), ) ).definition; @@ -508,7 +510,14 @@ module(basename(import.meta.filename), function (hooks) { assert.strictEqual( (await resolve('annotate')).base, 'transform', - 'the carried policy is the one the gate consults', + 'the carried grant admits annotate', + ); + // The realm's own policy grants annotate too, so only a carried policy + // that grants nothing shows the gate consults the carried one. + await assert.rejects( + carrying([], LEDGER_1)('annotate'), + /operation-not-permitted/, + 'the gate consults the carried policy, which grants nothing', ); for (let name of ['seal', 'update']) { await assert.rejects(