Skip to content

feat(remote agents) hosted agent types and the owner's agent list - #870

Open
baxen wants to merge 1 commit into
baxen/agent-no-time-limitfrom
baxen/remote-agents
Open

baxen wants to merge 1 commit into
baxen/agent-no-time-limitfrom
baxen/remote-agents

Conversation

@baxen

@baxen baxen commented Oct 11, 2026 •

Copy link
Copy Markdown
Collaborator

Stacked on #874, the top of the Agents stack.

Agents could only make agents whose key lives on this device, and the agents page showed only those. This adds hosted agent types, whose provider holds the key and runs the agent, and a generic list of every agent the owner has, so each device can show agents that run elsewhere.

Hosted types

A type with remote steps is hosted. It has no run, models or timeoutMs, and its agents have no attention.

remote: {
  provision({ name, community, signal }) → { pubkey }  // provider makes the key
  activate({ pubkey, name, community, auth, signal })  // gets the owner's NIP-OA tag, publishes the profile, starts it
  profile({ pubkey, community, name, picture?, signal })
  remove({ pubkey, community, signal })                // idempotent
}
  • Create: the app runs provision, signs NIP-OA through prepareRemoteAgentAuthorization, runs activate, then lists the agent.
  • Failed setup: if anything after provision fails, the agent is kept as Setup unfinished, across restarts too. Finish setup continues it without making a second agent.
  • Rename and picture: these go through profile, then the listing is updated. The picture must be an HTTPS URL.
  • Settings: a hosted agent's settings are the type's own tabs calling its provider; save(config) rejects for it.
  • Delete: the provider removes the agent first, then it is archived, leaves its channels and its record is deleted. If the provider fails, the agent stays listed so Delete can be tried again.
  • Concurrency: setup and delete of one agent never run at the same time.

The owner's agent list (kind 30177)

  • Format: every agent, here or hosted, is listed in an owner-signed 30177 record with ["runtime", "<pluginId>/<typeId>"].
  • Compatibility: other desktops apply a 30177 whole, and their parser requires parallelism and respond_to (docs/relay-queries.md). So a record keeps every setting and tag of the record it replaces, and defaults those two to 1 / owner-only.
  • How each device shows it:
    • The key is on this device: here.
    • The runtime is a hosted type: hosted.
    • Anything else: On another device, read-only, never run here, and no Delete.
    • Records without a runtime belong to earlier systems and stay hidden.
  • Reads: records are read once per community connect, and a failed read is retried on the next sync. Listings written while a read is in flight survive it.

Teams stay local-only, so hosted agents can't join one yet.

Trying it

The example ack-bot plugin and its localhost provider live in a separate draft PR stacked on this one (#875), kept for reference and not for merging.

Testing

  • tsc and biome are clean.
  • New service tests cover: listing, the elsewhere view, hosted create, unfinished/finish/restart, rename, delete order and failure, unfinished delete, setup vs. delete, keeping a prior record, read/write race and retry, and HTTPS pictures.
  • New tests also cover publishAgentRecord and the page.
  • The full vitest suite passes on top of feat(agents2): set no deadline on an agent's work #874 (8263 tests).
  • baxen ran the hosted flow end to end in the desktop app with the ack-bot example: create, then an owner mention answered in its thread.

Not here yet

  • Builderlab on this contract.
  • The claude-code → claude-local rename, which changes the published runtime too; the next sync relists agents under the new key.
  • Live 30177 updates from other devices; for now they show on the next connect.

🤖 Generated with Claude Code

A type with `remote` steps (provision, activate, profile, remove) is
hosted: its provider holds the key and runs each agent. The app makes one
by asking the provider for its key, signing the owner's NIP-OA attestation,
handing that over, then listing it. A setup that fails partway is kept as
unfinished, across restarts, and Finish setup continues it without making
another. Rename and picture go through the provider; a hosted agent's
settings are its type's tabs calling its provider.

Every agent is listed in the owner's kind 30177 records with its type as a
`runtime` tag, so each of the owner's devices shows it here, hosted, or on
another device. A record keeps the settings and tags of the one it
replaces and defaults parallelism and respond_to, as other desktops parse
it whole. Records without a runtime are earlier systems' and stay hidden.
Agents on another device are read-only and never run here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Bradley Axen <baxen@squareup.com>
@baxen
baxen force-pushed the baxen/remote-agents branch from 3628fee to 4ba12f7 Compare October 12, 2026 01:18
@baxen baxen changed the title Agents2: hosted agent types and the owner's agent list Agents: hosted agent types and the owner's agent list Oct 12, 2026
@baxen
baxen changed the base branch from baxen/channel-templates to baxen/agent-no-time-limit October 12, 2026 01:18
@baxen baxen changed the title Agents: hosted agent types and the owner's agent list feat(remote agents) hosted agent types and the owner's agent list Oct 12, 2026
@baxen
baxen marked this pull request as ready for review October 12, 2026 04:49
@baxen
baxen requested a review from a team as a code owner October 12, 2026 04:49
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 12, 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-12T04:53:36.682777Z 4ba12f7 Draft marked ready
ℹ️ 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: 4ba12f7d5f

ℹ️ 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".

Comment on lines +1069 to +1071
originOf(identity.relay) !== origin ||
this.listing.has(identity.pubkey)
)

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 Reconcile after an in-flight listing write

