Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 98 additions & 22 deletions docs/agent-control.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Local agent controls

The Agents page uses one app-owned native controller for creating, importing,
editing and running local agents. Managed cards are keyed by exact identity and
community. Browser-only access keeps the read-only old library; it cannot run
editing and running local agents. Inventory cards join records by exact public key;
managed actions remain keyed by native identity/community record. Browser-only access keeps the read-only old library; it cannot run
agents. The only product entry point is ordinary desktop startup.

## Normal desktop workflow
Expand All @@ -16,16 +16,17 @@ configuration and persistent native settings. Coordinate the native rebuild/rela
quit other Foundation copies first. Saved enabled agents can restore on startup.
Keep imported agents disabled and old Buzz running until an attended handover.

Open **Agents → My agents** for imported identities, their destination community,
process evidence and visible **Start / Stop**. **Edit**, **Duplicate**, and
**Delete** are in the card’s three-dot menu. Duplicate seeds Create with editable
settings and a fresh identity; write-only environment values require re-entry.
Delete stops the local process and removes this app's settings and Keychain key
after confirmation. It does not archive the relay identity or erase messages.
Deployed remote records are refused.
Same-key identities at different destinations have separate
cards; actions use native ID/revision, never the display name. Managed controls
remain available when the old library is disconnected, unavailable or archived.
Open **Agents** for discovered and imported identities grouped by known community
associations. Each identity card keeps the configured destinations’ **Start / Stop**
controls. **Edit**, **Duplicate**, and **Delete** are in the card’s three-dot menu.
Duplicate seeds Create with editable settings and a fresh identity; write-only
environment values require re-entry. Delete stops the local process and removes
this app's settings and Keychain key after confirmation. The key remains while
another setup of the same identity still uses it. Delete does not archive the
relay identity or erase messages. Deployed remote records are refused. Actions
use native ID/revision, never the display name. Older hosts without parked
inventory retain the **My agents** and read-only library sections. Managed
controls remain available when discovery is disconnected, unavailable or archived.

**Add agent** shares the Edit fields and model browser. In the development desktop,
Create generates a native key, obtains the captured viewer's owner authorization,
Expand All @@ -50,12 +51,38 @@ worker count. With **Each thread** conversation context, separate threads can
use different workers while retaining separate histories. Existing saved agents
keep their settings; add the variable and restart them to enable more workers.

**Not imported from old Buzz** is a separate collapsible section. Expanding it
loads installed identities for the connected community; already-managed exact
identities are excluded. Each remaining row says **Not imported** and has its own
**Import** action. Source/destination overrides and source warnings stay under
Import options. Import focuses the imported card and says **Imported, not started**.
It does not start a listener, invite an agent or change the old library.
**Clone to this community** opens the existing creation dialog with only the old
agent’s name and resolved instructions. Review that text for embedded secrets.
Runtime settings and workspace use this app’s defaults and remain editable.
Clone generates a new native identity; it does not copy identity keys, environment
values, command arguments, paths, history or membership. Saving leaves the new
agent stopped. The source is read-only and no legacy credential access occurs.

**Import** preserves the selected old-installation identity and private key. It
requires an explicit destination and a fresh source/destination-bound preview.
Successful import saves a configured, stopped setup. It does not start a listener,
invite an agent, or modify the source installation. An identity already held
locally cannot be imported again into another community; use **Clone** instead.
The native prepare and commit boundaries both enforce that exact-key rule.

The unified inventory offers Import only under **Available to import**, once per
exact public key. Multiple old installations require an explicit source choice.
The selected row opens the import review. Without a selected community, such
as in Personal space, the review asks for the destination before it loads a
fresh preview. Each startup reads every old installation again: an identity
deleted from all of them, or whose installation is removed, leaves the list. A
damaged source keeps its previous entries. Source read failures remain visible.
Older hosts retain the separate installation browser as a compatibility path.

**Use here** is recovery for older incomplete local imports, not a normal next
step after Import. It retains the identity/key, requires owner-authorized community
confirmation, and leaves the recovered setup stopped with app-launch start off.
The development broker and the packaged desktop app both provide that
confirmation; the desktop app signs it natively (`relay_agent_resolve`). Native
code refuses a new
community when that identity already has a configured setup elsewhere. A retry
for the already recovered destination is harmless. Existing historical setups
remain visible and controllable; this rule does not move or delete them.

Local team-linked imports snapshot the deployment team's instructions from the
chosen library's `agents/teams.json`, alongside the resolved persona prompt.
Expand All @@ -74,7 +101,7 @@ perform the same attended old-Buzz handover as for a fresh import.

To use an agent, open a channel and select it from **@ mentions**. Both mention
menus include the selected community’s people directory alongside channel members
and managed agents. Directory reads are bounded; narrow the search for more people.
and configured managed agents. Directory reads are bounded; narrow the search for more people.
A nonmember is labeled **Not in channel · Choose whether to add when you send**.
Selection alone does nothing. Send asks, as block/buzz desktop does: **Invite**
or **Do nothing**. Without add permission, the only action is **Send anyway**.
Expand All @@ -89,7 +116,7 @@ before a new add; unknown outcomes are never silently replaced. Channel, thread,
and forum-channel composers share this behavior. DM participants and session
admission rules are unchanged.

