Repository navigation
fix(mothership): redact file text before grep clips or splits it #8929
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,6 @@ | ||
| import { sleep } from '@sim/utils/helpers' | ||
| import { isRecordLike } from '@sim/utils/object' | ||
| import { truncate } from '@sim/utils/string' | ||
| import { escapeRegExp } from '@sim/utils/string' | ||
| import type { ReadFileTextResponse } from 'sim/embed' | ||
| import { listCatalogTools } from '@/lib/catalog/application/list-tools' | ||
| import { readBlockCatalog } from '@/lib/catalog/application/read-block-catalog' | ||
|
|
@@ -14,6 +14,9 @@ import { | |
| agentCliOk, | ||
| } from '@/lib/mothership/agent-cli/types' | ||
| import { buildIntegrationToolSchemas } from '@/lib/mothership/chat/payload' | ||
| import { projectResolvedSecretModelContent } from '@/executor/utils/resolved-secret-content-projection' | ||
| import { OPAQUE_RESOLVED_SECRET_REPLACEMENT } from '@/executor/utils/resolved-secret-matcher' | ||
| import type { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry' | ||
|
|
||
| /** | ||
| * `grep <pattern> [--scope a,b] [--in <world|id|name|world/id>] [-i] [-C n] [-A n] [-B n] [--count] [--limit n]` — | ||
|
|
@@ -430,8 +433,36 @@ function compilePattern(raw: string, ignoreCase: boolean): (line: string) => boo | |
| return (line) => (ignoreCase ? line.toLowerCase() : line).includes(needle) | ||
| } | ||
|
|
||
| /** What redaction leaves in place of a secret: a `{{NAME}}` label or the opaque marker. */ | ||
| const REDACTION_MARKER = new RegExp( | ||
| `\\{\\{[^{}]+\\}\\}|${escapeRegExp(OPAQUE_RESOLVED_SECRET_REPLACEMENT)}`, | ||
| 'g' | ||
| ) | ||
|
|
||
| /** Clips redacted text, cutting before a redaction marker rather than through it. */ | ||
| function clip(line: string): string { | ||
| return truncate(line, MAX_LINE_CHARS, '… [line truncated]') | ||
| if (line.length <= MAX_LINE_CHARS) return line | ||
| let cut = MAX_LINE_CHARS | ||
| for (const marker of line.matchAll(REDACTION_MARKER)) { | ||
| if (marker.index >= cut) break | ||
| if (marker.index + marker[0].length > cut) { | ||
| cut = marker.index | ||
| break | ||
| } | ||
| } | ||
| return `${line.slice(0, cut)}… [line truncated]` | ||
| } | ||
|
|
||
| /** | ||
| * Model egress redacts whole secrets only, so a line clipped, or a multi-line secret split into | ||
| * lines, would carry a fragment it cannot recognize. The text is redacted whole first, through the | ||
| * same provenance egress uses; null means it could not be, and the resource is not searched. | ||
| * Without a registry egress withholds the whole result, so there is nothing to redact against. | ||
| */ | ||
| function redactForModel(text: string, egress: ResolvedSecretTraceRegistry | undefined) { | ||
| if (!egress) return text | ||
| const projection = projectResolvedSecretModelContent(text, egress) | ||
| return projection.safe && typeof projection.value === 'string' ? projection.value : null | ||
| } | ||
|
|
||
| function didYouMean(scope: string): string { | ||
|
|
@@ -587,9 +618,17 @@ export const universalGrepCommand: AgentCliEngine = { | |
| } | ||
| return now | ||
| } | ||
| const egress = runtime.resolvedSecretTraceRegistry?.forkForPropagatedEntries() | ||
| for (const resource of candidates) { | ||
| if (resource.text === null) continue | ||
| const lines = resource.text.split('\n') | ||
| const text = redactForModel(resource.text, egress) | ||
| if (text === null) { | ||
| bounded.reportIncomplete( | ||
| `${resource.scope}/${resource.label} (${resource.id}): could not be checked for secret values, so it was not searched.` | ||
| ) | ||
| continue | ||
| } | ||
| const lines = text.split('\n') | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Replacing a multi-line secret with one marker removes its newlines before Keep redaction before matching, but preserve the original line positions or map the redacted positions back to them. |
||
| const selected = new Map<number, boolean>() | ||
| const remainingLines = limit - out.length | ||
| let nextContextLine = 0 | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| import type { Principal } from '@sim/auth/principal' | ||
| import type { CopilotChatDelegationContext } from '@/lib/mothership/auth/application-delegation' | ||
| import type { AgentCliRawResult } from '@/lib/mothership/generated/agent-cli' | ||
| import type { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry' | ||
| /** | ||
| * Sim's half of the mothership↔CLI translation layer: generic execution PRIMITIVES. | ||
| * The worker owns the grammar (what commands exist, how argv parses, pipes, help, the | ||
|
|
@@ -36,6 +37,11 @@ export interface AgentCliRuntime { | |
| /** The human the command acts as — reference resolution and grants scope to them. */ | ||
| userId: string | ||
| chatId?: string | ||
| /** | ||
| * The invocation's secret provenance, which model egress redacts the result with. Egress | ||
| * matches whole secrets, so an engine that clips or splits text it read redacts it here first. | ||
| */ | ||
| resolvedSecretTraceRegistry?: ResolvedSecretTraceRegistry | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P1: This optional field lets grep run without provenance, and Prompt for AI agents |
||
| signal?: AbortSignal | ||
| } | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2:
describeSyntaxErrorremoves the quoted excerpt but leaves V8’sUnexpected token 's'prefix, so malformed JSON starting with a secret still sends its first character to Chat. Return a generic syntax diagnostic instead.Prompt for AI agents