When a local agent is renamed or repictured while its previous 30177 publication is pending, the refresh reaches this guard and skips the newer state; after the old publication settles, the finally handler only clears this.listing and never calls list again. Until another refresh or reconnect happens, the relay record—and therefore other devices—continues showing the stale name or picture. Re-run reconciliation after clearing the in-flight marker.

AGENTS.md reference: AGENTS.md:L126-L129

Useful? React with 👍 / 👎.

Comment on lines +894 to +895
private async saveHosted(pubkey: string, change: AgentChange) {
const { agent, remote, origin, binding } = this.hosted(pubkey);

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 Serialize hosted profile saves with deletion

When a user edits the name and immediately clicks Delete, the input blur can start saveHosted while removeHosted runs because profile saves do not use the per-agent exclusion. If the profile call or subsequent 30177 publication completes after removeRelayAgent, writeListing recreates the just-deleted record and leaves a ghost hosted agent. Serialize profile saves through the same per-pubkey lifecycle guard as setup and deletion.

AGENTS.md reference: AGENTS.md:L126-L129

Useful? React with 👍 / 👎.

Comment on lines +740 to +743
if (
pubkey === binding.viewer ||
this.identities.some((identity) => identity.pubkey === pubkey)
)

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 Reject keys already present in the owner inventory

If a provider returns a pubkey already represented by a hosted/elsewhere 30177 record or an unfinished setup, this check accepts it because it examines only native identities. Setup then activates that key and overwrites the existing record's name and runtime, effectively replacing an existing agent instead of creating one. Validate the returned key against loaded and in-flight owner listings plus unfinished setups before persisting or activating it.

AGENTS.md reference: AGENTS.md:L126-L129

Useful? React with 👍 / 👎.

@wesbillman wesbillman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Four additional changes needed: preserve listings across read failures, order replacement records, exclude hosted types from native import, and retain focus after setup. Details and smallest fixes are inline. The existing in-flight listing and profile/delete race reports also remain applicable.

Star Lord automated source review via Wes’s account. Head 4ba12f7d5f2b2c34e9516e11fa440c789e2637ec; base 7e0ffeb6730396dfc357547eb3c8f5526fc3146f. Source-only: no tests, app/provider execution, or CI validation. Inspected changed source, commit metadata and PR description; no attached images in the description.

Comment on lines +1117 to +1119
this.edits.get(origin)?.set(pubkey, row);
const listings = this.listings.get(origin);
if (!listings) return;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P2] Keep accepted writes when the initial listing read fails

After readListings fails, it removes edits without installing listings. A hosted create/Finish setup can still succeed at the provider and relay, but setRow then stores nothing; setUp deletes the unfinished entry and throws because find returns nothing. A write completed during a read is also lost if that read subsequently rejects and discards its edits. The running agent disappears from the local inventory until another successful sync. Preserve locally accepted rows independently of read success, and merge them into the next successful read; cover both failure orderings.

Comment on lines +103 to +104
const id = outbox.send(
{ kind: 30177, content: JSON.stringify(content), tags },

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P2] Make replacement agent records strictly newer

This send has no ordering information from the record it replaces. The outbox uses whole-second wall-clock timestamps for kind 30177; its monotonic replacement handling covers only 30175/30178 (catalog-envelope.ts). Two quick name/picture updates can therefore share a timestamp, and newer selects the lower event ID rather than the last edit. writeListing nevertheless caches the last write as current, so reconnect/another device can restore the older value without reconciliation correcting it. Retain the observed record timestamp and ensure each 30177 replacement advances beyond both observed and local writes; add same-second replacement coverage.

Comment on lines +826 to 828
const kind = this.types().find((entry) => entry.key === type);
if (!kind) throw new Error("That agent type is not available");
const settings = defaults("", kind);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P2] Exclude hosted types from the native import path

BringOver still receives every registered type and offers each in its As selector. Selecting a new hosted type reaches this shared add path via port, which accepts it and imports the legacy key natively without calling the provider. The result is shown as here, but hosted types cannot have run, so the imported agent never answers; another device misclassifies its published runtime as provider-hosted even though the provider never received it. Filter hosted types out of Bring over and reject them at the native port/add boundary before saving the identity.

Comment on lines +452 to +455
<AgentDelete agents={agents} agent={agent} onRemoved={onClose} />
</>
) : (
<Button variant="primary" onClick={onBuild}>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P2] Preserve keyboard focus when Finish setup succeeds

When agents.finish updates this agent to finished, this conditional replaces the fragment containing the focused Finish setup button with a different Open button. AgentPeek has no focus handoff, so keyboard activation loses its focused control on success instead of allowing the user to continue into the agent. Keep the action mounted across this transition, or explicitly transfer focus to Open only if it was still on Finish setup. This follows DESIGN.md’s States, feedback, and motion rule; the new page test mocks finish without changing the snapshot, so it never exercises the transition.

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