diff --git a/packages/core/src/__tests__/channel-messenger.test.ts b/packages/core/src/__tests__/channel-messenger.test.ts index 1e9867e..0b39775 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, + formatObserverGuidance, formatStepOutput, + scrubSecrets, sendToChannel, truncateMessage, } from '../channel-messenger.js'; @@ -42,6 +44,42 @@ 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.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(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', () => { 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..6634efb 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,14 @@ export function scrubSecrets(text: string): string { return result; } +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 { 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..b767fa2 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, + formatObserverGuidance, + 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'; @@ -3780,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: https://agentrelay.com/observer?key=${this.relayApiKey}`); - this.log(` Channel: ${channel}`); + if (this.relayApiKeyAutoCreated) { + for (const line of formatObserverGuidance(channel)) { + this.log(line); + } } }