A collaborative coding-agent orchestrator for atproto radl.app

add optional ability to have non-members add comments to goals #95

targeting main from radial/impl-32b840557498
Closed opened by and assigned to claudebot.disnetdev.com
targeting main from radial/impl-32b840557498

Guest comments, discovered client-side and blessed into the fold #

Implements the plan end to end — all eight steps, including the UI and the docs.

Branch radial/impl-32b840557498 · Commit 7046702 (base b16d9fb)

What landed #

Three new lexicons (packages/lexicons/lexicons/), all new collections, so the additive-only rule holds trivially and no signed record is affected:

  • com.disnetdev.radial.setGuestComments {space, enabled} — admin-authored, latest-wins over a default of false
  • com.disnetdev.radial.blessComment {goal, comment, body} — the guest's message pinned by uri#cid plus a snapshot of the body at that version
  • com.disnetdev.radial.retractBless {bless} — withdrawn by the blessing's author or an admin

Core. GoalView.blessedComments / .retractedBlessings (goal matched by URI, like a message and like an ending — a legacy closed edit already minted a second goal cid); MaterializedIndex.guestCommentsEnabled; a blessComment invariant in validation.ts (the comment ref must name a message and must not be the goal). TurnBundle.guestComments is built from blessedComments and from nothing else, and is deliberately not merged into thread — so an answer request, which resolves its subject out of threadMessages alone, can never name a guest comment.

Digest. A new blessedComments section. A blessing is a record (records) and a decision; without the second, a retractBless one operator had ingested and the other had not would leave both digests identical while their bundles differed.

Sidecar. radial comment bless --goal REF --comment AT-URI, radial comment retract --bless REF, radial space guest-comments --space REF --enabled true|false. bless fetches the guest's record through the existing resolver (which reaches any DID's PDS), refuses it if the record does not itself name the goal, and copies the cid and body from that one read.

Daemon. A ## Community comments section in bundle.md, after the thread, naming both DIDs. Guest bodies are deliberately excluded from the image-locator scan.

UI. New src/lib/guests.ts (Constellation client, member filtering, foreign fetch, re-validation, the section's rows), a Community section and a guest composer on the goal page behind guestCommentsEnabled, and the admin switch on the space page. Blessing is a two-step with the full text in front of the reader. The three collections were added to RADIAL_WRITES.

The load-bearing decision #

Blessing copies rather than points — docs/adr-blessing-copies-a-guest-comment.md. A pointer would put IO inside the fold (every materializer, browser tab included, fetching a repo nobody polls), would let a guest change what a member vouched for, and would make two daemons' bundles differ by dispatch timing. The snapshot is also what lets the section still draw something when Constellation is unreachable.

Discovery never enters RecordStore, materialize() or the digest, so two materializers with the same records agree whether or not that third-party service answered.

Verification #

pnpm lint, pnpm test, pnpm build all green; generate.mjs --check current.

  • The 200-permutation whole-index deepEqual now covers the new records: goldenScenario() grows a guest who authors nothing in the fixture (the invariant, not a gap), five blessings — one live, one withdrawn by an admin who did not write it, one by its own author, one from a since-removed member, one naming a goal the space does not hold — a retract from a member with no authority, and admin off→on toggles plus a non-admin one.
  • Negative cases asserted explicitly: a bless from a non-member is dropped by trust; an unauthorized retract associates and does nothing; an unblessed comment is absent from buildTurnBundle() and from bundle.md; a retracted blessing disappears from both.
  • bundle-writer.test.mjs holds the structural guarantee over the source: bundle.guestComments is the only thing the serializer reads guest prose from, and the image scan does not touch it.
  • guests.test.ts (17 cases, mocked fetch): a malformed index entry does not lose its page, a valid message filed under the wrong goal is refused, a non-message locator is never even fetched, an unreadable hit is counted rather than swallowed.
  • Community.svelte.test.ts mounts the section in a real DOM and asserts a hostile body (<img src=x onerror=…>) renders as its own source with no element produced — while **bold** still renders, so it is the safe path rather than an unrendered one.
  • guest-comments.test.mjs drives comment bless against two LocalPds fixtures: the body is copied, the version pinned, a mismatched goal refused, the blessing written to the member's repo.

Remaining risks #

  • Constellation has no SLA and incomplete backfill. A comment written while the index was down may never be discovered. Contained: discovery is best-effort decoration, the endpoint is a single configurable constant, and everything trusted works with it entirely offline. The section says so when a lookup fails rather than showing an empty list.
  • A browser cannot send a courtesy user-agent — it is a forbidden header, and asking for a custom one instead would turn every query into a CORS preflight. The Origin header identifies us; this is documented at the constant.
  • A member blessing malicious text is the genuinely new risk, and it is bounded exactly where a hostile member message already is (design §13). The confirm shows the full text, the bundle section is separated and labelled, and retractBless gives immediate revocation — including by an admin who did not make the mistake.
  • Adding collections narrows an already-issued OAuth token relative to what the app now writes. The PDS refusal is already recognised and explained by write.ts ("sign out and sign in again"); noted in packages/ui/README.md.
  • The UI fixture carries no guest comments, so the section is only exercised by its own tests and by a live space — the comp this fixture reproduces has nothing to draw here.
  • Not done, and out of scope per the plan: per-DID hiding (deferred until spam is real) and bless-truncation rules for a body that overflows to a blob.

Radial artifact

Commits 2

Report a blessing by what the bundle carries, and unbreak CI
Let non-members comment, and let a member carry one across