Skip to content

Policy: admit anonymous writes as a user the realm's config names - #6577

Merged
habdelra merged 13 commits into
mainfrom
cs-13579-policy-admit-anonymous-writes-as-a-user-the-realms-config
Oct 6, 2026
Merged

habdelra merged 13 commits into
mainfrom
cs-13579-policy-admit-anonymous-writes-as-a-user-the-realms-config

Conversation

@habdelra

@habdelra habdelra commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Important

Merge order (each must merge before the next):

  1. Compile a policy grant's opt-in to callers who aren't signed in #6534: compile a grant's anonymous opt-in (merged)
  2. Policy: admit anonymous reads through a grant that opts in, rate-limited per realm and address #6568 (CS-13536): anonymous reads (merged)
  3. Policy: admit anonymous searches through a query grant that opts in #6575 (CS-13578): anonymous search, stacked on Policy: admit anonymous reads through a grant that opts in, rate-limited per realm and address #6568
  4. This PR (CS-13579): anonymous writes, stacked on Policy: admit anonymous searches through a query grant that opts in #6575
    4a. Policy: let a grant on a declared operation admit a caller who isn't signed in #6579 (CS-13605): anonymous named operations, stacked on Policy: admit anonymous writes as a user the realm's config names #6577. boxel-skills#174's §12 describes it
    4b. Policy: explain how a realm treats a caller who isn't signed in #6581 (CS-13609): explaining a visitor's refusal, stacked on Policy: let a grant on a declared operation admit a caller who isn't signed in #6579. boxel-skills#174's §10/§12 describe it
  5. Add the judgment calls to the realm-policy-authoring skill boxel-skills#175 (CS-13108): merged, and merged into boxel-skills#174. This section is §12, Refusals §13, its section §14 and the checklist §15.
    5a. Say that no policy grant reaches a path the realm ignores boxel-skills#178 (CS-13336): merged, and merged into boxel-skills#174
  6. Teach the realm-policy-authoring skill anonymous grants and a realm's anonymous traffic controls boxel-skills#174 (CS-13571): the authoring skill for anonymous grants; re-checked against what shipped
  7. State every limit of a realm policy in the skills boxel-skills#179 (CS-13109): states every policy limit in realm-policy-authoring and card-operations-authoring; stacked on boxel-skills#174, merges after it
    7a. Say that a policy's rules are not secret from the writers of other realms boxel-skills#181 (CS-13398): a realm-policy-authoring §9 bullet, stacked on z-index handling for modals opening modals #179, merges right after it
  8. CS-13572 (CS-13571): once every open skills PR has merged, one boxel-skills release (v0.1.10) covering boxel-skills#175 and boxel-skills#177 (both merged), Add deployment for host package #178, Refactor indexer to run wholly inside the host app #174, z-index handling for modals opening modals #179 and Refactor Realm's handle method to accept a response object #181, then one plain-title boxel PR moving the pin to it

Every PR in this order carries this callout. Whoever opens one of the PRs above tells the owners of the others, and each owner updates its own callout.

In service of CS-13521, which lets a realm's policy admit a caller who isn't signed in: an OperationGrant on a base operation opts in with anonymous: true, an anonymous write is made as a user the governed realm names, and the governed realm limits and blocks such callers by address. Reads and searches are already switched on. This PR switches on writes.

Who a write is made as. A grant that opts a write in names actingUser, a key in the governed realm's realm.json config (compiled as CompiledOperationGrant.anonymous.actingUserKey).

  • The key is resolved when the write is decided, never when the policy compiles. So changing the config, removing the key, or the user losing write takes effect on the next write without touching the policy card.
  • The value must be a Matrix user id (isMatrixUserId), and the user must hold write on the realm's ACL (RealmPermissionChecker).
  • A grant whose key doesn't resolve admits nothing. The gate drops it before it judges any predicate, so the write is refused as if the grant weren't there rather than made as nobody.
  • Resolution lives in card-operations/acting-users.ts: one ActingUsers per request, shared by every scope the request builds. Each key is resolved once, and the request remembers who its admitted writes were made as and why any key failed (key-missing, not-a-matrix-id, no-write).
  • The realm supplies the resolver through OperationPolicyAccess.actingUser, so capability checks and explain judge a visitor's write by the same check. That includes a capability check on a realm anyone may read, whose ACL lets the visitor's check through: its write lane is judged by the gate wherever the policy opens a write to such callers.
  • The coordinator's index job is initiated by the first admitted write's acting user (CommitBatchOptions.actingUser). It shares that user's writer lane, and that user's reads wait for it as for their own writes, so a realm's config should name a dedicated account rather than a person who edits the realm. The caller's actor stays empty, so actor() in a program still refuses, and a grant whose where reads actor() never admits a visitor, even on a card naming its acting user.

