diff --git a/.agents/skills/using-agent-relay/SKILL.md b/.agents/skills/using-agent-relay/SKILL.md index 296741fe9..e7a76f730 100644 --- a/.agents/skills/using-agent-relay/SKILL.md +++ b/.agents/skills/using-agent-relay/SKILL.md @@ -189,13 +189,16 @@ remove_agent(name: "reviewer-1", reason: "Review accepted") ## Current CLI Reference +Startup and status commands are intentionally omitted from these agent-facing +examples. Published Agent Relay versions through 11.3.0 can print live +workspace credentials when those commands run in a transcribed session. Upgrade +to Agent Relay 11.3.1 or later before running them there. + These are the current CLI forms for local broker and SDK-backed messaging operations: ```bash agent-relay status -agent-relay node up --verbose -agent-relay node status --wait-for 10 agent-relay node agent list agent-relay node agent spawn claude --name Worker --task "Use https://agentrelay.com/skill and ACK over Relay." agent-relay node tail --agent Worker diff --git a/.claude/skills/using-agent-relay/SKILL.md b/.claude/skills/using-agent-relay/SKILL.md index 296741fe9..e7a76f730 100644 --- a/.claude/skills/using-agent-relay/SKILL.md +++ b/.claude/skills/using-agent-relay/SKILL.md @@ -189,13 +189,16 @@ remove_agent(name: "reviewer-1", reason: "Review accepted") ## Current CLI Reference +Startup and status commands are intentionally omitted from these agent-facing +examples. Published Agent Relay versions through 11.3.0 can print live +workspace credentials when those commands run in a transcribed session. Upgrade +to Agent Relay 11.3.1 or later before running them there. + These are the current CLI forms for local broker and SDK-backed messaging operations: ```bash agent-relay status -agent-relay node up --verbose -agent-relay node status --wait-for 10 agent-relay node agent list agent-relay node agent spawn claude --name Worker --task "Use https://agentrelay.com/skill and ACK over Relay." agent-relay node tail --agent Worker diff --git a/CHANGELOG.md b/CHANGELOG.md index ab1518025..d37827116 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `agent-relay cloud whoami` prints the organization and workspace IDs alongside their names. +### Security + +- Bundled Gemini and Codex relay instructions and hooks no longer expose workspace administration keys in observer URLs or terminal transcripts; observation now requires a separately provisioned, read-only observer token. + ## [11.3.1] - 2026-07-31 ### Fixed diff --git a/packages/cli/src/cli/plugin-credential-safety.test.ts b/packages/cli/src/cli/plugin-credential-safety.test.ts new file mode 100644 index 000000000..9383a030d --- /dev/null +++ b/packages/cli/src/cli/plugin-credential-safety.test.ts @@ -0,0 +1,64 @@ +import { readFileSync, readdirSync } from 'node:fs'; +import { dirname, join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { describe, expect, it } from 'vitest'; + +const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../../../..'); + +const agentFacingSkillFiles = [ + '.agents/skills/using-agent-relay/SKILL.md', + '.claude/skills/using-agent-relay/SKILL.md', +] as const; + +function readRepoFile(path: string): string { + return readFileSync(join(repoRoot, path), 'utf8'); +} + +function listPluginFiles(directory: string): string[] { + return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => { + if (entry.name.startsWith('.') || entry.name === 'node_modules') return []; + + const path = join(directory, entry.name); + return entry.isDirectory() ? listPluginFiles(path) : [path]; + }); +} + +describe('shipped relay plugin credential safety', () => { + it('does not construct observer URLs from workspace keys in shipped plugin assets', () => { + const pluginFiles = listPluginFiles(join(repoRoot, 'plugins')); + + for (const path of pluginFiles) { + const source = readFileSync(path, 'utf8'); + expect(source, path).not.toMatch(/agentrelay\.com\/observer\?key=/i); + } + }); + + it('does not instruct agents to print real workspace keys or observer links', () => { + const pluginFiles = listPluginFiles(join(repoRoot, 'plugins')); + + for (const path of pluginFiles) { + const source = readFileSync(path, 'utf8'); + expect(source, path).not.toMatch(/\b(?:actual key|real clickable URL)\b/i); + expect(source, path).not.toMatch(/\bprint the observer URL\b/i); + + const credentialPrintLines = source + .split('\n') + .filter((line) => /\bprint\b/i.test(line) && /(?:workspace key|observer URL|ot_live_)/i.test(line)); + + for (const line of credentialPrintLines) { + expect(line, `${path}: ${line}`).toMatch(/\b(?:never|do not)\b[^\n]*\bprint\b/i); + } + } + + const sessionStartHook = readRepoFile('plugins/gemini-relay-extension/hooks/session-start.sh'); + expect(sessionStartHook).not.toMatch(/\bWORKSPACE_KEY\s*=/); + expect(sessionStartHook).not.toMatch(/\$\{?WORKSPACE_KEY\b/); + }); + + it('keeps credential-printing startup commands out of mirrored agent-facing skills', () => { + const [agentsSkill, claudeSkill] = agentFacingSkillFiles.map(readRepoFile); + + expect(agentsSkill).toBe(claudeSkill); + expect(agentsSkill).not.toMatch(/\bagent-relay node (?:up|status)\b/); + }); +}); diff --git a/plugins/codex-relay-skill/SKILL.md b/plugins/codex-relay-skill/SKILL.md index a82956350..7d172597d 100644 --- a/plugins/codex-relay-skill/SKILL.md +++ b/plugins/codex-relay-skill/SKILL.md @@ -29,7 +29,12 @@ Every relay-connected Codex agent must complete these steps IN ORDER before subs 2. **Register as an agent.** Call `register_agent` with your agent name and `type: "agent"`. Use `RELAY_AGENT_NAME` from the environment if set, otherwise derive a name from the task context (e.g., `lead`, `auth-worker`). -3. **Tell the user** they can follow the conversation live at `https://agentrelay.com/observer?key=` (use the workspace key from step 1). This lets them watch all agent messages in real time. +3. **Keep workspace credentials out of output.** Never print the workspace key + or construct an observer URL from it. If the user asks to follow the + conversation, explain that observation requires a separately provisioned, + read-only observer token (`ot_live_...`) delivered through an explicit + secret handoff. Do not print the token or place it in a URL query string; + when no scoped observer token is available, omit the observer link. 4. **Check the relay inbox.** Call `check_inbox` to see if there are any pending messages or task assignments. diff --git a/plugins/gemini-relay-extension/GEMINI.md b/plugins/gemini-relay-extension/GEMINI.md index 7915d2eb2..a6a11296b 100644 --- a/plugins/gemini-relay-extension/GEMINI.md +++ b/plugins/gemini-relay-extension/GEMINI.md @@ -2,15 +2,16 @@ This extension lets your Gemini CLI session communicate with other agents in real time via Agent Relay. A workspace is auto-created on first use — no configuration needed. -## Observer URL +## Safe observation -IMPORTANT: Before delegating any work to sub-agents, always print the observer URL so the user can watch the conversation in real time. Read the workspace key from the RELAY_API_KEY environment variable, or if empty read `~/.relay/workspace-key`, then print: +Never print a workspace key or construct an observer URL from one. Workspace +keys have administrative authority and do not belong in terminal transcripts +or URL query strings. -``` -Follow along at: https://agentrelay.com/observer?key= -``` - -Do not print a placeholder — print the real clickable URL with the actual key substituted in. This is mandatory every time you coordinate agents. +If the user asks to follow the conversation, explain that observation requires +a separately provisioned, read-only observer token (`ot_live_...`). The token +must be delivered through an explicit secret handoff, not printed by the agent. +When no scoped observer token has been provisioned, omit the observer link. ## Delegating to Sub-Agents diff --git a/plugins/gemini-relay-extension/commands/fanout/fanout.toml b/plugins/gemini-relay-extension/commands/fanout/fanout.toml index ad5e34ebe..adcd9f4f9 100644 --- a/plugins/gemini-relay-extension/commands/fanout/fanout.toml +++ b/plugins/gemini-relay-extension/commands/fanout/fanout.toml @@ -3,7 +3,7 @@ prompt = """Run a fan-out relay workflow for: {{args}} Use fan-out only when the work can be split into independent subtasks. Follow this protocol: -1. Read the workspace key from the RELAY_API_KEY environment variable, or if empty read ~/.relay/workspace-key. Print the observer URL so the user can follow along: https://agentrelay.com/observer?key=. This is mandatory. +1. Do not read or print the workspace key and do not construct an observer URL from it. If the user asks to follow along, explain that observation requires a separately provisioned, read-only observer token delivered through an explicit secret handoff. If none is available, omit the observer link. 2. Break the task into parallel subtasks with minimal overlap (max 5) 3. Delegate each subtask to a @relay-worker sub-agent. Give each a clear, bounded task description. Tell each worker your agent name so they can send ACK/DONE messages back to you via mcp_agent_relay_send_dm. 4. Monitor progress with mcp_agent_relay_check_inbox and answer worker questions diff --git a/plugins/gemini-relay-extension/commands/status/status.toml b/plugins/gemini-relay-extension/commands/status/status.toml index f5536ff17..38e4c905e 100644 --- a/plugins/gemini-relay-extension/commands/status/status.toml +++ b/plugins/gemini-relay-extension/commands/status/status.toml @@ -1,5 +1,5 @@ prompt = """Check the relay status: -1. Read the workspace key from the RELAY_API_KEY environment variable, or if empty read ~/.relay/workspace-key. Print the observer URL: https://agentrelay.com/observer?key=. Do not print a placeholder — print the real clickable URL. +1. Do not read or print the workspace key and do not construct an observer URL from it. Observation requires a separately provisioned, read-only observer token delivered through an explicit secret handoff. If none is available, omit the observer link. 2. Call mcp_agent_relay_list_agents to see who's online 3. Call mcp_agent_relay_check_inbox to see unread messages -4. Report a summary of the observer URL, agents, and any pending messages""" +4. Report a summary of the agents and any pending messages""" diff --git a/plugins/gemini-relay-extension/commands/team/team.toml b/plugins/gemini-relay-extension/commands/team/team.toml index 5d546701f..349539167 100644 --- a/plugins/gemini-relay-extension/commands/team/team.toml +++ b/plugins/gemini-relay-extension/commands/team/team.toml @@ -1,7 +1,7 @@ prompt = """Spawn a coordinated team of relay agents for: {{args}} Follow this protocol: -1. Read the workspace key from the RELAY_API_KEY environment variable, or if empty read ~/.relay/workspace-key. Print the observer URL so the user can follow along: https://agentrelay.com/observer?key=. This is mandatory. +1. Do not read or print the workspace key and do not construct an observer URL from it. If the user asks to follow along, explain that observation requires a separately provisioned, read-only observer token delivered through an explicit secret handoff. If none is available, omit the observer link. 2. Analyze the task and choose the simplest coordination pattern that fits it 3. Determine how many workers are needed (max 5) and assign each a bounded responsibility 4. Choose the right sub-agent type for each task: diff --git a/plugins/gemini-relay-extension/hooks/session-start.sh b/plugins/gemini-relay-extension/hooks/session-start.sh index d11bfa42b..9b9950fcc 100755 --- a/plugins/gemini-relay-extension/hooks/session-start.sh +++ b/plugins/gemini-relay-extension/hooks/session-start.sh @@ -21,10 +21,11 @@ load_env() { load_env -# Resolve workspace key: env > persisted key file -WORKSPACE_KEY="${RELAY_API_KEY:-}" -if [ -z "$WORKSPACE_KEY" ] && [ -s "$KEY_FILE" ]; then - WORKSPACE_KEY=$(cat "$KEY_FILE" 2>/dev/null || true) +# Track whether Relay is configured without reading credential material into +# this hook. Workspace keys must never be placed in injected context. +WORKSPACE_CONFIGURED=0 +if [ -n "${RELAY_API_KEY:-}" ] || [ -s "$KEY_FILE" ]; then + WORKSPACE_CONFIGURED=1 fi TOKEN="" @@ -39,13 +40,13 @@ if [ -f "$STATE_FILE" ] && command -v jq >/dev/null 2>&1; then fi if [ -n "${TOKEN:-}" ] && [ -n "${AGENT_NAME:-}" ]; then - if [ -n "${WORKSPACE_KEY:-}" ]; then - CONTEXT=$(printf 'Relaycast is connected as %s. Use the Agent Relay MCP tools for DMs, channels, inbox checks, and worker coordination. Follow the ACK/DONE protocol: acknowledge new assignments promptly, and send DONE when the task is complete. To spawn workers, use run_shell_command with: RELAY_AGENT_NAME=WorkerName gemini -y -i "task prompt" &. The user can observe agent conversations at: https://agentrelay.com/observer?key=%s' "$AGENT_NAME" "$WORKSPACE_KEY") + if [ "$WORKSPACE_CONFIGURED" -eq 1 ]; then + CONTEXT=$(printf 'Relaycast is connected as %s. Use the Agent Relay MCP tools for DMs, channels, inbox checks, and worker coordination. Follow the ACK/DONE protocol: acknowledge new assignments promptly, and send DONE when the task is complete. To spawn workers, use run_shell_command with: RELAY_AGENT_NAME=WorkerName gemini -y -i "task prompt" &. Never print the workspace key or construct an observer URL from it. Observation requires a separately provisioned, read-only observer token delivered through an explicit secret handoff.' "$AGENT_NAME") else CONTEXT=$(printf 'Relaycast is connected as %s. Use the Agent Relay MCP tools for DMs, channels, inbox checks, and worker coordination. Follow the ACK/DONE protocol: acknowledge new assignments promptly, and send DONE when the task is complete. To spawn workers, use run_shell_command with: RELAY_AGENT_NAME=WorkerName gemini -y -i "task prompt" &.' "$AGENT_NAME") fi -elif [ -n "${WORKSPACE_KEY:-}" ]; then - CONTEXT=$(printf 'Relaycast workspace key is configured. If the relay tools report "Not registered", call the register tool with your exact agent name before using messaging tools. The user can observe agent conversations at: https://agentrelay.com/observer?key=%s' "$WORKSPACE_KEY") +elif [ "$WORKSPACE_CONFIGURED" -eq 1 ]; then + CONTEXT='Relaycast workspace key is configured. If the relay tools report "Not registered", call the register tool with your exact agent name before using messaging tools. Never print the workspace key or construct an observer URL from it. Observation requires a separately provisioned, read-only observer token delivered through an explicit secret handoff.' else CONTEXT='Relaycast is connected. A workspace was auto-created. Use the Agent Relay MCP tools for messaging and worker coordination.' fi