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
106 changes: 106 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,112 @@ result = (
)
```

## Watching a Run

When a run starts, the runner mints and prints a link you can open to follow it
live:

```text
[workflow 00:02] Workspace created for this workflow.
[workflow 00:02] Observer: https://agentrelay.com/observer?key=ot_live_...
[workflow 00:02] Channel: wf-ship-feature-a1b2c3
```

Open the `Observer:` URL and you see messages, agent activity, handoffs, and
deliveries in real time. The link carries a **scoped observer token**
(`ot_live_`): read-only, expiring in 24 hours, and individually revocable.

Minting is best-effort, so a link is not guaranteed — see
[If no link appears](#if-no-link-appears). A failed mint never fails the run.

Treat the link itself as a shared secret. The token is a bearer credential in a
query parameter, so anyone who gets the URL can read the stream it covers until
it expires or you revoke it. It is far safer than a workspace key — it cannot
send, spawn, or administer — but it is not public.

### The two ways a run gets a workspace

Which link you get depends on where the run's Relaycast workspace comes from.

**No `RELAY_API_KEY` set** — the runner creates a throwaway workspace for this
run alone and mints an observer link covering all of it, DMs included. The
workspace is anonymous and disappears from your reach when the run ends, so
**copy the link while the run is going**. Nothing persists the underlying key,
and there is no way to recover it afterward.

**`RELAY_API_KEY` set** — the runner uses your workspace and mints a link scoped
to just this run's channel, with agent DMs excluded, so the link does not expose
unrelated traffic in a shared workspace. This is the better setup for anything
you may want to revisit: the run is in a workspace you own, so you can mint
fresh links whenever you like.

```bash
# One-time: create a workspace you own and keep the key
curl -sX POST https://api.relaycast.dev/v1/workspaces \
-H 'content-type: application/json' -d '{"name":"my-workflows"}' \
| jq -r '.data.api_key // .api_key'

export RELAY_API_KEY=rk_live_... # put this in your shell profile
relayflows run workflow.yaml
```

If you already use the Agent Relay CLI, `agent-relay workspace key --reveal-secrets`
prints the key of your active workspace. Note it is **masked** without
`--reveal-secrets`.

### Minting more links yourself

With a workspace you own, `agent-relay observer` mints links on demand:

```bash
agent-relay observer # read-only link, 24h, DMs excluded
agent-relay observer --channels wf-ship-a1b2c3 # scope to one run
agent-relay observer --include-dms # include agent DMs
agent-relay observer --expires 7d # longer-lived link
agent-relay observer list # what is outstanding
agent-relay observer revoke <id> # cut one off immediately
```

### Never share the workspace key

A workspace key (`rk_live_`) is an **administrative** credential — it can send
messages, spawn and remove agents, and change workspace settings. Do not put one
in an observer URL, a chat message, or a terminal transcript; query strings end
up in browser history, referrer headers, and proxy logs. The realtime endpoint
rejects it outright, so a link built from one cannot stream anyway.

| | Workspace key (`rk_live_`) | Observer token (`ot_live_`) |
| --- | --- | --- |
| Read messages and activity | yes | yes |
| Send, spawn agents, administer | yes | **no** |
| Expires | no | yes |
| Revocable individually | no | yes |
| Scopable to channels | no | yes |

The runner only ever prints `ot_live_` links, and scrubs `rk_live_` values out of
channel output.

### Configuration

| Variable | Purpose |
| --- | --- |
| `RELAY_API_KEY` | Workspace key to run against. Unset means a throwaway workspace per run. |
| `RELAY_OBSERVER_URL` | Observer dashboard base. Defaults to `https://agentrelay.com/observer`. |
| `RELAY_OBSERVER_EXPIRES` | Link lifetime as `30m` / `24h` / `7d`. Defaults to `24h`; unparseable or over `90d` falls back to `24h`. |
| `RELAYCAST_BASE_URL` | Relaycast engine base. Defaults to `https://api.relaycast.dev`. |

### If no link appears

- **`Observation: unavailable`** — the token could not be minted (engine
unreachable, or it rejected the request). The run is unaffected; minting is
best-effort by design and never fails a run.
- **No observer lines at all** — the run needed no broker: every step was
`deterministic`, `worktree`, `integration`, or `waitFor`, or an external
executor handled agent spawning, or Relaycast was disabled with
`AGENT_RELAY_WORKFLOW_DISABLE_RELAYCAST=1`.
- **`Observation: run agent-relay observer`** — you set `RELAY_API_KEY` and
minting failed. Mint a link by hand with `agent-relay observer`.

## Consumer-Facing Apps + AI SDK Communicate Flows

