From 02fcf8fff72e96ef5e1edc7daf6f7292e522d888 Mon Sep 17 00:00:00 2001 From: relay Date: Sun, 2 Aug 2026 07:33:44 -0400 Subject: [PATCH 1/2] fix(core): redact credentials from observer output --- .../src/__tests__/channel-messenger.test.ts | 25 +++++++++++++++++++ packages/core/src/channel-messenger.ts | 5 ++++ packages/core/src/runner.ts | 8 ++++-- 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/packages/core/src/__tests__/channel-messenger.test.ts b/packages/core/src/__tests__/channel-messenger.test.ts index 1e9867e..3ef2372 100644 --- a/packages/core/src/__tests__/channel-messenger.test.ts +++ b/packages/core/src/__tests__/channel-messenger.test.ts @@ -4,7 +4,9 @@ import { describe, expect, it, vi } from 'vitest'; import { ChannelMessenger, formatError, + formatObserverUrl, formatStepOutput, + scrubSecrets, sendToChannel, truncateMessage, } from '../channel-messenger.js'; @@ -42,6 +44,29 @@ describe('channel messenger helpers', () => { expect(formatted).not.toContain('ghp_abcdefghijklmnopqrstuvwxyzABCDEFGHIJ'); }); + it.each([ + 'rk_live_0123456789abcdef', + 'at_live_0123456789abcdef', + 'nt_live_0123456789abcdef', + 'ot_live_0123456789abcdef', + 'cld_at_0123456789abcdef', + 'rth_at_0123456789abcdef', + 'ocl_node_enr_0123456789abcdef', + 'br_0123456789abcdef', + ])('scrubSecrets redacts Relay credential value %s', (credential) => { + const scrubbed = scrubSecrets(`request denied for ${credential}`); + expect(scrubbed).toBe('request denied for [REDACTED]'); + expect(scrubbed).not.toContain(credential); + }); + + it('redacts the auto-created workspace key in observer output', () => { + const workspaceKey = 'rk_live_0123456789abcdef'; + const observerUrl = formatObserverUrl(workspaceKey); + + expect(observerUrl).toBe('https://agentrelay.com/observer?key=[REDACTED]'); + expect(observerUrl).not.toContain(workspaceKey); + }); + it('formatError normalizes unknown errors', () => { expect(formatError('build', new Error('Boom'))).toBe('**[build]** Failed: Boom'); expect(formatError('build', 'bad input')).toBe('**[build]** Failed: bad input'); diff --git a/packages/core/src/channel-messenger.ts b/packages/core/src/channel-messenger.ts index 565afa7..52c1c02 100644 --- a/packages/core/src/channel-messenger.ts +++ b/packages/core/src/channel-messenger.ts @@ -47,6 +47,7 @@ export function formatError(stepName: string, error: unknown): string { // Common secret patterns to redact from channel output. const SECRET_PATTERNS = [ /(?:api[_-]?key|apikey|secret[_-]?key|access[_-]?token|auth[_-]?token|bearer)\s*[:=]\s*\S+/gi, + /(?:rk_live_|at_live_|nt_live_|ot_live_|cld_at_|rth_at_|ocl_node_enr_|br_)[a-zA-Z0-9_%-]+(?:\.[a-zA-Z0-9_%-]+)*/g, /(?:sk|pk|rk|ak)[-_][a-zA-Z0-9]{20,}/g, /ghp_[a-zA-Z0-9]{36,}/g, /gho_[a-zA-Z0-9]{36,}/g, @@ -97,6 +98,10 @@ export function scrubSecrets(text: string): string { return result; } +export function formatObserverUrl(workspaceKey: string): string { + return scrubSecrets(`https://agentrelay.com/observer?key=${workspaceKey}`); +} + function stripMalformedPtyFrameGarbage(line: string): string { const strippedRuns = line.replace(MALFORMED_PTY_FRAME_RUN_RE, ' '); const compact = strippedRuns.replace(SPINNER_RE, '').replace(/\s+/g, ''); diff --git a/packages/core/src/runner.ts b/packages/core/src/runner.ts index ee17545..02b2c68 100644 --- a/packages/core/src/runner.ts +++ b/packages/core/src/runner.ts @@ -64,7 +64,11 @@ import { ensureRelayfileMount, type MountHandle } from '@relayfile/sdk/workspace import { collectCliSession, type CliSessionReport } from './cli-session-collector.js'; import { executeApiStep } from './api-executor.js'; import { BudgetExceededError, BudgetTracker } from './budget-tracker.js'; -import { ChannelMessenger, scrubForChannel as scrubWorkflowOutputForChannel } from './channel-messenger.js'; +import { + ChannelMessenger, + formatObserverUrl, + scrubForChannel as scrubWorkflowOutputForChannel, +} from './channel-messenger.js'; import { InMemoryWorkflowDb } from './memory-db.js'; import { buildCommand as buildProcessCommand, spawnProcess } from './process-spawner.js'; import { createProcessBackendExecutor } from './process-backend-executor.js'; @@ -3782,7 +3786,7 @@ export class WorkflowRunner { this.log('API key resolved'); if (this.relayApiKeyAutoCreated && this.relayApiKey) { this.log(`Workspace created — follow this run in Relaycast:`); - this.log(` Observer: https://agentrelay.com/observer?key=${this.relayApiKey}`); + this.log(` Observer: ${formatObserverUrl(this.relayApiKey)}`); this.log(` Channel: ${channel}`); } } From 8528c8795a81ab5566e8431a2ef4f9e932b802f4 Mon Sep 17 00:00:00 2001 From: relay Date: Sun, 2 Aug 2026 08:08:42 -0400 Subject: [PATCH 2/2] fix(core): omit credential-bearing observer links --- .../src/__tests__/channel-messenger.test.ts | 25 ++++++++++++++----- packages/core/src/channel-messenger.ts | 8 ++++-- packages/core/src/runner.ts | 10 ++++---- 3 files changed, 30 insertions(+), 13 deletions(-) diff --git a/packages/core/src/__tests__/channel-messenger.test.ts b/packages/core/src/__tests__/channel-messenger.test.ts index 3ef2372..0b39775 100644 --- a/packages/core/src/__tests__/channel-messenger.test.ts +++ b/packages/core/src/__tests__/channel-messenger.test.ts @@ -4,7 +4,7 @@ import { describe, expect, it, vi } from 'vitest'; import { ChannelMessenger, formatError, - formatObserverUrl, + formatObserverGuidance, formatStepOutput, scrubSecrets, sendToChannel, @@ -59,12 +59,25 @@ describe('channel messenger helpers', () => { expect(scrubbed).not.toContain(credential); }); - it('redacts the auto-created workspace key in observer output', () => { - const workspaceKey = 'rk_live_0123456789abcdef'; - const observerUrl = formatObserverUrl(workspaceKey); + it.each([ + 'broker started on port 3888', + 'the library is at ./br', + 'abbreviation', + 'number_of_brokers=4', + 'https://agentrelay.com/observer', + ])('scrubSecrets preserves non-secret output %s', (text) => { + expect(scrubSecrets(text)).toBe(text); + }); + + it('omits credential-bearing observer links from auto-created workspace guidance', () => { + const guidance = formatObserverGuidance('workflow-room'); - expect(observerUrl).toBe('https://agentrelay.com/observer?key=[REDACTED]'); - expect(observerUrl).not.toContain(workspaceKey); + expect(guidance).toEqual([ + 'Workspace created for this workflow.', + ' Observation: requires a separately provisioned, read-only observer token', + ' Channel: workflow-room', + ]); + expect(guidance.join('\n')).not.toMatch(/observer\?key=|\[REDACTED\]/); }); it('formatError normalizes unknown errors', () => { diff --git a/packages/core/src/channel-messenger.ts b/packages/core/src/channel-messenger.ts index 52c1c02..6634efb 100644 --- a/packages/core/src/channel-messenger.ts +++ b/packages/core/src/channel-messenger.ts @@ -98,8 +98,12 @@ export function scrubSecrets(text: string): string { return result; } -export function formatObserverUrl(workspaceKey: string): string { - return scrubSecrets(`https://agentrelay.com/observer?key=${workspaceKey}`); +export function formatObserverGuidance(channel: string): string[] { + return [ + 'Workspace created for this workflow.', + ' Observation: requires a separately provisioned, read-only observer token', + ` Channel: ${channel}`, + ]; } function stripMalformedPtyFrameGarbage(line: string): string { diff --git a/packages/core/src/runner.ts b/packages/core/src/runner.ts index 02b2c68..b767fa2 100644 --- a/packages/core/src/runner.ts +++ b/packages/core/src/runner.ts @@ -66,7 +66,7 @@ import { executeApiStep } from './api-executor.js'; import { BudgetExceededError, BudgetTracker } from './budget-tracker.js'; import { ChannelMessenger, - formatObserverUrl, + formatObserverGuidance, scrubForChannel as scrubWorkflowOutputForChannel, } from './channel-messenger.js'; import { InMemoryWorkflowDb } from './memory-db.js'; @@ -3784,10 +3784,10 @@ export class WorkflowRunner { this.log('Resolving Relaycast API key...'); await this.ensureRelaycastApiKey(channel); this.log('API key resolved'); - if (this.relayApiKeyAutoCreated && this.relayApiKey) { - this.log(`Workspace created — follow this run in Relaycast:`); - this.log(` Observer: ${formatObserverUrl(this.relayApiKey)}`); - this.log(` Channel: ${channel}`); + if (this.relayApiKeyAutoCreated) { + for (const line of formatObserverGuidance(channel)) { + this.log(line); + } } }