A confirmed outgoing channel or thread mention now starts an exact imported local
Once an identity is configured by Import or legacy **Use here** recovery, a confirmed outgoing channel or thread mention starts that exact local
agent (public key + community), without a separate Start click. Import itself
remains non-starting. Stop cancels earlier pending mention wakes and active work;
a later deliberate mention can start the agent again. Plain name text without
Expand Down Expand Up @@ -554,8 +581,10 @@ Settings says **Shell setup not verified**; Buzz does not check it before Start.
Snapshots name the deciding key (`launchModelEnv`/`launchProviderEnv`,
including `DATABRICKS_MODEL` or a hidden provider behind a blank buzz-agent
model) and omit the resolved value.
- Import previews only the chosen installed/development library and requires an
explicit secure **Destination community** origin. Old Buzz ignores saved relay
- Local browsing reads only the chosen installed/development library without a
destination. Native keeps no pending import for that read and invalidates any
prior import token. An actionable import preview requires an explicit secure
**Destination community** origin. Old Buzz ignores saved relay
pins at runtime; blank, stale or malformed saved pins do not route or hide
identities here. Native validates the chosen destination, shows it beside each
exact key, and retains it with the preview token through commit. Source or
Expand Down Expand Up @@ -818,3 +847,50 @@ configuration. The current internal release repository builds the old desktop;
its generic build environment injection is not a Pi resource-bundling contract
for this app. Signed bundling, automatic employee provisioning and release
pipeline migration require separate release work; no release is published here.

### Community setup confirmation

Local installation import validates the source configuration, owner authorization
and private key. It does not require relay inventory or a community confirmation.
The imported agent stays stopped.

For explicit setup in a community, the broker can sign the selected owner's
intent for an identity/community pair. Native code verifies that signature against
the retained source-owner authorization. This does not establish channel membership,
key availability or exclusive community membership. It does not reserve a community
before import. Setup recovery cannot add another community to an identity that
already has a configured setup elsewhere; copying that agent requires Clone.

Joined-community discovery reads each joined community's scoped inventory without
selecting it or opening a relay session. Exact keys appear once with all known
associations. Each failed community read has its own warning and Refresh retry;
successful reads remain visible. Discovery does not provide credentials, an import
source, or permission to extend an existing local identity into another community.

### Local inventory actions

Startup copies only identity names, public keys and source labels into a durable
inventory. It does not read keys, configure a setup, or start an imported agent.
Import copies the selected local key and settings, independent of relay inventory.
New imports require a destination and are saved configured but stopped.
**Use here** only recovers older incomplete imports. **Start** remains a separate
action; a later deliberate mention can also start a configured agent. Existing
saved setups without the configured flag keep their prior behavior.

The unified card's **Import** opens the existing installation form with its exact
identity and known local source selected. The source is fixed during review;
if the preview fails or the identity is unavailable there, you can choose
another source. **Clone**
from a local source or imported identity opens a review of only its name and
instructions; creation generates a fresh key. Clone never imports the old key.

Community groups show known associations, not exclusive membership or admission.
An inventory failure does not block local Import or setup confirmation. Every
configured setup keeps Start, Stop and Edit, whichever community is selected.
Cards for other communities also offer Clone to bring a new identity here,
without changing the source.
Archived discovery rows remain hidden after sources join, except where local
controls must remain reachable. Linked profiles remain visible on identity cards.

Before starting an imported identity, stop the old agent and disable its automatic
startup in the old application. Do not run duplicate copies of the same identity.
36 changes: 26 additions & 10 deletions docs/agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,14 @@ add-existing membership, Save/recovery and all runner management are out of V1.
- Only definition ID/name, identity public key/name/definition link, and optional
avatar artwork leave the host. Prompts, configuration, credentials and execution receipts are not
projected. This is local library evidence, **not verified ownership**.
- Selected definitions remain one card each, including definitions without an
identity. Exact linked keys remain available in each card’s identity disclosure, including namesakes; unlinked and
unmatched identities use Custom agents/Other identities groupings. Unlike old Buzz's
runtime-dependent representative selection, this read-only view shows all
non-archived linked keys and has no profile/start action or running badge.
- Confirmed relay archives hide identity rows, not definition cards. Missing
archive evidence is labeled; it does not erase the saved library. This is
display behavior, never mention permission.
- The library shows one tile per exact identity, grouped only by explicit profile
links. Each tile discloses its full public key. Profiles with no linked identity
appear separately; an archived identity does not become an empty profile.
- Only distinct keys with the same displayed name need a short npub suffix. Names
alone never create a profile group. Suffix collisions extend deterministically using
the complete inventory, including identities hidden by archive filtering.
- Missing archive evidence keeps identities visible. Archive filtering affects
display only, never mention permission or runtime control.
- One lazy host read per opening/Refresh; no polling or relay-directory startup
scan. Concurrent host requests coalesce. Read caps: 8 MiB / 2000 records;
malformed/missing files fail visibly without echoing their contents. The host
Expand Down Expand Up @@ -520,5 +520,21 @@ service. Each relay session binds its own view. A ready native record takes
precedence only in its matching community; otherwise the ready legacy display
inventory supplies the name, then the public profile. Plugin disable restores
public-profile names. These labels never change identity keys, membership,
credentials, or runtime admission. Profile panels consume this view; other name
surfaces are being migrated separately.
credentials, or runtime admission. Profile panels, messages, mention choices,
activity, conversation labels and new notifications consume this view. Mention
parsing still uses signed identity evidence before resolving its visible label.