A good production split is:
Expand Down
133 changes: 133 additions & 0 deletions docs/observer.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
---
title: 'Watch a run'
description: 'Follow a running workflow live at agentrelay.com with a read-only observer link, and understand where that link comes from.'
---

A workflow run that uses a broker mints a link you can open to follow it in real
time — messages, agent activity, handoffs, and deliveries as they happen.
Minting is best-effort: it can fail without failing the run, and a run that
needs no broker prints no link at all. See [If no link appears](#if-no-link-appears).

```text
[workflow 00:02] Workspace created for this workflow.
[workflow 00:02] Observer: https://agentrelay.com/observer?key=ot_live_...
[workflow 00:02] Channel: wf-ship-feature-a1b2c3
```

Open the `Observer:` URL, or hand it to a teammate. It carries a scoped
**observer token** (`ot_live_`): read-only, expiring in 24 hours by default, and
individually revocable.

Treat the link as a shared secret. The token is a bearer credential in a query
parameter, so anyone holding the URL can read the stream it covers until it
expires or is revoked. That is a far smaller blast radius than a workspace key —
it cannot send, spawn, or administer — but it is not public. Revoke one with
`agent-relay observer revoke <id>`.

## Where the link comes from

The runner needs a Relaycast workspace to coordinate agents. Where that
workspace comes from decides what your link covers.

### Without `RELAY_API_KEY`

The runner creates a throwaway workspace for this run alone, then mints a link
covering all of it, agent DMs included.

That workspace is anonymous — it is not attached to your account, and nothing
persists its key. **Copy the link while the run is going.** Once the run ends
there is no way to recover access.

### With `RELAY_API_KEY`

The runner uses your workspace and mints a link scoped to just this run's
channel, with agent DMs excluded, so the link does not expose unrelated traffic
in a workspace you share with others.

This is the better setup for anything you may want to revisit — the run lives in
a workspace you own, so you can mint fresh links at any time.

```bash
# One-time: create a workspace and keep its key
curl -sX POST https://api.relaycast.dev/v1/workspaces \
-H 'content-type: application/json' -d '{"name":"my-workflows"}' \
| jq -r '.data.api_key // .api_key'

export RELAY_API_KEY=rk_live_...
relayflows run workflow.yaml
```

If you already use the Agent Relay CLI, `agent-relay workspace key --reveal-secrets`
prints your active workspace key. Without `--reveal-secrets` it is masked.

## Minting links yourself

With a workspace you own:

```bash
agent-relay observer # read-only link, 24h, DMs excluded
agent-relay observer --channels wf-ship-a1b2c3 # scope to one run's channel
agent-relay observer --include-dms # include agent DMs
agent-relay observer --expires 7d # longer-lived link
agent-relay observer list # what is outstanding
agent-relay observer revoke <id> # cut one off immediately
```

## Never share a workspace key

A workspace key (`rk_live_`) is an **administrative** credential: it can send
messages, spawn and remove agents, and change workspace settings. Do not put one
in an observer URL, a chat message, or a terminal transcript — query strings end
up in browser history, referrer headers, and proxy logs.

| | Workspace key (`rk_live_`) | Observer token (`ot_live_`) |
| --- | --- | --- |
| Read messages and activity | yes | yes |
| Send, spawn agents, administer | yes | **no** |
| Expires | no | yes |
| Revocable individually | no | yes |
| Scopable to channels | no | yes |

The realtime endpoint enforces this: it rejects a workspace key and accepts only
an observer token carrying `stream:read`. The runner prints `ot_live_` links
only, and scrubs `rk_live_` values out of channel output.

## Configuration

| Variable | Purpose |
| --- | --- |
| `RELAY_API_KEY` | Workspace key to run against. Unset means a throwaway workspace per run. |
| `RELAY_OBSERVER_URL` | Observer dashboard base. Defaults to `https://agentrelay.com/observer`. |
| `RELAY_OBSERVER_EXPIRES` | Link lifetime as `30m` / `24h` / `7d`. Defaults to `24h`; unparseable or over `90d` falls back to `24h`. |
| `RELAYCAST_BASE_URL` | Relaycast engine base. Defaults to `https://api.relaycast.dev`. |

## If no link appears

<AccordionGroup>
<Accordion title="Observation: unavailable">
The token could not be minted — the engine was unreachable or rejected the
request. The run itself is unaffected: minting is best-effort by design and
never fails a run.
</Accordion>
<Accordion title="No observer lines at all">
The run needed no broker: every step was `deterministic`, `worktree`,
`integration`, or `waitFor`, or an external executor handled agent
spawning, or Relaycast was disabled with
`AGENT_RELAY_WORKFLOW_DISABLE_RELAYCAST=1`.
</Accordion>
<Accordion title="Observation: run `agent-relay observer`">
You set `RELAY_API_KEY` and minting failed. Mint a link by hand with that
command.
</Accordion>
</AccordionGroup>

## See also

<CardGroup cols={2}>
<Card title="Run workflows" href="/docs/cli-run-workflows">
Execute local workflow files and resume failed runs.
</Card>
<Card title="Observer" href="/docs/observer">
The observer dashboard itself.
</Card>
</CardGroup>
80 changes: 76 additions & 4 deletions packages/core/src/__tests__/channel-messenger.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
ChannelMessenger,
formatError,
formatObserverGuidance,
isObserverGuidanceLine,
formatStepOutput,
scrubSecrets,
sendToChannel,
Expand Down Expand Up @@ -69,15 +70,86 @@ describe('channel messenger helpers', () => {
expect(scrubSecrets(text)).toBe(text);
});

it('omits credential-bearing observer links from auto-created workspace guidance', () => {
const guidance = formatObserverGuidance('workflow-room');
it('prints the minted observer link for an auto-created workspace', () => {
const guidance = formatObserverGuidance('workflow-room', {
workspaceCreated: true,
observerUrl: 'https://agentrelay.com/observer?key=ot_live_abc123',
});

expect(guidance).toEqual([
'Workspace created for this workflow.',
' Observer: https://agentrelay.com/observer?key=ot_live_abc123',
' Channel: workflow-room',
]);
});

it('never puts a workspace key in the guidance when minting failed', () => {
const guidance = formatObserverGuidance('workflow-room', { workspaceCreated: true });

expect(guidance).toEqual([
'Workspace created for this workflow.',
' Observation: requires a separately provisioned, read-only observer token',
' Observation: unavailable — could not mint a read-only observer token ' +
'(set RELAY_API_KEY to run against a workspace you own)',
' Channel: workflow-room',
]);
expect(guidance.join('\n')).not.toMatch(/rk_live_|observer\?key=/);
});

it.each([
[{ workspaceCreated: true, observerUrl: 'https://agentrelay.com/observer?key=ot_live_a' }],
[{ workspaceCreated: true, observerUrl: 'http://localhost:4000/observer?key=ot_live_a' }],
[{ workspaceCreated: true }],
[{}],
])('survives terminal output filtering for %j', (options) => {
// Every guidance line must clear the filter — a link the user cannot see is
// the bug this whole path exists to fix.
for (const line of formatObserverGuidance('wf-demo-ab12', options)) {
expect(isObserverGuidanceLine(`[workflow 00:03] ${line}`)).toBe(true);
}
});

it('survives filtering for a channel name without the generated wf- prefix', () => {
// `swarm.channel` may be set to anything; its guidance must not be dropped.
for (const line of formatObserverGuidance('team-room', { workspaceCreated: true })) {
expect(isObserverGuidanceLine(`[workflow 00:03] ${line}`)).toBe(true);
}
});

it.each([
['a workspace key', 'https://agentrelay.com/observer?key=rk_live_secret'],
['an agent token', 'https://agentrelay.com/observer?key=at_live_secret'],
['no key at all', 'https://agentrelay.com/observer'],
['a malformed URL', 'not a url'],
])('refuses to print an observer link carrying %s', (_label, url) => {
const guidance = formatObserverGuidance('wf-demo', {
workspaceCreated: true,
observerUrl: url,
});

expect(guidance.join('\n')).not.toContain('Observer:');
expect(guidance.join('\n')).not.toContain('secret');
expect(guidance).toContain(
' Observation: unavailable — could not mint a read-only observer token ' +
'(set RELAY_API_KEY to run against a workspace you own)'
);
});

it('does not whitelist ordinary workflow chatter', () => {
expect(isObserverGuidanceLine('[workflow 00:03] Resolving Relaycast API key...')).toBe(false);
expect(isObserverGuidanceLine('[broker] worker started')).toBe(false);
expect(isObserverGuidanceLine('[workflow 00:03] Creating channel: wf-demo...')).toBe(false);
// A bare `Channel: ` elsewhere in a log line must not buy an exemption.
expect(isObserverGuidanceLine('[broker] joined Channel: wf-demo')).toBe(false);
});

it('points a bring-your-own-key run at the observer command when minting failed', () => {
const guidance = formatObserverGuidance('workflow-room');

expect(guidance).toEqual([
' Observation: run `agent-relay observer` to mint a read-only link',
' Channel: workflow-room',
]);
expect(guidance.join('\n')).not.toMatch(/observer\?key=|\[REDACTED\]/);
expect(guidance.join('\n')).not.toContain('Workspace created');
});

it('formatError normalizes unknown errors', () => {
Expand Down
Loading
Loading