Where writes are admitted. card+json POST, PATCH and DELETE are tagged with their operations, and /_operations with any operation, so the realm admits a visitor to them exactly as it admits one to a read. card+source writes and _atomic stay ACL-only. Created cards get realm-minted ids, as they already do for any caller who can't read the realm. Every refusal a non-reader would see as not found, including a POST to a subdirectory that the destination check refuses, is the visitor's 401 actor-required (#refusalSeenBy). The card-write routes now render that 401 and the count refusals with their codes.

Counting. A visitor's write is counted only once the batch is admitted. The coordinator gains beforeCommit, which runs after every entry is admitted and staged and before anything is written, and the realm counts there (#anonymousCommit).

  • A card+json write costs one unit. An /_operations batch costs one unit per entry. A batch that only reads is counted once its reads have run.
  • A batch that doesn't fit is refused whole with 429 rate-limited and Retry-After, and nothing is written. A count that can't be read gives 503 rate-limit-unavailable.
  • A refused write costs nothing, and so does a batch with no entries. A request is counted once however many times its batch commits.
  • An address with no budget left is turned away at admission, before anything runs, as for reads.
  • The dispatch's counting mode is now explicit: once-served (reads and search), by-handler (capability checks and writes), or never (stylesheets).

Telemetry. An admitted write's anonymous-request record carries actingUsers and, for a batch, cost. A refusal whose grants failed on their acting user carries actingUserFailures, so "why was my form's submission refused" points at the config key.

Tests: realm-policy-anonymous-write-test.ts covers:

  • feedback created as the submitter;
  • each acting-user failure, and its record;
  • a config change and a removed key taking effect on the next write;
  • an update admitted only where its condition holds, with identical 401s for a closed and a missing draft;
  • an actor() grant never admitting a visitor;
  • the 429 with nothing written;
  • refused writes costing nothing;
  • batch counting, all-or-nothing;
  • the subdirectory POST;
  • the capability write lane;
  • explain;
  • signed-in callers unaffected.

The anonymous read test moves to the new posture: an envelope create stays closed, an envelope read is now open, and explain judges a write whose acting user the realm doesn't name as denied. The card-write, capabilities, school-example, explain and gate policy tests run unchanged.

🤖 Generated with Claude Code

habdelra and others added 2 commits October 6, 2026 13:36
A grant that opts a write in to callers who aren't signed in admits it only
where its acting-user key names, in the governed realm's current realm.json
config, a Matrix user who may write the realm; the write is made as that user.
card+json POST/PATCH/DELETE and /_operations admit such callers. A write is
counted once every entry is admitted and before anything is written, one unit
per entry, and a batch that doesn't fit gets 429 with nothing written. Every
refusal a non-reader would see as not found is the anonymous 401. Capability
checks and explain judge such writes through the same acting-user check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…mitted routes are recorded

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-06T17:51:33.707583Z 82b55d0 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 82b55d04e9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

request,
requestContext,
scope,
() => entries.length,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid charging empty operations batches

For an anonymous caller admitted to /_operations, an empty boxel:operations list is explicitly valid, and a query-target entry can also expand to zero entries. This passes 0 to #chargeCaller, but DBAnonymousRateLimiter.charge rejects costs below one, so the request is reported as 503 rate-limit-unavailable instead of returning the valid empty result. Skip beforeCommit when entries.length === 0, as #countAnonymous already does for zero-cost capability checks.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code 🤖] Fixed in 611d92da3b. #anonymousCommit's count returns without charging when the cost is below one, so an empty batch, or one whose many target finds nothing, is answered as it is for anyone and costs nothing. Pinned by "an empty batch is answered as it is for anyone, and costs nothing".

Comment thread packages/runtime-common/realm.ts Outdated
let batch = this.#mintPosture(coarseDeclined);
let batch = {
...this.#mintPosture(coarseDeclined),
...this.#anonymousCommit(request, requestContext, scope, () => 1),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Charge an anonymous PATCH request only once

When an admitted anonymous PATCH is unchanged but its clean index document cannot be read, #patchAdmittedCard invokes its commit closure once normally and again with reserialize=true. Because this shared batch now contains beforeCommit, both invocations charge the visitor, so one PATCH consumes two rate-limit units and can even return 429 on its internal retry after the first unit was already spent. Make the charge callback one-shot for the request or omit it from the recovery commit.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code 🤖] Fixed in 611d92da3b. The count is memoized per request (counting ??= count()), so a PATCH that commits again to reserialize is still one write and one unit, and the second commit can't 429 after the first has landed.

@habdelra habdelra left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code 🤖] Independent review of this PR's own diff (head 82b55d0), read against the worktree. The core gate path holds up. I couldn't find a way for a visitor to write through a grant that didn't opt in, or through one whose acting user doesn't resolve. actor()-reading grants never admit, card+source and _atomic stay ACL-only, and ids are minted on every anonymous create path. The acting user is resolved against the governed realm's config and ACL, including explain's draft core. beforeCommit sits after staging and every admit, and before commitStaged. #disclosableFailure and seenBy keep hidden and missing targets indistinguishable, including for a 429 raised after admission. Signed-in paths add no queries, and the new #cardWriteRefusal branches only match codes a signed-in caller never gets.

Findings inline:

  1. An envelope with zero entries makes the rate limiter throw, so the caller gets a 503 and the realm logs a warning.
  2. A capability check on a world-readable realm refuses every visitor write the routes would admit.
  3. Visitor index jobs share the acting user's writer lane and read-your-writes drain, which the WriteOptions comment says can't happen.
  4. The PATCH reserialize retry charges a visitor twice.
  5. Acting-user resolution runs before any charge on refused writes, and can call Matrix.
  6. Test gaps.

Comment thread packages/runtime-common/realm.ts Outdated
return {
actingUser: () => scope.actingUsers.admitted[0],
beforeCommit: async () => {
let counted = await this.#chargeCaller(request, caller, cost(), {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code 🤖] A batch with no entries gets a 503 from the rate limiter. beforeCommit charges cost() with no cost < 1 guard (#countAnonymous has one). In the envelope, cost is entries.length, which is 0 for an empty boxel:operations list. The envelope parser documents that list as valid. It is also 0 for an expect: "many" query target whose anonymously granted search finds nothing. writes.length === 0 sends that batch to beforeCommit. Then DBAnonymousRateLimiter.charge throws on a cost below 1, #chargeCaller catches the throw as a count failure, and this returns rate-limit-unavailable. The visitor gets a 503 where a signed-in caller gets 200 []. The realm also logs a "could not count" warning and records an unavailable event for every such request.

Suggested fix: return early when cost() < 1 (count nothing and refuse nothing), and add a test for an empty batch and a zero-match many batch.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code 🤖] Fixed in 611d92da3b. A cost below one counts nothing and refuses nothing. Pinned by "an empty batch is answered as it is for anyone, and costs nothing".

Comment thread packages/runtime-common/realm.ts Outdated
// what a grant opts in to them, made as a user who may write the realm.
if (
(await this.#policyJudges(write.refusal, requestContext)) ||
requestContext.anonymousCaller

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code 🤖] On a world-readable realm, this check refuses every visitor write the routes would admit. _capabilities is judged as a read. On a realm whose ACL gives * read, a visitor passes that check, so #admitsAnonymous never runs and requestContext.anonymousCaller is never set. The write probe then refuses with CoarseAuthenticationRequired, and #policyJudges is false because no user signed in. This branch is skipped, so every write pair comes back writesRefused: 'actor-required'.

The real POST/PATCH/DELETE and /_operations are judged as write. Their refusal makes the visitor an anonymous caller, and the gate admits them through the opted-in grant. A public site with a feedback form, which is the main use for this feature, would hide its submit button from visitors even though the submission succeeds.

Suggested fix: when write.refusal is CoarseAuthenticationRequired and nobody signed in, ask whether the policy opens any write to anonymous callers (the same summary #anonymousCallerFor reads), and judge the lane through the gate then. The current test realm isn't world-readable, so this needs a test on one that is.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code 🤖] Fixed in 611d92da3b. When the write lane's refusal is CoarseAuthenticationRequired and nobody signed in, #capabilityLanes asks #anonymousCallerFor with the anonymous-eligible write operations (ANONYMOUS_WRITE_CHECK, never counted). If the policy opens a write to such callers and the realm doesn't refuse the address, the gate judges the lane, acting-user check included. Pinned by "a realm anyone may read answers a capability check about a write it opens to such callers", which also posts the write on the same world-readable realm.

Comment thread packages/runtime-common/realm.ts Outdated
// see `drainRequestersOwnIndexing`).
// reads. Absent for system-originated writes. A write by a caller who isn't
// signed in is initiated by the acting user it is made as (see
// `CommitBatchOptions.actingUser`), whose job no reader waits on: such a

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code 🤖] "Whose job no reader waits on" isn't true when the acting user is a real account. initiatingUser becomes the acting user (coordinator opts.actor ?? opts.actingUser?.()). The initiatedBy key does two things:

  • It chooses the writer lane: indexingWriterLane and prerenderHtmlWriterLane use #user:<acting user>.
  • It drives drainRequestersOwnIndexing, through incrementalIndexingInitiatedBy(authenticatedUser).

So if the config names a person, such as the editor or owner, two things follow. Their own writes index in the same lane as every visitor submission. And each of their reads waits, up to the drain budget, for in-flight visitor jobs. A burst of visitors from many addresses then slows a real user's editing and reads.

Either correct the comment and state that the config should name a dedicated account, or keep the attribution in telemetry and give anonymous jobs a lane of their own, for example null or the owner lane, rather than the acting user's.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code 🤖] Fixed in 611d92da3b. The comment now states the contract: a visitor's write shares its acting user's writer lane, and that user's reads wait for it as for their own writes, so a realm's config names a dedicated account rather than a person who edits the realm. Attribution stays on the acting user, which is the point of naming one. The authoring skill's anonymous section (boxel-skills#174) is being updated to tell authors to configure a dedicated account.

Comment thread packages/runtime-common/realm.ts Outdated
let batch = this.#mintPosture(coarseDeclined);
let batch = {
...this.#mintPosture(coarseDeclined),
...this.#anonymousCommit(request, requestContext, scope, () => 1),

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code 🤖] A visitor PATCH can be charged twice. beforeCommit charges on every commitBatch call that carries these batch options. #patchAdmittedCard calls commit() a second time, as commit(true), when the patch changed nothing and the index holds no document for the card. Each call reaches beforeCommit, so the visitor pays 2 units for one write. If the first charge used their last unit, they get a 429 after the first commit has already landed.

Suggested fix: make the charge run at most once per admission (memoize the promise in #anonymousCommit), or leave the batch options' beforeCommit off the reserialize call.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code 🤖] Fixed in 611d92da3b. The count is memoized per admission, so the reserialize commit doesn't charge again.

Comment thread packages/runtime-common/realm.ts Outdated
await fetchRealmPermissions(this.#dbAdapter, new URL(this.url)),
this.#matrixClient,
);
return (await checker.can(user, 'write'))

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code 🤖] Each visitor write that matches an opted-in grant does this resolution before any charge. Each such request costs a realm_permissions SELECT and a config read. can() goes through effectiveRealmPermissions, which calls matrixClient.getProfile(user) whenever the ACL has a users row. Resolution runs in decide, before predicates are evaluated. A refused write costs nothing, so the only limit is the admission-time remaining() check, which refusals never use up. A visitor can repeat a write whose condition fails, without limit, and each repeat costs the realm a DB round trip and possibly a Matrix call.

This is low severity, but consider either of these:

  • Check the user's write permission without the Matrix lookup when they are listed explicitly.
  • Cache the resolution briefly, keyed by the config and permissions version.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code 🤖] Leaving this as is. The resolution is one config read and one realm_permissions SELECT per request, resolved once per key however many entries name it. That is the same per-request ACL read every signed-in write already pays in checkPermission, and the Matrix profile lookup is the existing RealmPermissionChecker behaviour for ACLs with a users row. A visitor who repeats a refused write is still turned away at admission once their address is out of budget, and the realm's blocklist covers an address that abuses refusals. A short-lived cache keyed on the config and ACL would add a staleness window to exactly the check that has to take effect on the next write.

);
}

test('a caller who is not signed in leaves feedback, written as the user the realm names', async function (assert) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code 🤖] Test gaps. These behaviors this PR relies on have no test:

  • Minted ids. A visitor POST (and an envelope create) carrying its own id or lid gets a realm-minted id. The current assertion only checks startsWith(NEWSROOM).
  • Side-loads. A visitor card+json create or update with included side-loads is refused, and nothing is written.
  • Index job attribution. The write's index job has initiatedBy set to the acting user, and the caller's actor stays empty.
  • Envelope existence. An envelope update to a closed draft and to a missing one gives byte-identical 401s. Only card+json is covered.
  • Read-only batches. A visitor batch that only reads is counted once per entry, and gets a 429 when it doesn't fit.
  • Empty batches. A visitor batch with no entries, or with a zero-match expect: "many" target. This currently gives a 503.
  • 503. A write whose count can't be read gives rate-limit-unavailable with Retry-After, and nothing is written.
  • World-readable realm. A capability check on a world-readable realm with an anonymous write grant. This currently answers denied.
  • Delete. A visitor DELETE admitted through an unconditional or a card-only where grant. The only delete test is the actor() one.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code 🤖] Added in 611d92da3b:

  • a client-chosen lid replaced by a realm-minted id;
  • a side-load refused, with nothing written;
  • byte-identical envelope 401s for a closed and a missing draft;
  • a read-only batch counted per entry, then 429;
  • an empty batch costing nothing;
  • a DELETE admitted through a card-only where grant;
  • the world-readable capability check.

