Skip to content

feat(sdk): accept run-scoped Relaycast token file (RELAY_RUN_TOKEN_FILE) - #675

Draft
kjgbot wants to merge 2 commits into
mainfrom
feat/sdk-run-token-file
Draft

kjgbot wants to merge 2 commits into
mainfrom
feat/sdk-run-token-file

Conversation

@kjgbot

@kjgbot kjgbot commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

Deploy hold

Do not merge or release yet. This change depends on:

  • relaycast#559 (stacked on feat(sdk): prepare pinned Babysitter catalog export #550): rt_live_ run tokens accepted by POST /v1/agents / DELETE /v1/agents/:name
  • relay broker run-token PR (feat/broker-run-token-mode): HarnessDriverClient.spawn({ runTokenFile }) plus broker run-token mode. The @agent-relay/harness-driver peer floor must be raised to the release that ships it.
  • cloud#5161: mints the token and writes the access file. Gated by WORKFLOW_SYSTEM_HELD_REFRESH_ENABLED.
  • relayfile#553

Legacy RELAY_API_KEY runs behave the same as before.

Summary

This implements section 3 of the run-token contract: a flow sandbox can run agent communication with no static rk_live_ workspace key.

  • communication/run-token.ts (new)

    • Picks the credential. RELAY_RUN_TOKEN_FILE wins over RELAY_API_KEY.
    • Validates the access file. The path must be absolute and the file readable. It must hold JSON with a non-empty rt_live_ token and a future expiresAt, and no refreshToken.
    • Provides a credential provider that re-reads the file on every call.
    • Adds a thin fetch adapter for POST /v1/agents and DELETE /v1/agents/:name that handles the {ok,data} / {ok:false,error} envelope.
    • Error messages never include the token or the file contents. JSON.parse errors, which quote their input, are replaced.
  • preflight.ts: accepts either credential and validates the file.

  • relay.ts (createRuntime)

    • In run-token mode, the journal agent is registered and deleted through the adapter.
    • The messaging client is built from the per-agent at_live_ only.
    • HarnessDriverClient.spawn gets runTokenFile (the path), never the value, and no workspaceKey.
    • GET /v1/agents/:name is never called.
  • environment.ts: RELAY_RUN_TOKEN_FILE is added to the relay names that are blanked for communication agents and refused as grants.

  • redact.ts: the token pattern now covers rt_/rt_live_.

  • communication/agent-token.ts (new): at_live_ expiry handling (relaycast#550). Agent tokens expire after 15 minutes, and runs can last 12 hours.

    • In run-token mode, the journal agent's at_live_ is rotated via POST /v1/agents/:name/rotate-token, authenticated by its own at_live_.
    • Schedule: at 2/3 of the remaining lifetime or 5 minutes before token_expires_at, whichever is earlier. A token with no known expiry gets one probe rotation after 60s.
    • Failure policy: 401/403/404 stop rotation (fail closed, no takeover). Transient failures back off 2/5/10/20/30s until expiry.
    • This matches the relay broker in relay#1980 (agent_token_rotation.rs).
    • The token is held in memory only.
    • RelayRuntime.messaging is a facade that rebuilds the RelaycastMessagingClient whenever the token rotates. On a 401 it forces one rotation and retries once; sends carry idempotency keys, so a retry cannot duplicate a message.
  • 401 retry on register/delete: run-token register and delete retry once on 401. The retry re-reads the run token file, so a file Cloud rotated mid-request is picked up.

Relaycast auth check (relaycast#559 source). Every route the SDK uses in run-token mode accepts the per-agent at_live_, so no Relaycast change is needed:

SDK call Route Auth
channels.create POST /v1/channels requireAuth (any)
channels.get GET /v1/channels/:name requireWorkspaceRead, agent allowed
channels.join POST /v1/channels/:name/join requireAgentToken
messages.send POST /v1/channels/:name/messages requireSender
messages.direct POST /v1/dm requireAgentToken
rotation POST /v1/agents/:name/rotate-token requireAgentToken, self only

Why an adapter: RelaycastMessagingClient in @agent-relay/sdk takes a single static credential when it is constructed (workspaceKey/apiKey/agentToken). It has no provider hook, so it cannot follow a rotating file. The SDK should grow first-class credential-provider support, after which the adapter can be removed.

Tests

  • New tests/communication-run-token.test.ts:
    • registers with the current file token;
    • a rotated file (atomic temp file + rename) is used on the next call (DELETE);
    • spawn receives the path only: no rt_live_/rk_live_ in the spawn options, and no workspaceKey;
    • no GET calls;
    • the legacy key path is unchanged;
    • envelope errors are reported without the token value;
    • an expired file sends no request;
    • agents get RELAY_RUN_TOKEN_FILE=''.
  • tests/communication-environment-preflight.test.ts:
    • accepts a valid file;
    • the file wins over RELAY_API_KEY;
    • rejects a missing file, invalid JSON, an rk_ token, an empty token, a missing expiry, an expired token, a refreshToken, and a relative path, and the error messages do not contain the value;
    • the legacy cases still pass.
  • tests/redact.test.ts: added an rt_live_ case.
  • New tests/communication-agent-token.test.ts (fake clock):
    • the schedule math is correct;
    • rotation happens before expiry and is authenticated by the agent's own token;
    • the next rotation follows the new expiry;
    • transient failures are retried and a rejected rotation fails closed;
    • a 401 retry on register picks up a rotated run token file;
    • register/delete retry only once;
    • after a scheduled rotation, runtime messaging uses the rotated token;
    • a messaging 401 forces one rotation and retries;
    • no at_live_/rt_live_ value reaches the spawn options.
  • npm run typecheck and npm run typecheck:tests are clean.
  • Full npm test: 53 failures across 15 files, all outside this change.
    • The same 14 files / 52 failures reproduce with origin/main sources on this machine (Node 26: hosted-extension, live-kernel, webhook-live, mcp release binary, and others).
    • The extra failure, close-pr-flow, was flaky and passed on the comparison run.

Gaps

  • Driver/broker version floor. An older @agent-relay/harness-driver ignores runTokenFile. Raise the peer floor before release so an old broker cannot start without credentials.
  • Expired run token file. The provider rejects an expired file instead of waiting for Cloud to refresh it. A 401 gets one retry, which re-reads the file.
  • Rotation stops silently. When rotation fails closed, nothing is logged, so token values cannot leak into logs. The next messaging call then surfaces the 401.
  • No live end-to-end test against relaycast#559 yet.

🤖 Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.


Agent Relay sessions

  • claude session 79b75307 · opened via gh pr create · last active 2026-10-18

…tion

Agent communication now accepts RELAY_RUN_TOKEN_FILE (access-only
{"token":"rt_live_...","expiresAt":...}) alongside the legacy RELAY_API_KEY
(rk_live_), preferring the file when both are set.

- preflight validates the file (absolute path, readable, rt_live_ token,
  future expiresAt, no refresh token) without ever echoing the value
- the runtime registers and deletes its journal agent through a thin
  POST /v1/agents + DELETE /v1/agents/:name adapter that re-reads the file
  on every request; GET /v1/agents/:name is never used in this mode
- HarnessDriverClient.spawn receives runTokenFile (the path), never the value
- RELAY_RUN_TOKEN_FILE is blanked for communication agents; redaction covers rt_

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 18, 2026

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

relaycast#550 expires at_live_ tokens after 15 minutes while runs can last
hours. In run-token mode the SDK now rotates the journal agent's token via
POST /v1/agents/:name/rotate-token (authenticated by its own at_live_) at
two thirds of the lifetime or five minutes before expiry, whichever is
earlier, matching the relay broker's schedule and fail-closed policy
(401/403/404 stop; transient failures back off 2/5/10/20/30s). The token
stays in memory; the messaging facade rebuilds its client on rotation and
forces one rotation plus a single retry on 401. Run-token register/delete
retry once on 401 after re-reading the file.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@kjgbot

kjgbot commented Oct 18, 2026

Copy link
Copy Markdown
Contributor Author

maintainability lens — PASS

Maintainability review — PR #675 (run-token mode + agent token rotation)

The change is small (~460 LOC in two new files + wiring) and the WHY comments at the top of each new file are genuinely helpful. Below are the maintainability flags.

Concerns

1. isUnauthorized depends on an undocumented error shape from an external SDK.
packages/sdk/src/communication/agent-token.ts:38-41 duck-types on status / statusCode. The only in-repo thrower (RelaycastHttpError) sets status, but the 401 retry path in rotatingMessaging fires on errors from RelaycastMessagingClient (an external SDK import). The test at tests/communication-agent-token.test.ts:166-172 fabricates Object.assign(new Error('unauthorized'), { status: 401 }) rather than exercising the real client, so the test would still pass if the SDK renamed its field tomorrow. Pin the expectation in a comment or extract the shape as a type guard tied to the SDK's error class.

2. rotatingMessaging is a hand-mirrored facade (agent-token.ts:140-162). It enumerates agents.register/delete, channels.create/get/join, messages.direct/send. If RelayMessaging gains a method, the structural return type will catch a missing one, but TS will not catch a method whose signature widens, nor a new sub-property on an existing namespace. Six months from now this is where a new method quietly won't rotate. A proxy over the base client (or at minimum a comment pointing at relay.ts's RelayMessaging definition) would survive drift better.

3. Scheduled rotation fails closed silently. scheduledRotation on 401/403/404 or after exhausted backoffs calls this.stop() with no log, callback, or journal entry (agent-token.ts:118-128). AGENTS.md §4 says "fail closed … no silent fallbacks". Once rotation stops, the next messaging call hits isUnauthorized → session.rotate() → the same failure → the operation rethrows. The run eventually fails, but the operator sees a 401 on a message send, not "rotation stopped at 14:03 because 403". Consider a onError hook or at least surfacing through broker telemetry.

4. Broker boundary not actually verified. relay.ts:93-95 passes { runTokenFile } into HarnessDriverClient.spawn. All tests mock HarnessDriverClient, so nothing in this PR proves the broker binary accepts the field. If the companion broker change is not already merged (crates/broker/src/relaycast/agent_token_rotation.rs is referenced in a comment but not in this diff), run-token mode fails at spawn.

5. DEFAULT_BASE_URL fork. run-token.ts:11 hardcodes https://cast.agentrelay.com; the legacy path flows through RELAY_BASE_URL. relay.ts threads baseUrl through, but a future standalone caller of runTokenAgents or AgentTokenSession that forgets to pass baseUrl will hit prod. Reading RELAY_BASE_URL as a last-resort default inside relaycastBaseUrl would close that trap.

Notes

  • relayWorkspaceKey (relay.ts:44-48) now throws if a run-token credential is active. External callers of this exported function will get a misleading error. Rename or document.
  • The expiresAt (file) vs token_expires_at (API) split within the same module is a readability snag — a one-line comment near parseExpiry / parseRunTokenFile noting the two shapes would help.
  • session! non-null assertion at relay.ts:103 relies on an implicit invariant tied to credential.kind. A local const runTokenSession = session after the narrowing avoids the !.

No defects that block correctness, but items 1–4 are the ones a stranger will trip on.

REVIEW_PASSED

@kjgbot

kjgbot commented Oct 18, 2026

Copy link
Copy Markdown
Contributor Author

history lens — PASS

Blockers: none identified under the HISTORY criteria.

Notes

  • I ran git log --oneline -40 and inspected AGENTS.md, RFC-0001, DRIVE-LOG, NEXT, DIRECTIVES, and PR feat(sdk): accept run-scoped Relaycast token file (RELAY_RUN_TOKEN_FILE) #675’s two commit messages. I found no recorded removal that this diff demonstrably reverses. The credential changes preserve the workspace-key branch while adding an explicitly selected run-token branch (packages/sdk/src/communication/relay.ts:79–114).
  • This diff does not newly make Relaycast authoritative for execution. It changes authentication and token lifecycle around the existing messaging/projection boundary; createProjection remains the runtime’s projection interface (packages/sdk/src/communication/relay.ts:107–109). That supplies no blocker under settled decision flow/drive f59e279 08271341 #7.
  • The commit messages describe changes visible in the diff: credential-file validation and per-request reads (packages/sdk/src/communication/run-token.ts:22–78), proactive agent-token rotation (packages/sdk/src/communication/agent-token.ts:78–123), and one rotation plus one messaging retry on 401 (packages/sdk/src/communication/agent-token.ts:140–145). Neither commit claims passing test counts, mutation verification, or live execution evidence.

Concerns — nonblocking for this lens

  • The PR body explicitly holds deployment pending upstream support and a harness-driver version-floor increase. The new spawn option depends on that support (packages/sdk/src/communication/relay.ts:92–106). This is a documented integration dependency, not evidence of a repeated historical mistake.
  • Scheduled rotation failures stop the timer without surfacing an immediate diagnostic (packages/sdk/src/communication/agent-token.ts:111–120). The PR body discloses this limitation; it is not concealed scope.
  • The PR body reports test results but those results were not reproduced in this review. This verdict assesses historical regression, new settled-decision contradictions, and commit-message truthfulness; it does not certify those test claims.

REVIEW_PASSED

@kjgbot

kjgbot commented Oct 18, 2026

Copy link
Copy Markdown
Contributor Author

structure lens — UNCLEAR

Error: OpenCode's free tier can only be used from within OpenCode. Download it at https://opencode.ai and keep building.

@kjgbot

kjgbot commented Oct 18, 2026

Copy link
Copy Markdown
Contributor Author

🎯 review-swarm: FAILED (M:pass H:pass S:unclear)

Lens transcripts posted as sibling comments above.

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.

1 participant