A space created through com.atproto.simplespace is never declared to the authority store, so listRepos/registerNotify/notifyWrite answer SpaceNotFound #105

Closed
opened 2026-08-21 02:10:00 +00:00 by claude-bot · 2 comments
Collaborator

ISimpleSpaceStore and ISpaceAuthorityStore hold separate state, and nothing bridges them at space-creation time.

CreateSimpleSpaceEndpoint (src/ATProtoNet.Server/Spaces/SimpleSpaceEndpoints.cs) writes only through ISimpleSpaceStore.CreateSpaceAsync. But three of the authority endpoints gate on the other store first:

  • ListSpaceReposEndpointSpaceAuthorityEndpoints.cs:156
  • RegisterNotifyEndpointSpaceAuthorityEndpoints.cs:228
  • NotifyWriteEndpointSpaceAuthorityEndpoints.cs:348

Each calls ISpaceAuthorityStore.GetSpaceStateAsync and throws SpaceNotFound unless it answers Granted. InMemorySpaceAuthorityStore answers Granted only for a space passed to DeclareSpace, and EfCoreSpaceAuthorityStore<T> (added in #102) only for one passed to DeclareSpaceAsync. Neither is called by anything but tests — grep -rn DeclareSpace --include=*.cs finds only SpaceServerEndpointTests.

So a service registered the documented way — AddSpaceAuthority<T>(key).AddSimpleSpace<T>() — creates a space that mints credentials correctly (the access policy reads the simplespace store) but whose writer set can never be populated: notifyWrite is refused with SpaceNotFound, registerNotify is refused, and listRepos returns the same. Since the writer set is the sync boundary, no syncer can find anything in the space. The operator's only fix today is to call DeclareSpace/DeclareSpaceAsync out of band after every createSpace, which is not documented anywhere.

The same gap exists for deletion: deleteSpace flags the space in the simplespace store, but the authority store keeps answering Granted, so listRepos on a deleted space returns its writer set instead of SpaceDeleted.

Worth deciding where the bridge belongs rather than patching one call site:

  • have the simplespace endpoints call ISpaceAuthorityStore.DeclareSpace…/MarkDeleted… (needs those on the interface, which they currently are not — they are implementation-specific methods on both concrete stores), or
  • ship an ISpaceAuthorityStore adapter that reads space existence and deletion from an ISimpleSpaceStore and keeps only the writer set and subscriptions of its own, which is what a PDS actually wants, or
  • let GetSpaceStateAsync fall back to the registered ISpaceAccessPolicy.

A regression test belongs with it: create a space through createSpace, then drive notifyWrite and listRepos for it through the test host, as SpaceServerEndpointTests already does for a manually declared space.


Filed by Claude while working on #102 (run).

`ISimpleSpaceStore` and `ISpaceAuthorityStore` hold separate state, and nothing bridges them at space-creation time. `CreateSimpleSpaceEndpoint` (`src/ATProtoNet.Server/Spaces/SimpleSpaceEndpoints.cs`) writes only through `ISimpleSpaceStore.CreateSpaceAsync`. But three of the authority endpoints gate on the *other* store first: - `ListSpaceReposEndpoint` — `SpaceAuthorityEndpoints.cs:156` - `RegisterNotifyEndpoint` — `SpaceAuthorityEndpoints.cs:228` - `NotifyWriteEndpoint` — `SpaceAuthorityEndpoints.cs:348` Each calls `ISpaceAuthorityStore.GetSpaceStateAsync` and throws `SpaceNotFound` unless it answers `Granted`. `InMemorySpaceAuthorityStore` answers `Granted` only for a space passed to `DeclareSpace`, and `EfCoreSpaceAuthorityStore<T>` (added in #102) only for one passed to `DeclareSpaceAsync`. Neither is called by anything but tests — `grep -rn DeclareSpace --include=*.cs` finds only `SpaceServerEndpointTests`. So a service registered the documented way — `AddSpaceAuthority<T>(key).AddSimpleSpace<T>()` — creates a space that mints credentials correctly (the access policy reads the `simplespace` store) but whose writer set can never be populated: `notifyWrite` is refused with `SpaceNotFound`, `registerNotify` is refused, and `listRepos` returns the same. Since the writer set is the sync boundary, no syncer can find anything in the space. The operator's only fix today is to call `DeclareSpace`/`DeclareSpaceAsync` out of band after every `createSpace`, which is not documented anywhere. The same gap exists for deletion: `deleteSpace` flags the space in the `simplespace` store, but the authority store keeps answering `Granted`, so `listRepos` on a deleted space returns its writer set instead of `SpaceDeleted`. Worth deciding where the bridge belongs rather than patching one call site: - have the `simplespace` endpoints call `ISpaceAuthorityStore.DeclareSpace…`/`MarkDeleted…` (needs those on the interface, which they currently are not — they are implementation-specific methods on both concrete stores), or - ship an `ISpaceAuthorityStore` adapter that reads space existence and deletion from an `ISimpleSpaceStore` and keeps only the writer set and subscriptions of its own, which is what a PDS actually wants, or - let `GetSpaceStateAsync` fall back to the registered `ISpaceAccessPolicy`. A regression test belongs with it: create a space through `createSpace`, then drive `notifyWrite` and `listRepos` for it through the test host, as `SpaceServerEndpointTests` already does for a manually declared space. --- _Filed by Claude while working on #102 ([run](https://git.grandiras.net/Grandiras/claude-bot/actions/runs/339))._
Author
Collaborator

🤖 On it — follow along in the action run; the reply will link the full session transcript.

📜 Live transcript — updating every ~5 minutes while the task runs.

🤖 On it — follow along in [the action run](https://git.grandiras.net/Grandiras/claude-bot/actions/runs/359); the reply will link the full session transcript. <!-- claude-run:comment --> 📜 [Live transcript](https://git.grandiras.net/Grandiras/claude-bot/src/branch/claude-logs/transcripts/Grandiras/ATProto.NET/issue-105/run-359.md) — updating every ~5 minutes while the task runs.
Author
Collaborator

Pushed claude/issue-105.

What the fix is

I took the adapter option from the issue rather than adding DeclareSpace…/MarkDeleted… to ISpaceAuthorityStore. The two stores answer different questions, and only one of them is written by createSpace:

  • New SimpleSpaceAuthorityStore (src/ATProtoNet.Server/Spaces/SimpleSpaceAuthorityStore.cs) implements ISpaceAuthorityStore by reading space existence and deletion from an ISimpleSpaceStore, and delegating the writer set and the notification registrations to an inner store.
  • AddSpaceAuthority<TStore>() now registers TStore concretely and resolves ISpaceAuthorityStore through a factory that wraps it in the bridge when an ISimpleSpaceStore is present. The decision happens at resolution, so AddSpaceAuthority<T>().AddSimpleSpace<T>() works in either order.
  • Existence is read, not copied, so deleteSpace needs no second write: listRepos answers SpaceDeleted from the same moment, which is what tells a syncer that missed the notification to drop its copy.
  • A space the simplespace store has never heard of falls through to the inner store, so DeclareSpace/DeclareSpaceAsync keep working for a bespoke space type running alongside the baseline. A store registered as ISpaceAuthorityStore before AddSpaceAuthority<T>() is left exactly as registered (documented; wrap by hand).
  • InMemorySpaceStoreWarning unwraps the bridge before checking whether the writer set is in-process, so the startup line still appears.

Tests

SpaceServerEndpointTests now drives the path the issue asked for through the test host — createSpace over the owner's session, notifyWrite with real service auth signed by the writing account, then listRepos with a credential minted for that space — plus registerNotify on a created space, listRepos after deleteSpace (using a credential minted before the deletion, since a credential outlives the space), and notifyWrite for a space neither store knows still answering SpaceNotFound. The two existing tests that called DeclareSpace out of band no longer need to. SimpleSpaceAuthorityStoreTests covers the store directly (granted / deleted / fall-through / unknown / write-through), and SpaceStoreRegistrationTests pins both registration orders and the unwrapped no-simplespace case.

Verified: dotnet build clean (0 warnings), dotnet test tests/ATProtoNet.Tests/1708 passed, 0 failed, 5 skipped. I also confirmed the new tests are genuine regression tests by stubbing the bridge back out — 4 of them fail without it — then restored and re-ran.

Docs: the docs/spaces.md paragraph that told operators to declare each simplespace space to the authority store now describes the bridge and scopes DeclareSpace… to bespoke space types; the XML docs on both DeclareSpace overloads say the same. CHANGELOG.md has an entry under [Unreleased] → Fixed.


⏱ 7m 50s · 42 turns · tokens: 136.6k in / 30.3k out (+4230.9k cache-read)
📜 Full session transcript
→ Opened PR: #106

Pushed `claude/issue-105`. ## What the fix is I took the **adapter** option from the issue rather than adding `DeclareSpace…`/`MarkDeleted…` to `ISpaceAuthorityStore`. The two stores answer different questions, and only one of them is written by `createSpace`: - New `SimpleSpaceAuthorityStore` (`src/ATProtoNet.Server/Spaces/SimpleSpaceAuthorityStore.cs`) implements `ISpaceAuthorityStore` by **reading** space existence and deletion from an `ISimpleSpaceStore`, and delegating the writer set and the notification registrations to an inner store. - `AddSpaceAuthority<TStore>()` now registers `TStore` concretely and resolves `ISpaceAuthorityStore` through a factory that wraps it in the bridge when an `ISimpleSpaceStore` is present. The decision happens at resolution, so `AddSpaceAuthority<T>().AddSimpleSpace<T>()` works in **either** order. - Existence is read, not copied, so `deleteSpace` needs no second write: `listRepos` answers `SpaceDeleted` from the same moment, which is what tells a syncer that missed the notification to drop its copy. - A space the `simplespace` store has never heard of **falls through** to the inner store, so `DeclareSpace`/`DeclareSpaceAsync` keep working for a bespoke space type running alongside the baseline. A store registered as `ISpaceAuthorityStore` before `AddSpaceAuthority<T>()` is left exactly as registered (documented; wrap by hand). - `InMemorySpaceStoreWarning` unwraps the bridge before checking whether the writer set is in-process, so the startup line still appears. ## Tests `SpaceServerEndpointTests` now drives the path the issue asked for through the test host — `createSpace` over the owner's session, `notifyWrite` with real service auth signed by the writing account, then `listRepos` with a credential minted for that space — plus `registerNotify` on a created space, `listRepos` after `deleteSpace` (using a credential minted before the deletion, since a credential outlives the space), and `notifyWrite` for a space neither store knows still answering `SpaceNotFound`. The two existing tests that called `DeclareSpace` out of band no longer need to. `SimpleSpaceAuthorityStoreTests` covers the store directly (granted / deleted / fall-through / unknown / write-through), and `SpaceStoreRegistrationTests` pins both registration orders and the unwrapped no-`simplespace` case. Verified: `dotnet build` clean (0 warnings), `dotnet test tests/ATProtoNet.Tests/` → **1708 passed, 0 failed, 5 skipped**. I also confirmed the new tests are genuine regression tests by stubbing the bridge back out — 4 of them fail without it — then restored and re-ran. Docs: the `docs/spaces.md` paragraph that told operators to declare each `simplespace` space to the authority store now describes the bridge and scopes `DeclareSpace…` to bespoke space types; the XML docs on both `DeclareSpace` overloads say the same. `CHANGELOG.md` has an entry under `[Unreleased] → Fixed`. --- ⏱ 7m 50s · 42 turns · tokens: 136.6k in / 30.3k out (+4230.9k cache-read) 📜 [Full session transcript](https://git.grandiras.net/Grandiras/claude-bot/src/branch/claude-logs/transcripts/Grandiras/ATProto.NET/issue-105/run-359.md) → Opened PR: https://git.grandiras.net/Grandiras/ATProto.NET/pulls/106
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Grandiras/ATProto.NET#105
No description provided.