Not added:

  • The 503 path. The test server has no hook to make the limiter fail mid-request; the path shares anonymousCountRefusal with the 429 the tests do cover.
  • The index job's initiatedBy. It is asserted indirectly by the actingUsers on the admitted record, which reads the same ActingUsers.admitted the coordinator takes initiatingUser from.

…dable capability checks about anonymous writes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…searches-through-a-query-grant-that' into cs-13579-policy-admit-anonymous-writes-as-a-user-the-realms-config
@habdelra
habdelra requested a review from a team October 6, 2026 18:22
…y-grant-that' into cs-13579-policy-admit-anonymous-writes-as-a-user-the-realms-config

# Conflicts:
#	packages/runtime-common/realm.ts

@lukemelia lukemelia left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code 🤖] The design reads well: every acting-user failure fails closed, counting happens only once a batch is admitted and before anything is written, and the test module covers each bullet in the description. One structural request, not a blocker.

Adopt AnonymousAdmission for the new realm.ts methods. The base branch extracted anonymous admission and counting into that collaborator, and the realm already delegates to it in ten places. This branch merged the extraction in but added three private methods to realm.ts that do the same kind of work inline: #anonymousCommit, #actingUsersFor and #resolveActingUser. Together they are about seventy lines with one caller each, so the move is mechanical, and the anonymous-write tests are thorough enough to catch a slip. The inline comments say where each should go. #refusalSeenBy and the capability-check wiring are fine where they are: the first is about the coarse-declined posture for any non-reader, and the second is the join between the ACL outcome and the dispatch.

