Repository navigation
Conversation
wesbillman
left a comment
There was a problem hiding this comment.
Carl, an automated reviewer, commenting via Wes’s GitHub account.
Changes requested: four P2 correctness/user-control defects are detailed inline. The three P3 notes are optional and do not gate this draft.
Reviewed head 44ecc4d9817f8ce3f77bcb31d89acaaac454c1cf against target base be00aa0ce0931f9a7e27019170e89f8528c3ea95 (merge base 71c0644a09d44d6696be842978a8c042a5f29cd6). Existing applicable CI checks are green; Windows native validation was skipped. Five additional review-only Vitest/RTL probes passed on that head with no production edits. Those probes demonstrate current behavior, not fixes; native signing/credential conclusions are source-traced.
Before merge: address the four P2 threads with regression coverage and exercise the create/edit/run/stop flow in the app. Neither this review nor the PR’s reported validation exercised a real plugin-agent Keychain/relay workflow. The documented shared-WebView trust, live-only delivery, selected-community operation, and new-agent membership limitations are accepted scope, not additional blockers.
| // Stop or delete while the key was being read must win over this send. | ||
| let pubkey = identity.pubkey.clone(); | ||
| run(owner, move |host| { | ||
| (host.controller.plugin_identity(&id)?.pubkey == pubkey) | ||
| .then_some(()) | ||
| .ok_or_else(|| "Agent identity changed".into()) | ||
| }) |
There was a problem hiding this comment.
[P2] Fence the originating run after the credential wait
A publish can enter native while its run is valid, then block on a cold Keychain read at lines 1385–1391. If the run times out, settings are saved, or the plugin/session is replaced before that read completes, this check still succeeds: plugin_identity checks the current enabled flag and pubkey, not the originating run or saved revision. Stop→Start also passes because it restores enabled without changing the revision. The stale event is then sent for the first time after revocation, potentially during a newer run, contrary to the per-run guarantee in docs/plugin-architecture.md:1007–1012.
Carry a run lease/generation and deadline across IPC and revalidate it, plus the saved revision, after credential acquisition and immediately before send. Invalidate it on the documented cancellation paths, including Stop→Start. Add a gated-credential regression asserting no HTTP send after revocation. A JS service probe reproduces the missing cancellation boundary; the native credential/send ordering was source-traced, not exercised against real Keychain.
| }) | ||
| .await?; | ||
| let (identity, key) = tauri::async_runtime::spawn_blocking(move || { | ||
| credentials.retry(); |
There was a problem hiding this comment.
[P2] Do not retry a denied credential unlock on every automated publish
credentials.retry() clears the shared bundle's Denied latch (credentials/bundle.rs:23–26). After a user refuses an unlock, the next incoming event that causes this agent to publish therefore retries the OS credential operation without another user action. This directly violates Credentials::retry's deliberate Start/Retry contract (secret.rs:204–205) and the existing denial-persistence test; automatic traffic must not repeatedly reopen a refused unlock.
Remove the unconditional retry from publication and expose recovery through an explicit user-controlled Start/Retry path. Cover denial followed by multiple automatic publishes, then an explicit retry. The latch reset is confirmed from source; repeated native dialogs were not tested.
| const invalid = agentTypeError( | ||
| current, | ||
| types, | ||
| agent.harness.environmentKeys, | ||
| ); |
There was a problem hiding this comment.
[P2] Supply the type registry at every editor entry point
This validation makes plugin-agent Save depend on agentTypes, but only the Agents-page caller supplies it. ProfileRuntime.tsx:232, ProfileAgentRuntime.tsx:101, and AgentUpdateReview.tsx:260 omit the registry; the profiles plugin does not inject it either. Opening an enabled plugin agent from its profile therefore falsely reports that its type is unavailable and rejects even a name change.
An RTL probe mounted ProfileAgentRuntime with a registered active type: opening the editor and saving a changed name produced the unavailable-type error and no save call. Passing the same registry directly to AgentEditor displayed the configuration form. Thread the registry through all supported entry points and cover those mounts. The profile runtime surface also needs to omit process-only controls such as Start on launch and Harness log for these agents.
| /** An emptied field sends nothing, so a saved value stays as it is. */ | ||
| function withSecret( | ||
| environment: AgentDraft["environment"], | ||
| name: string, | ||
| value: string, | ||
| ) { | ||
| const { [name]: _, ...rest } = environment; | ||
| return value ? { ...rest, [name]: value } : rest; |
There was a problem hiding this comment.
[P2] Provide an explicit removal action for saved optional secrets
Blank input intentionally preserves a saved value, but this form has no other path that emits the native deletion patch ({ TOKEN: null }). Unlike harness agents, plugin agents do not render AgentEnvironmentEditor's Remove control. Once an optional token is saved, the user cannot remove it from this agent through the UI while keeping the identity; typing a replacement and clearing it only sends {} and retains the old token.
Add a host-owned “Remove saved value” action that sends null, with required-secret validation still enforced. Also allow removing saved secret names a later type version no longer declares. An RTL probe confirms that clearing the input produces an empty environment patch and that no remove/clear/delete action is rendered.
| // Values typed for one runtime must not be saved on another. | ||
| const { plugin: _, ...harness } = { | ||
| ...draft, | ||
| environment: {}, | ||
| }; |
There was a problem hiding this comment.
[P3, optional] Keep the harness worker seed conditional on the selected runtime
Harness → plugin → Harness resets environment to {}, losing the documented BUZZ_ACP_AGENTS=10 default for a new harness agent. Conversely, duplicating a plugin agent takes the unconditional seed at line 91 and saves this process-only override onto an agent whose form cannot remove it. Seed the worker override only for harness drafts, both on initialization and when switching back; add cases for switching and plugin duplication. Source-traced; not a merge blocker on its own.
| Registering a type or creating an agent opens no relay subscription. The service | ||
| listens to `session.subscribeLive` on the selected community's ready session and | ||
| matches each event locally, for every enabled agent of an active type whose | ||
| community is the selected one. An agent therefore sees what its owner's connection | ||
| already streams: the channel kinds in `features/relay/live.ts` for the channels the | ||
| owner has joined, plus the global routes. Replayed history, finite reads and local | ||
| unsent intent never run it. |
There was a problem hiding this comment.
[P3, optional] Describe the window-dependent live-input limits
The new listener receives accept()'s visibility-filtered output, not every live wire event. eventVisibility requires locally retained evidence for every e target of reactions, edits and deletions, even with a valid h tag. Thus a live reaction to an older, unloaded message can be dropped while the socket is healthy, and two windows can deliver different inputs. The stream also includes previewed channels, not just joined ones. Please document these limits rather than weakening the existing access check to obtain wider delivery.
Also clarify that leaving a channel stops new delivery but does not cancel events already queued in AgentTypesService; the service receives no channel-access revocation signal. These are source-traced contract clarifications, not requests to add subscriptions or reliable catch-up in this cut.
| const signal = AbortSignal.any([ | ||
| instance.controller.signal, | ||
| AbortSignal.timeout(instance.type.timeoutMs ?? 30_000), | ||
| ended.signal, |
There was a problem hiding this comment.
[P3, optional] Validate timeout overrides before registration
register validates concurrency but accepts timeoutMs: -1 (confirmed by a focused probe). AbortSignal.timeout(-1) then throws here, before the try, so the failure bypasses the promised error counter and becomes a rejected fire-and-forget execution. Reject invalid/non-integral timeout values during registration, or construct the signal inside the handled path. This is invalid-plugin-input hardening, not a blocker for correctly configured types.
A plugin registers an agent type through ctx.agentTypes: a config page, a function from config to a NIP-01 subscription, and a run function. Each agent created with that type gets its own native bot identity and owner attestation through the existing Create agent path, and runs in the app with no process. The app matches each enabled agent's subscription against live events the session already streams and calls run with the agent identity and saved config. agent.publish signs kinds 9, 40003, 7 and 5 natively as the agent and posts them over HTTP; the key never enters the WebView. Ported from b054338 onto current main. The type exports for plugin authors in src/plugins/author.ts are left out: that file is FOUNDATION and the edit awaits approval. Signed-off-by: ART3 <822f566773c6b5ec5de21614f3900e5dd78a36d772fb826a3ee0f4b140d68255@buzz.block.builderlab.xyz>
A type declares write-only secrets, which the host asks for under Configure and saves in the agent's environment; agent.secret(name) reads one during a run. A type may also set concurrency, from 1 to 16 runs in progress per agent. The identity a run receives is now made for that run. publish and secret reject once the run has ended, and the run's signal aborts at the same moment. Before, they were built once per agent and ignored the run's deadline: a function that outlived its timeout could still publish while the next queued run was in progress. Ported from 56df2f4 without its live run view. The edit guidance for streaming text through repeated edits is removed from the docs. Signed-off-by: ART3 <822f566773c6b5ec5de21614f3900e5dd78a36d772fb826a3ee0f4b140d68255@buzz.block.builderlab.xyz>
Native now issues a lease per run (agent_identity_run_begin/end), bound to the saved revision and the run's deadline. Publish and secret reads check it before and after the credential read, and Stop, Start, save and delete end it, so a publish waiting on the Keychain when its run ends never sends. Automated publishes no longer call credentials.retry(); only a deliberate Start reopens a refused unlock. The agent type registry reaches every AgentEditor entry point (profile runtime, profile agent runtime, update review), and the profile surface omits Start on launch and Harness log for plugin agents. Saved optional secrets and secrets a type no longer declares get a Remove saved value action that sends null. timeoutMs is validated at registration, and the docs describe the window-dependent limits of live input. Signed-off-by: ART3 <822f566773c6b5ec5de21614f3900e5dd78a36d772fb826a3ee0f4b140d68255@buzz.block.builderlab.xyz>
44ecc4d to
c7c703a
Compare
A plugin agent that exists always listens: the snapshot reports it as running, Start, Stop and Restart are refused for it, and the service listens for every plugin agent of an active type regardless of the saved enabled flag. The UI no longer offers Start, Stop or Restart for plugin agents on the card, editor or profile. Silence an agent by deleting it or disabling its plugin. With no Start or Stop to race against, the native per-run lease (agent_identity_run_begin/end, run ids on publish and secret, identity revision) is removed. Delete still wins over an in-flight send through the existing existence check, and the JS run scope still rejects publishes that start after a run ends. Publish still never re-asks for a refused unlock. Signed-off-by: ART5 <a7397738d0f05003030d4487431db3096f2f48e09df21eb15f91f3a9aecf5f0c@buzz.block.builderlab.xyz>
Signed-off-by: ART5 <a7397738d0f05003030d4487431db3096f2f48e09df21eb15f91f3a9aecf5f0c@buzz.block.builderlab.xyz>
|
🤖 We exercised this API while building a Codex agent plugin. The split between host-owned identity/publishing and plugin-owned configuration/execution worked well. Two issues to fix before landing:
For follow-ups, long-running agents would benefit from priority control delivery for stop/steer which reach an active turn immediately, per-conversation queues, and structured activity reporting. Those don’t need to expand this foundational PR. |
|
🤖 Closing as stale (open >3 days, conflicts with main) per baxen. Branch kept; reopen if still wanted. |
Why
A plugin should be able to contribute a kind of agent: its settings form, what it listens for, and the function that runs. This is the first landable cut of the two prototypes (
checkpoint/plugin-agentsandart4/coding-agent), ported onto current main. Design sketch and review are in the Buzz thread in #buzz1-ittt.What
ctx.agentTypes.register({ id, title, defaults, Configure, validate, subscription, run, timeoutMs, concurrency, secrets }).plugin: { type, config }. Create, the key, the owner attestation and the profile use the existing native path. It has no process; Start and Stop setenabled.session.subscribeLive, the live events the owner's session already receives. It opens no relay subscription.agent.publishsigns kinds 9, 40003, 7 and 5 natively as the agent and posts over HTTP. The key never enters the WebView.secretsare write-only values saved in the agent's environment and read withagent.secret(name)during a run.publishandsecretreject once the run has ended, andsignalaborts at the same moment. In the prototypes they were built once per agent and ignored the run's deadline, so a function that outlived its timeout could still publish while the next queued run was in progress.Left out on purpose:
src/plugins/author.ts. That file is FOUNDATION and the edit awaits approval, so an external plugin can callctx.agentTypesbut cannot yet import its types from@buzz/author.ctx.host.fetch(Add ctx.host.fetch: a streamed host request for plugins #530).Known limits
agent.secretreturns the value into the WebView, which all enabled plugins share.Validation
Checked at
44ecc4d9.bin/pnpm typecheckpassed.src/features/agent-types,src/features/agents,src/features/relay/live.test.tsandsrc/bundled/agents(45 files, 525 tests).bin/cargo test -p buzz-agent-controllerpassed (145 tests, 2 ignored).bin/cargo test -p buzz-foundation browser_permissionspassed.Not done: I have not run the app or created an agent through the dialog on this branch. The full Vitest suite, the browser tests and
just scanwere not run locally. This is ready to try, not validated.