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
50 changes: 50 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ The same plugin can also turn a project-scoped local Relay broker into a Herdr
workspace: the fleet picker opens one attached pane per live broker agent and
projects the broker's authoritative state onto each pane.

It can also warm an existing Agent Relay Cloud agent's Daytona box and attach
its broker stream as a Herdr pane. The agent works in the box's live Relayfile
mount; no repository clone or push is involved.

This is a connector, not a notifier. If you want a push notification on your
phone when an agent blocks, several plugins do that well — see
[Related plugins](#related-plugins). Use this one when the thing that should
Expand Down Expand Up @@ -39,6 +43,14 @@ The optional `fleet` entrypoint:
- creates a `Relay fleet` workspace with one attached pane per live broker agent
- reports broker `current_state` through Herdr's `pane.report_agent` API

The optional `cloud` entrypoint:

- uses the existing non-interactive `agent-relay login` session
- warms the selected Cloud agent box in Relayfile workspace mode
- spawns or reuses one PTY agent in the box's `/workspace` live mount
- opens that remote broker terminal through the same `fleet-agent` pane and
status projector used for local agents

Never touched by the bridge entrypoint:

- pane output, scrollback, working directory, environment, or terminal titles
Expand All @@ -54,6 +66,7 @@ send prompts or keystrokes.
- Herdr 0.7.5 or newer
- Node 22 or newer
- `agent-relay` with a running project-scoped local broker for the fleet picker
- an `agent-relay login` session and an active Cloud agent for the Cloud picker

No Agent Relay account, API key, or signup is needed to start — setup creates a
free workspace for you.
Expand Down Expand Up @@ -162,6 +175,43 @@ start it instead of surfacing the raw connection-file error. The failed picker
pane stays open until you press Enter, so the recovery message does not
disappear with the process.

## Open a Cloud sandbox agent
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.

Like the fleet picker, the Cloud picker is available only on Linux and macOS;
it cannot be opened on Windows.

Sign in once, then open the Cloud picker:

```sh
agent-relay login
herdr plugin pane open --plugin agent-relay.herdr-bridge --entrypoint cloud
```

If the active Cloud workspace has more than one configured Cloud agent, choose
one by id or by coding harness:

```sh
herdr plugin pane open --plugin agent-relay.herdr-bridge --entrypoint cloud \
--env HERDR_RELAY_CLOUD_AGENT_ID=cloud-agent-id

herdr plugin pane open --plugin agent-relay.herdr-bridge --entrypoint cloud \
--env HERDR_RELAY_CLOUD_HARNESS=claude
```

The picker calls Cloud's existing async `cloud-agents/{id}/box` endpoint with a
Relayfile workspace source and `/workspace` mount, waits for the Daytona broker
to become ready, and then reuses a broker agent with the stable derived name or
spawns it once. `HERDR_RELAY_AGENT_NAME`, `HERDR_RELAY_SPAWN_TASK`,
`HERDR_RELAY_SPAWN_CHANNELS`, and `HERDR_RELAY_ATTACH_MODE` are optional
overrides. The pane is drivable by default.

The Cloud access token is retained by the Cloud SDK. The Relayfile token in the
box response is discarded because the sandbox mount daemon already owns it.
Only the sandbox broker URL and key cross into the pane process environment;
the key is never placed in process arguments or logs. Closing the Herdr pane
does not stop the box, because it may also be attached by Pear or another
client.

## Querying from Relay

Any agent in the workspace can call:
Expand Down
74 changes: 74 additions & 0 deletions dist/cloud-picker.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { pathToFileURL } from 'node:url';

import { prepareCloudPane } from './cloud.mjs';
import { attachMode, fleetProjectDir, openedPane } from './fleet.mjs';
import { requestHerdr } from './herdr-socket.mjs';
import { waitForDismiss } from './fleet-picker.mjs';

export const CLOUD_AGENT_ENTRYPOINT = 'fleet-agent';

export async function runCloudPicker({
environment = process.env,
prepare = prepareCloudPane,
request = requestHerdr,
logger = console,
} = {}) {
const socketPath = environment.HERDR_SOCKET_PATH;
if (!socketPath) throw new Error('Herdr did not provide HERDR_SOCKET_PATH');
const pluginId = environment.HERDR_PLUGIN_ID;
if (!pluginId) throw new Error('Herdr did not provide HERDR_PLUGIN_ID');

const projectDir = fleetProjectDir(environment);
const mode = attachMode(environment.HERDR_RELAY_ATTACH_MODE);
const prepared = await prepare({ environment });
const label = prepared.cloudAgent.harness || 'agent-relay';
const response = await request(socketPath, 'plugin.pane.open', {
plugin_id: pluginId,
entrypoint: CLOUD_AGENT_ENTRYPOINT,
placement: 'tab',
cwd: projectDir,
focus: true,
env: {
HERDR_RELAY_AGENT_NAME: prepared.agentName,
HERDR_RELAY_AGENT_LABEL: label,
HERDR_RELAY_ATTACH_MODE: mode,
RELAY_BROKER_URL: prepared.box.execUrl,
...(prepared.box.apiKey ? { RELAY_BROKER_API_KEY: prepared.box.apiKey } : {}),
},
});
const pane = openedPane(response);
try {
await request(socketPath, 'pane.rename', {
pane_id: pane.pane_id,
label: prepared.agentName,
});
} catch (error) {
logger.warn(`Could not label cloud pane ${pane.pane_id}: ${error.message}`);
}
logger.log(
`Opened Daytona sandbox agent "${prepared.agentName}" as Herdr pane ${pane.pane_id}; ` +
`working directory ${prepared.box.relayfileMountPath} is a live Relayfile mount.`
);
return {
paneId: pane.pane_id,
agentName: prepared.agentName,
sandboxId: prepared.box.sandboxId,
relayfileMountPath: prepared.box.relayfileMountPath,
};
}

export function isDirectEntrypoint(moduleUrl, argv1) {
return Boolean(argv1) && moduleUrl === pathToFileURL(argv1).href;
}

export async function main({ dismiss = waitForDismiss, pickerOptions } = {}) {
try {
await runCloudPicker(pickerOptions);
} catch (error) {
console.error(`Agent Relay cloud pane failed: ${error.message}`);
process.exitCode = 1;
await dismiss();
}
}

if (isDirectEntrypoint(import.meta.url, process.argv[1])) await main();
Loading
Loading