Since the collaborator lives on the base branch, which is still open, the move is best made once that branch settles rather than racing it.

Comment thread packages/runtime-common/realm.ts Outdated
// the caller's budget once every entry is admitted and before anything is
// written, so a batch the gate refuses costs nothing, and one that doesn't
// fit is refused whole. Nothing for any other caller.
#anonymousCommit(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code 🤖] This is entirely about counting an anonymous caller's batch, and its only dependency is the collaborator's own chargeCaller. It reads as a method on AnonymousAdmission, something like commitOptions(request, requestContext, scope, cost), returning the same Pick<CommitBatchOptions, 'actingUser' | 'beforeCommit'>. The two call sites would spread its result exactly as they do now.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code 🤖] Done in 77f23e5b83. The acting-user lookup is AnonymousAdmissionEnvironment.actingUser, a callback the realm supplies with its config and ACL reads. OperationPolicyAccess.actingUser points at the same function through AnonymousAdmission.resolveActingUser, so the gate, explain, capability checks and the collaborator all judge a key the same way. actingUsersFor and commitOptions are methods on AnonymousAdmission, and the two call sites spread commitOptions as they did before. Anonymous-write and capabilities tests: 43/43.

Comment thread packages/runtime-common/realm.ts Outdated

// The acting users of a request a caller who isn't signed in sent, one set
// for the request, which every scope it builds shares.
#actingUsersFor(requestContext: RequestContext): ActingUsers | undefined {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code 🤖] This lazily builds the request's one ActingUsers from a resolver, which is the same kind of per-request bookkeeping as callerFor. It belongs beside it on AnonymousAdmission, with the resolver coming from the environment (see the comment on #resolveActingUser).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code 🤖] Done in 77f23e5b83. The acting-user lookup is AnonymousAdmissionEnvironment.actingUser, a callback the realm supplies with its config and ACL reads. OperationPolicyAccess.actingUser points at the same function through AnonymousAdmission.resolveActingUser, so the gate, explain, capability checks and the collaborator all judge a key the same way. actingUsersFor and commitOptions are methods on AnonymousAdmission, and the two call sites spread commitOptions as they did before. Anonymous-write and capabilities tests: 43/43.

