From 14c2ead9c0759fb834623762d426a61c57b9cd6b Mon Sep 17 00:00:00 2001 From: Hassan Abdel-Rahman Date: Mon, 5 Oct 2026 12:34:18 -0400 Subject: [PATCH 1/2] Let an operation grant admit callers who aren't signed in `OperationGrant` gains `anonymous`, which opens the grant's base operation to callers with no session, and `actingUser`, the key in the governed realm's realm.json settings naming the user an anonymous write is made as. Co-Authored-By: Claude Opus 5.5 --- realm-policy/realm-policy.gts | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/realm-policy/realm-policy.gts b/realm-policy/realm-policy.gts index 36b2c7c2..48a00597 100644 --- a/realm-policy/realm-policy.gts +++ b/realm-policy/realm-policy.gts @@ -28,6 +28,7 @@ import { import PolicyPredicateField from '@cardstack/catalog/fields/policy-predicate/policy-predicate'; import { subscribeToRealm } from '@cardstack/runtime-common'; import StringField from 'https://cardstack.com/base/string'; +import BooleanField from 'https://cardstack.com/base/boolean'; import { BoxelInput, Button, @@ -59,6 +60,22 @@ export class OperationGrant extends FieldDef { // A BXL boolean expression over the caller and the target. Absent means // the grant is unconditional. @field where = contains(PolicyPredicateField); + // Whether the grant also admits a caller who isn't signed in. Off, it + // admits only signed-in callers, so a grant written before anonymous + // callers could reach a policy never starts admitting them. Only a grant + // on a base operation may set it. Such a caller has no actor, so a `where` + // that reads `actor()` never holds for them: an anonymous grant is scoped by + // what the target holds. How hard anonymous callers may use the realm, and + // which addresses it refuses, are the governed realm's own settings, not + // the policy's. + @field anonymous = contains(BooleanField); + // For an anonymous grant on a write: the key, in the governed realm's + // `realm.json` settings, whose value is the user the write is made as. + // That user must be able to write the realm themselves. The realm names + // the user rather than the policy, because the policy can live in another + // realm and its writers must not decide whose identity this realm's writes + // carry. + @field actingUser = contains(StringField); static embedded = class Embedded extends Component { }; From 79bae3357dbc23d53e89c7e1700ca57daecd7f2c Mon Sep 17 00:00:00 2001 From: Hassan Abdel-Rahman Date: Mon, 5 Oct 2026 12:58:12 -0400 Subject: [PATCH 2/2] Say exactly which grants may admit anonymous callers The opt-in applies to a base operation under its own name, and the realm doesn't evaluate a `where` reading `actor()` for an anonymous caller. The acting-user key is used exactly as written. Drop an unstyled class. Co-Authored-By: Claude Opus 5.5 --- realm-policy/realm-policy.gts | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/realm-policy/realm-policy.gts b/realm-policy/realm-policy.gts index 48a00597..d7b2d070 100644 --- a/realm-policy/realm-policy.gts +++ b/realm-policy/realm-policy.gts @@ -62,19 +62,19 @@ export class OperationGrant extends FieldDef { @field where = contains(PolicyPredicateField); // Whether the grant also admits a caller who isn't signed in. Off, it // admits only signed-in callers, so a grant written before anonymous - // callers could reach a policy never starts admitting them. Only a grant - // on a base operation may set it. Such a caller has no actor, so a `where` - // that reads `actor()` never holds for them: an anonymous grant is scoped by - // what the target holds. How hard anonymous callers may use the realm, and - // which addresses it refuses, are the governed realm's own settings, not - // the policy's. + // callers could reach a policy never starts admitting them. Only a grant on + // a base operation under its own name (`read`, `update`, …) may set it, not + // one on a custom operation or a named query. The realm doesn't evaluate a + // `where` that reads `actor()` for such a caller, so a grant whose `where` + // does never admits one: an anonymous grant is scoped by what the target + // holds. How hard anonymous callers may use the realm, and which addresses + // it refuses, are the governed realm's own settings, not the policy's. @field anonymous = contains(BooleanField); - // For an anonymous grant on a write: the key, in the governed realm's - // `realm.json` settings, whose value is the user the write is made as. - // That user must be able to write the realm themselves. The realm names - // the user rather than the policy, because the policy can live in another - // realm and its writers must not decide whose identity this realm's writes - // carry. + // For an anonymous grant on a write: the key, exactly as written, in the + // governed realm's `realm.json` settings, whose value is the user the write + // is made as. The realm names the user rather than the policy, because the + // policy can live in another realm and its writers must not decide whose + // identity this realm's writes carry. @field actingUser = contains(StringField); static embedded = class Embedded extends Component { @@ -92,7 +92,7 @@ export class OperationGrant extends FieldDef { {{/if}} {{#if @model.anonymous}} - + anyone {{#if @model.actingUser}}