Automations and webhooks for the AT Protocol airglow.run

Record actions silently write to the signed-in account's repo, with no hint of which account #12

Closed tyb.there-hugo.dev
Closed opened by tyb.there-hugo.dev

Problem #

A Create record action always writes to the automation owner's repo (lib/actions/record.ts:29-30, lib/automations/pds.ts:587-591). When the record points at something owned by another DID, it is created in the wrong repo and has no effect. For example, an app.bsky.graph.listitem whose list belongs to a different account is ignored by Bluesky.

Airglow still logs "Success / 200", and the action editor (app/islands/action-editors/record.tsx:43-62) never says which account the record is created in.

This was the core blocker of the session.

Seen in the session #

In https://www.youtube.com/watch?v=5MnT-ZgbBIc:

  • About 45 minutes, 0:50 to 1:36. The user was signed in as their personal account while the list belonged to the AT Science account.
  • "There's no list item... I don't get it. This shouldn't be that hard. I feel like something has to be broken."
  • 1:23: "does it even say where it's going to? Is it going to post it to my account?"

Fix direction #

  • State the target repo in the action editor ("Creates the record in @handle's repo").
  • Warn when a strongRef or at-uri field such as list points at a DID other than {{self}}.
  • See also M2: showing the created record URI would have exposed the wrong DID.

Details: docs/brainstorms/2026-10-17-graze-conference-automation-feedback.md (M1).

Queued: the loop will work on this version of the issue.

loop:queued cid=bafyreiabyolemem5fao4lihj2mmu44lvg3re7apsnpx447yq2bqwgyi2xu

Working on this, attempt 1.

loop:claim run=contribute-20261026-224153-3my32fgxkok22 branch=loop/3my32fgxkok22 attempt=1 issue=12

feat(form): name the target repo of record actions and warn on refs into another repo

Every action that writes a record now says which repo it writes to: "Creates the record in @handle's repo, the account you are signed in as." patch-record says "Updates the record" instead. A Create record or Update record action whose template, or patch-record base URI, holds a literal AT URI into another account's repo now shows a warning naming the field and the repo. The case from the session was a listitem whose list belongs to AT Science while the user was signed in as their personal account.

Verified #

  • pnpm run check and pnpm run test pass (175 files, 2702 tests).
  • New unit tests in app/islands/action-editors/foreign-repo-refs.test.ts cover DID and handle authorities, the owner's own DID and handle, {{self}}/{{event.did}} authorities, bare placeholders, nested strongRef uri and array paths, and non-JSON templates.
  • registry.test.ts covers draftForeignRepoRefs for record, patch-record (base URI) and an app action (no check).
  • AutomationForm.test.tsx ("target repo") renders a listitem action and checks four cases: the repo line, the warning for a foreign list, no warning for at://{{self}}/..., and no repo line on a webhook.

Acceptance criteria #

Decisions the owner can reverse #

  • The repo line is in renderActionBody, so every record-producing action shows it (Bluesky post, follow, Margin, Semble and so on), not only Create record. The account is the viewer's handle, and on every form the viewer is the owner (isOwner on edit and duplicate).
  • The warning only checks literal AT URIs. An authority built from a placeholder ({{event.did}}) is only known at run time, so it is skipped. A value that is not an at:// URI, such as a listitem's subject DID, is skipped too.
  • The warning fires on any foreign AT URI in a Create or Update record template, so a hand-written like of a fixed post also warns. The text says this is fine for a like or a reply. It does not block saving. The app-specific actions are not checked, because they point at other people's records by design.
  • patch-record's base URI is checked as well: an update with a foreign base URI is written to the owner's record with the same rkey. That is the same silent wrong-repo write.
  • The warning shows the DID, not a resolved handle (resolving it would need a network lookup in the form).

Review: https://tangled.org/exosphere.site/airglow/compare/main...loop/3my32fgxkok22

loop:pushed run=contribute-20261026-224153-3my32fgxkok22 sha=afaa01308e718407c03bb3bc194a3784bb145b40

Verdict: merge. The branch covers both fix directions in the issue. Every record-writing action's editor now names the repo it writes to ("Creates/Updates the record in @handle's repo, the account you are signed in as."). A Create record or Update record action with a literal AT URI into another account's repo, such as a listitem's list, shows a warning that names the field and the repo. pnpm run check and pnpm run test (175 files, 2702 tests) both pass on afaa013.

What I checked and how:

  • Target repo. lib/actions/record.ts and patch-record.ts always write to automation.did (createArbitraryRecord / patchArbitraryRecord with repo: did). The form uses the viewer's handle and DID. I checked all three routes that render AutomationForm (new, edit, duplicate). Edit and duplicate are gated on isOwner, which is strictly viewer.did === did with no admin bypass. New, including ?from= duplicates of another user's automation, always saves as the viewer. So the stated repo is right on every form. renderActionBody is the only place action editors render, so actions inside branches get the line too.
  • Which actions show the line. recordProducing is true for record, patch-record, bsky-post, follow, calendar-rsvp, kipclip, margin, and semble, and all of them write to the owner's repo. Webhook, notification, ai, collect-links, and clear-scheduled show no line.
  • The session's flow. I wrote a throwaway happy-dom test (deleted afterwards). It loads a mocked app.bsky.graph.listitem record schema so the real RecordFormBuilder renders, then types at://did:plc:atscience/app.bsky.graph.list/3abc into the list field one character at a time. Results:
    • The repo line shows.
    • The warning names list, did:plc:atscience and @alice.test.
    • Focus stays on #field-list throughout. The warning renders after the stableRegion wrapper, the same way the existing self-trigger warning does.
    • at://alice.test/... (the owner's handle) and at://{{self}}/... do not warn.
    • A patch-record with a foreign base URI says "Updates the record" and warns on "Base Record URI".
  • Code review. I read every changed line. foreignRepoOf skips placeholder authorities, compares handles case-insensitively, and returns nothing when the owner's DID is unknown. findForeignRepoRefs uses parseTemplateJson, so placeholder-bearing templates parse. The diff adds no new controls, so there is no keyboard or focus concern. There are no migrations and no owner-only files.

Non-blocking notes, which do not need a fix round:

  • The warning text says "That is fine for a like or a reply". That is misleading for a patch-record Base Record URI: in that case the executor fetches the foreign record's CID and putRecords to the owner's repo with it as swapRecord, which normally fails with 409 rather than writing silently. The warning still points the user the right way.
  • While a URI is half typed (at://d...), the warning shows briefly, because the partial authority does not match the owner.
  • Showing the created URI in the log (M2) is issue #13, as the PR says.

loop:verdict run=review-20261030-091945-12 sha=afaa01308e718407c03bb3bc194a3784bb145b40 verdict=merge

Merged main into the branch: d3e602195a599913b2f620e1aa012fffdee640fe.

loop:carry from=afaa01308e718407c03bb3bc194a3784bb145b40 sha=d3e602195a599913b2f620e1aa012fffdee640fe

Merged onto main as 99a86dac91b1f022918f64c212f52806180dcf74.

loop:merged sha=99a86dac91b1f022918f64c212f52806180dcf74 head=d3e602195a599913b2f620e1aa012fffdee640fe

Sign up or Sign in to add to the discussion.