Comment thread packages/runtime-common/realm.ts Outdated

// Who `key` names in this realm's current `realm.json` `config`, and
// whether that user may write the realm, as the realm's ACL says now.
async #resolveActingUser(key: string): Promise<ActingUserResolution> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code 🤖] This one should stay realm code, since it reads realm config, fetches the ACL and consults the permission checker, but as an environment callback rather than a private method. AnonymousAdmissionEnvironment already supplies hasPolicy, openedOperations and access that way; an actingUser(key) entry fits the same pattern, and OperationPolicyAccess.actingUser would point at the same function so the gate, explain and the collaborator all judge a key identically.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code 🤖] Done in 77f23e5b83. The acting-user lookup is AnonymousAdmissionEnvironment.actingUser, a callback the realm supplies with its config and ACL reads. OperationPolicyAccess.actingUser points at the same function through AnonymousAdmission.resolveActingUser, so the gate, explain, capability checks and the collaborator all judge a key the same way. actingUsersFor and commitOptions are methods on AnonymousAdmission, and the two call sites spread commitOptions as they did before. Anonymous-write and capabilities tests: 43/43.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Host Test Results

  1 files  ±    0    1 suites  ±0   8m 16s ⏱️ - 1h 45m 0s
298 tests  - 4 879  298 ✅  - 4 868  0 💤  - 11  0 ❌ ±0 
298 runs   - 4 894  298 ✅  - 4 883  0 💤  - 11  0 ❌ ±0 