### Additive community inventory

The active session reads the owner's kind-30175 profiles and kind-30177 identities
from its accessible relay. It also retains the local library reader. The inventory
joins exact public keys, not equal names; explicit profile references use the
publisher's slug mapping only when local definitions do not collide. Local names
and artwork win for matching keys. Native configuration still wins within its
matching community. A failed source leaves the other source visible with a warning.

Discovery is not global coverage, verified membership, credentials, or execution
status. Native cards keep their controls. Other known identities appear in a
read-only section. Each card offers its own Import; the separate installation
browser appears only for repair.
No keys, config, memory, membership, or runtime state are changed by discovery.
16 changes: 14 additions & 2 deletions src/app/agent-control.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,21 @@ it("retains working injected control across Agents disable/re-enable and communi
.find((entry) => entry.pluginId === "buzz.agents");
// The production registration wrapper passes its injected capability as props.
const component = page?.component as unknown as () => {
props: { control: AgentControl };
props: {
control: AgentControl;
communities: Pick<
typeof services.communities,
"snapshot" | "subscribe"
>;
};
};
return component().props.control;
const props = component().props;
expect(Object.keys(props.communities).sort()).toEqual([
"snapshot",
"subscribe",
]);
expect(props.communities.snapshot()).toBe(services.communities.snapshot());
return props.control;
};
expect(injectedControl()).toBe(control);
await control.refresh();
Expand Down
15 changes: 6 additions & 9 deletions src/bundled/agents/AgentsPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -561,13 +561,14 @@ it("focuses the imported managed identity without starting it", async () => {
fireEvent.click(
await screen.findByRole("button", { name: "Import Fixture agent" }),
);
const notice = await screen.findByText(
"Imported, not started. Start it when you are ready.",
);
const notice = await screen.findByText(/Imported, not started\./);
const imported = notice.closest("article");
if (!imported) throw Error("Imported card missing");
expect(imported).toHaveTextContent("wss://third.example");
expect(notice.parentElement).toHaveFocus();
expect(
within(imported).queryByRole("button", { name: "Use here" }),
).toBeNull();
expect(within(imported).getByRole("button", { name: "Start" })).toBeEnabled();
expect(f.calls.some((call) => call.action === "start")).toBe(false);
});
Expand Down Expand Up @@ -1241,9 +1242,7 @@ it("credential import keeps real Stop controls reachable without trapping the ed
await gate;
});
await waitFor(() => expect(control.snapshot().busy).toBe(false));
expect(
screen.queryByText("Imported, not started. Start it when you are ready."),
).toBeNull();
expect(screen.queryByText(/Imported, not started\./)).toBeNull();
await act(async () => control.refresh());
const imported = control
.snapshot()
Expand Down Expand Up @@ -2680,9 +2679,7 @@ it("uses snapshot capabilities rather than JS wrappers and retains older-host im
fireEvent.click(
await screen.findByRole("button", { name: "Import Fixture agent" }),
);
const notice = await screen.findByText(
"Imported, not started. Start it when you are ready.",
);
const notice = await screen.findByText(/Imported, not started\./);
const card = notice.closest("article");
if (!card) throw Error("Imported card missing");
expect(within(card).getByRole("button", { name: "Start" })).toBeEnabled();
Expand Down
6 changes: 0 additions & 6 deletions src/bundled/agents/InventoryIdentityCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -187,19 +187,13 @@ export function InventoryIdentityCard({
state.busy ||
state.status !== "ready" ||
!!decision.blocked ||
!data.localInventoryActions ||
!control.configureHere
}
onClick={() => onUseHere(row.pubkey, "use")}
>
Use here
</Button>
{decision.blocked && <p role="status">{decision.blocked}</p>}
{!data.localInventoryActions && (
<p>
Restart an updated desktop build to use local inventory actions.
</p>
)}
</>
)}
{(tile || decision.action === "clone") && cloneAction}
Expand Down
6 changes: 2 additions & 4 deletions src/bundled/agents/ManagedAgentActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ export function ManagedAgentActions({
>
Use here
</Button>
) : state.data?.localInventoryActions && control.configureHere ? (
) : control.configureHere ? (
<LocalInventoryAction
control={control}
agent={agent}
Expand All @@ -121,9 +121,7 @@ export function ManagedAgentActions({
onUsed={() => {}}
onClone={() => {}}
/>
) : (
<p>Update the desktop app to set up this imported identity.</p>
))}
) : null)}
{agent.startOnAppLaunch && (
<p className="m-0 text-body-sm text-secondary">Starts with this app.</p>
)}
Expand Down
Loading
Loading