Skip to content

feat(cli): add agent-relay observer to mint read-only follow-along links - #1422

Open
willwashburn wants to merge 2 commits into
mainfrom
claude/agent-relay-skills-review-a8mgtd-observer-cli
Open

feat(cli): add agent-relay observer to mint read-only follow-along links#1422
willwashburn wants to merge 2 commits into
mainfrom
claude/agent-relay-skills-review-a8mgtd-observer-cli

Conversation

@willwashburn

@willwashburn willwashburn commented Aug 3, 2026

Copy link
Copy Markdown
Member

Summary

Letting a human watch a workspace live had no supported path, so the guidance we ship told agents not to offer one.

The engine rejects a workspace key on the realtime endpoint (GET /v1/ws) and accepts only a scoped ot_live_ observer token. But nothing in the CLI or MCP server minted one — grepping packages/cli/src for "observer" returned exactly one hit, cloud-room.ts:197, which rejects observer tokens. The only real routes were a hand-rolled POST /v1/observer-tokens or pasting an admin key into the dashboard login so it mints one server-side.

So plugins/codex-relay-skill/SKILL.md and plugins/gemini-relay-extension/GEMINI.md (from #1405) said observation "requires a separately provisioned, read-only observer token" and to "omit the observer link" when none exists. Correct about the danger, but with no reachable happy path it means nobody ever gets a link. Meanwhile the skills repo's relay-team / relay-fanout / relay-pipeline skills still mandate the opposite — printing the raw rk_live_ key in a URL, "This is mandatory" — which is the thing #1380 is meant to stop. This PR gives all of them one satisfiable answer.

What's added

  • agent-relay observer — mints a scoped token and prints the observer URL built from it. observer list and observer revoke <id> manage existing tokens.
  • get_observer_url MCP tool — same thing for an orchestrating agent, so a lead can hand the user a link without shelling out.
  • SDK exportscreateObserverToken, listObserverTokens, revokeObserverToken from @agent-relay/sdk.

The frontend already works: RelaySessionProvider.tsx:29 in relaycast accepts ot_live_ in ?key= exactly like rk_live_, so a scoped token is a drop-in URL replacement with no dashboard change.

Defaults, and why they differ from the dashboard's

The observer dashboard already auto-mints a token on workspace-key login (observer-dashboard/src/lib/observer-token.ts): 30-day expiry, include_dms: true. That is right for a token backing a browser session behind an httpOnly cookie. It is wrong for a token printed as a URL and pasted into chat, so this command defaults to 24 hours with agent DMs excluded, widened explicitly:

agent-relay observer                            # 24h, channels only
agent-relay observer --channels build,review    # narrow further
agent-relay observer --include-dms --expires 7d # widen deliberately
agent-relay observer revoke ot_abc123

These two defaults are the main judgement calls in this PR — easy to change if you'd rather match the dashboard.

observerUrl() throws on any credential that isn't an ot_live_ token, so the workspace-key-in-a-URL failure this command exists to prevent can't be reintroduced by a later caller.

Also updates the Codex skill and Gemini extension text to point at the command rather than describing the link as unobtainable. This is the first of several PRs from a review of the Agent Relay skills and plugins; the skills-repo side (including the relay-* plugin skills that print raw keys) follows separately.

Test Plan

  • Tests added — packages/cli/src/cli/commands/observer.test.ts (9 tests): default posture asserts includeDms: false and a 24h expiry; asserts the printed URL carries the token and that the workspace key appears nowhere in output; #-stripping and dedup on --channels; --expires 24 is rejected rather than guessed as hours or ms; missing token material fails loudly; observer list never prints ot_live_; observerUrl refuses a workspace key
  • Tests added — packages/sdk/src/__tests__/thin-client.test.ts (3 tests) covering the SDK helper's own scope/filter defaults, which the CLI tests stub out
  • packages/cli suite green — 899 passed, 11 skipped
  • packages/sdk suite green — 166 passed
  • npm run typecheck clean (exit 0)
  • bootstrap.test.ts command inventory updated. observer is a group with a default action, so the leaf-path walk skips it — added an explicit assertion so the primary command can't be dropped silently
  • Reverted unrelated package-lock.json drift that npm install introduced (committed lockfile still reads 11.2.0 vs package.json 11.4.0 — worth a separate look)
  • Manual testing against a live workspace — not run; no credentials in this environment. Worth one agent-relay observer against a real workspace before merge to confirm the minted token opens the dashboard

Screenshots

n/a


Generated by Claude Code

Review in cubic

…links

Sharing a live view of a workspace had no supported path. The engine
rejects a workspace key on the realtime endpoint and only accepts a
scoped `ot_live_` observer token, but nothing in the CLI or the MCP
server minted one — the only routes were a hand-rolled
`POST /v1/observer-tokens` or pasting an admin key into the dashboard
login. So the bundled instructions told agents that observation "requires
a separately provisioned observer token" and to omit the link when none
exists, which in practice meant never showing one.

Adds the missing primitive:

- `agent-relay observer` mints a scoped token and prints the observer URL
  built from it. `observer list` / `observer revoke <id>` manage tokens.
- `get_observer_url` MCP tool does the same for an orchestrating agent.
- `@agent-relay/sdk` exports `createObserverToken`, `listObserverTokens`,
  and `revokeObserverToken`.

Defaults are deliberately tighter than the observer dashboard's own
auto-minted token (30 days, DMs included): that one backs a browser
session behind an httpOnly cookie, while this one is printed as a URL
meant to be pasted into chat. 24 hours, agent DMs excluded, widened
explicitly via `--expires` / `--include-dms` / `--channels`.

`observerUrl` refuses any credential that is not an `ot_live_` token, so
the workspace-key-in-a-URL failure this command exists to prevent cannot
be reintroduced by a later caller.

Updates the Codex skill and Gemini extension instructions to point at the
command instead of describing the link as unobtainable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jmke9G9s7ftrN49opNmdx1
@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds scoped observer-token creation, listing, revocation, and URL generation to the SDK, CLI, and MCP interface. It also updates observation guidance and reclassifies the unreleased version as Minor.

Changes

Observer token workflows

Layer / File(s) Summary
SDK observer-token API
packages/sdk/src/messaging/thin-client.ts, packages/sdk/src/__tests__/thin-client.test.ts
The SDK adds observer scopes, token types, and workspace-scoped create, list, and revoke functions. Tests cover default and custom filters, expiry, listing, and revocation.
CLI commands and URL handling
packages/cli/src/cli/commands/observer.ts, packages/cli/src/cli/lib/observer-url.ts, packages/cli/src/cli/bootstrap.ts, packages/cli/src/cli/commands/observer.test.ts, packages/cli/src/cli/bootstrap.test.ts
The CLI adds observer token minting, metadata-only listing, revocation, option validation, URL construction, command registration, and tests.
MCP integration and guidance
packages/cli/src/cli/agent-relay-mcp.ts, plugins/codex-relay-skill/SKILL.md, plugins/gemini-relay-extension/GEMINI.md, CHANGELOG.md
The MCP interface adds get_observer_url. Plugin guidance uses the CLI workflow and documents token filters, expiry, and revocation. The unreleased version changes to Minor.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers: khaliqgant, claude

Poem

A rabbit mints a token bright,
Read-only scope and expiry right.
Channels narrow, DMs stay low,
URLs help observers go.
List or revoke with careful paws. 🐇

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 58.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main change: adding the observer CLI command to mint read-only follow-along links.
Description check ✅ Passed The description covers the summary, implementation details, test results, screenshots, and the pending manual test.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/agent-relay-skills-review-a8mgtd-observer-cli

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@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: 992f1d9df6

ℹ️ 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 +188 to +190
.action(async (options: Record<string, unknown>) => {
await runSdk(deps, async () => {
const tokens = await deps.listObserverTokens(connection(options));

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 Read inherited options in observer subcommands

When observer list or observer revoke is given --workspace-key, --base-url, or (for list) --json, Commander stores these duplicated options on the parent observer command, while this callback receives only the child command's local options object. Consequently agent-relay observer list --json still prints text, and explicit connection flags are ignored in favor of ambient credentials; the revoke action repeats the same pattern. Read optsWithGlobals() from the action command or otherwise merge the parent options before calling connection().

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
packages/cli/src/cli/agent-relay-mcp.ts (1)

582-643: 🧹 Nitpick | 🔵 Trivial

Consider an audit trail for agent-minted observer links.

get_observer_url lets an AI agent mint a follow-along link without an explicit CLI invocation by a human. Consider emitting a telemetry event or log line when a token is created through this tool, distinct from agent-relay observer, so workspace owners can audit who exposed a follow-along view and when.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/cli/agent-relay-mcp.ts` around lines 582 - 643, The
get_observer_url tool currently mints observer tokens without recording an audit
event. Add a telemetry event or log entry immediately after successful
createObserverToken validation, including the workspace, creation time, token
identifier, and MCP-tool source; keep it distinct from the agent-relay observer
command and avoid logging the token material or generated URL.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/cli/src/cli/agent-relay-mcp.ts`:
- Around line 582-643: The get_observer_url tool currently mints observer tokens
without recording an audit event. Add a telemetry event or log entry immediately
after successful createObserverToken validation, including the workspace,
creation time, token identifier, and MCP-tool source; keep it distinct from the
agent-relay observer command and avoid logging the token material or generated
URL.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 44ceb656-b252-46d8-ac8c-270d1bb53ad2

📥 Commits

Reviewing files that changed from the base of the PR and between 8a687a0 and fcaacb0.

📒 Files selected for processing (11)
  • CHANGELOG.md
  • packages/cli/src/cli/agent-relay-mcp.ts
  • packages/cli/src/cli/bootstrap.test.ts
  • packages/cli/src/cli/bootstrap.ts
  • packages/cli/src/cli/commands/observer.test.ts
  • packages/cli/src/cli/commands/observer.ts
  • packages/cli/src/cli/lib/observer-url.ts
  • packages/sdk/src/__tests__/thin-client.test.ts
  • packages/sdk/src/messaging/thin-client.ts
  • plugins/codex-relay-skill/SKILL.md
  • plugins/gemini-relay-extension/GEMINI.md

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

4 issues found and verified against the latest diff

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/cli/src/cli/lib/observer-url.ts">

<violation number="1" location="packages/cli/src/cli/lib/observer-url.ts:34">
P2: Non-HTTP(S) observer URLs pass validation and receive the bearer token in their query string. Restrict dashboard bases to `http:`/`https:` so a malformed or attacker-controlled setting cannot turn the generated link into a token-exfiltrating `data:`/custom-scheme URL.</violation>
</file>

<file name="packages/cli/src/cli/agent-relay-mcp.ts">

<violation number="1" location="packages/cli/src/cli/agent-relay-mcp.ts:620">
P2: An invalid `RELAY_OBSERVER_URL` creates a live 24-hour token and then fails before returning its URL or token material, leaving an unshareable credential active. Resolve/validate the observer base URL before minting so this configuration error has no token-creation side effect.</violation>
</file>

<file name="packages/cli/src/cli/commands/observer.ts">

<violation number="1" location="packages/cli/src/cli/commands/observer.ts:87">
P3: Repeated channel names can cause `--channels` to fail despite being collapsed before the SDK request. Apply the limit to the unique filter count.</violation>
</file>

<file name="packages/cli/src/cli/commands/observer.test.ts">

<violation number="1" location="packages/cli/src/cli/commands/observer.test.ts:57">
P3: This suite leaks environment mutations into other tests because `process.env` is changed directly without restoration. Using Vitest environment stubs and calling `vi.unstubAllEnvs()` in `afterEach` would preserve test isolation.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

try {
// Fail here rather than emitting a malformed link the caller only discovers
// after pasting it somewhere public.
new URL(value);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: Non-HTTP(S) observer URLs pass validation and receive the bearer token in their query string. Restrict dashboard bases to http:/https: so a malformed or attacker-controlled setting cannot turn the generated link into a token-exfiltrating data:/custom-scheme URL.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/cli/src/cli/lib/observer-url.ts, line 34:

<comment>Non-HTTP(S) observer URLs pass validation and receive the bearer token in their query string. Restrict dashboard bases to `http:`/`https:` so a malformed or attacker-controlled setting cannot turn the generated link into a token-exfiltrating `data:`/custom-scheme URL.</comment>

<file context>
@@ -0,0 +1,57 @@
+  try {
+    // Fail here rather than emitting a malformed link the caller only discovers
+    // after pasting it somewhere public.
+    new URL(value);
+  } catch {
+    throw new Error(`Invalid observer URL: ${value}`);
</file context>

const session = getSession();
requireWorkspaceKey(session);
const lifetimeHours = expires_in_hours ?? 24;
const token = await createObserverToken({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: An invalid RELAY_OBSERVER_URL creates a live 24-hour token and then fails before returning its URL or token material, leaving an unshareable credential active. Resolve/validate the observer base URL before minting so this configuration error has no token-creation side effect.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/cli/src/cli/agent-relay-mcp.ts, line 615:

<comment>An invalid `RELAY_OBSERVER_URL` creates a live 24-hour token and then fails before returning its URL or token material, leaving an unshareable credential active. Resolve/validate the observer base URL before minting so this configuration error has no token-creation side effect.</comment>

<file context>
@@ -577,6 +579,63 @@ function registerAgentRelayTools(
+      const session = getSession();
+      requireWorkspaceKey(session);
+      const lifetimeHours = expires_in_hours ?? 24;
+      const token = await createObserverToken({
+        workspaceKey: session.workspaceKey as string,
+        name: `observer-mcp-${Math.random().toString(36).slice(2, 10)}`,
</file context>

if (names.length === 0) {
throw new InvalidArgumentError('Expected at least one channel name.');
}
if (names.length > MAX_CHANNEL_FILTERS) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P3: Repeated channel names can cause --channels to fail despite being collapsed before the SDK request. Apply the limit to the unique filter count.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/cli/src/cli/commands/observer.ts, line 87:

<comment>Repeated channel names can cause `--channels` to fail despite being collapsed before the SDK request. Apply the limit to the unique filter count.</comment>

<file context>
@@ -0,0 +1,219 @@
+  if (names.length === 0) {
+    throw new InvalidArgumentError('Expected at least one channel name.');
+  }
+  if (names.length > MAX_CHANNEL_FILTERS) {
+    throw new InvalidArgumentError(`Expected at most ${MAX_CHANNEL_FILTERS} channel names.`);
+  }
</file context>


describe('agent-relay observer', () => {
beforeEach(() => {
process.env.RELAY_WORKSPACE_KEY = WORKSPACE_KEY;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P3: This suite leaks environment mutations into other tests because process.env is changed directly without restoration. Using Vitest environment stubs and calling vi.unstubAllEnvs() in afterEach would preserve test isolation.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/cli/src/cli/commands/observer.test.ts, line 57:

<comment>This suite leaks environment mutations into other tests because `process.env` is changed directly without restoration. Using Vitest environment stubs and calling `vi.unstubAllEnvs()` in `afterEach` would preserve test isolation.</comment>

<file context>
@@ -0,0 +1,159 @@
+
+describe('agent-relay observer', () => {
+  beforeEach(() => {
+    process.env.RELAY_WORKSPACE_KEY = WORKSPACE_KEY;
+    delete process.env.RELAY_OBSERVER_URL;
+    delete process.env.RELAY_BASE_URL;
</file context>

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