Results for commit bfbdb49. ± Comparison against earlier commit 4e9eda5.

Realm Server Test Results

    1 files  ±0    317 suites  ±0   2h 5m 7s ⏱️ + 4m 28s
5 057 tests ±0  5 057 ✅ ±0  0 💤 ±0  0 ❌ ±0 
5 116 runs  ±0  5 116 ✅ ±0  0 💤 ±0  0 ❌ ±0 

Results for commit bfbdb49. ± Comparison against earlier commit 4e9eda5.

…searches-through-a-query-grant-that' into cs-13579-policy-admit-anonymous-writes-as-a-user-the-realms-config
habdelra and others added 5 commits October 6, 2026 16:15
…y-grant-that' into cs-13579-policy-admit-anonymous-writes-as-a-user-the-realms-config
…y-grant-that' into cs-13579-policy-admit-anonymous-writes-as-a-user-the-realms-config
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…y-grant-that' into cs-13579-policy-admit-anonymous-writes-as-a-user-the-realms-config
…y-grant-that' into cs-13579-policy-admit-anonymous-writes-as-a-user-the-realms-config
…y-grant-that' into cs-13579-policy-admit-anonymous-writes-as-a-user-the-realms-config
@habdelra
habdelra changed the base branch from cs-13578-policy-admit-anonymous-searches-through-a-query-grant-that to main October 6, 2026 23:14
@habdelra
habdelra merged commit eddea6f into main Oct 6, 2026
113 of 115 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants