Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 39 additions & 2 deletions src/lib/steps/harness/pi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import { fileURLToPath } from 'node:url'

import type { ToolDefinition } from '@earendil-works/pi-coding-agent'

import { toolInputTouchesSecret } from './secret-paths.js'
import {
redactSecretGrepLines,
toolInputTouchesSecret,
} from './secret-paths.js'
import type { Harness, HarnessRunStepArgs, StepRunResult } from './types.js'

// The official Seam MCP — same server the anthropic harness and the seam-plugin
Expand Down Expand Up @@ -249,6 +252,7 @@ type PiToolExecute = (

function guardSecretFileTools(tools: ToolDefinition[]): ToolDefinition[] {
return tools.map((tool) => {
const toolName = (tool as unknown as { name?: unknown }).name
const runOriginal = (
tool as unknown as { execute: PiToolExecute }
).execute.bind(tool)
Expand All @@ -270,12 +274,45 @@ function guardSecretFileTools(tools: ToolDefinition[]): ToolDefinition[] {
details: {},
}
}
return runOriginal(toolCallId, params, signal, onUpdate, ctx)
const result = await runOriginal(
toolCallId,
params,
signal,
onUpdate,
ctx,
)
// The input-path guard above blocks a grep that *names* .env, but a broad
// grep (path ".") still scans it and returns its lines. Strip those from
// the output — parity with the anthropic harness's PostToolUse redaction.
return toolName === 'grep' ? redactSecretLinesFromResult(result) : result
}
return { ...tool, execute } as unknown as ToolDefinition
})
}

// Apply redactSecretGrepLines to each text block of a tool result, leaving the
// result's shape (details, image blocks, …) otherwise untouched.
function redactSecretLinesFromResult(result: unknown): unknown {
if (typeof result !== 'object' || result == null) return result
const content = (result as { content?: unknown }).content
if (!Array.isArray(content)) return result
const redacted = content.map((block) => {
if (
typeof block === 'object' &&
block != null &&
(block as { type?: unknown }).type === 'text' &&
typeof (block as { text?: unknown }).text === 'string'
) {
return {
...block,
text: redactSecretGrepLines((block as { text: string }).text),
}
}
return block
})
return { ...(result as object), content: redacted }
}

function readRole(message: unknown): string | undefined {
const role = (message as { role?: unknown }).role
return typeof role === 'string' ? role : undefined
Expand Down
Loading