mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-25 17:45:53 +01:00
fix(runner): show redacted shell commands
This commit is contained in:
1 parent
357caf4e3c
commit
f5f5997cb8
5 files changed
+54
-7
No files matched your search
@@ -88,7 +88,7 @@ function ProgressItem({
|
||||
<div className="pit-activity-detail">
|
||||
<p>
|
||||
{item.eventCount} sanitized progress update{item.eventCount === 1 ? "" : "s"} observed.
|
||||
Raw chain-of-thought and provider text are not exposed.
|
||||
Raw output, chain-of-thought, and credential values are not exposed.
|
||||
</p>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
@@ -130,8 +130,9 @@ While a turn runs, a live status rail appears immediately after send and tracks
|
||||
the newest safe Codex activity. Reasoning, planning, shell-command, file-change,
|
||||
MCP/dynamic-tool, assistant-stream, and Paperclip semantic-tool lifecycle events
|
||||
each trigger an interim frame. Discrete tools remain separate rows; only noisy
|
||||
text deltas are grouped. Raw provider text, command output, and chain of thought
|
||||
stay withheld.
|
||||
text deltas are grouped. Structured shell items show a bounded,
|
||||
credential-redacted command preview; raw command output, other provider
|
||||
payloads, and chain of thought stay withheld.
|
||||
|
||||
## Remote preview gateway
|
||||
|
||||
|
||||
@@ -772,6 +772,7 @@ export function projectCapabilityIssueThread(
|
||||
event: keyof typeof PROGRESS_EVENTS;
|
||||
at: string;
|
||||
count: number;
|
||||
command: string;
|
||||
}
|
||||
>();
|
||||
for (const entry of snapshot.evidence) {
|
||||
@@ -784,7 +785,7 @@ export function projectCapabilityIssueThread(
|
||||
const key = event.endsWith("_delta") ? `${entry.turnId}:${event}` : entry.id;
|
||||
const group = progressGroups.get(key);
|
||||
if (group === undefined) {
|
||||
progressGroups.set(key, { id: entry.id, turnId: entry.turnId, event, at: entry.at, count: 1 });
|
||||
progressGroups.set(key, { id: entry.id, turnId: entry.turnId, event, at: entry.at, count: 1, command: readString(entry.data.command) });
|
||||
} else {
|
||||
group.count += 1;
|
||||
}
|
||||
@@ -799,7 +800,7 @@ export function projectCapabilityIssueThread(
|
||||
activity: copy.activity,
|
||||
status: running ? "running" : "complete",
|
||||
label: copy.label,
|
||||
summary: running ? copy.running : copy.complete,
|
||||
summary: group.command || (running ? copy.running : copy.complete),
|
||||
eventCount: group.count,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -219,6 +219,27 @@ describe("Capability evidence redaction", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("shows bounded structured shell commands while redacting credential values", () => {
|
||||
const redacted = redactCapabilityEvidenceData("provider_event", {
|
||||
method: "item/started",
|
||||
params: {
|
||||
item: {
|
||||
type: "commandExecution",
|
||||
source: "unifiedExecStartup",
|
||||
command: "/bin/bash -lc 'curl -H \"Authorization: Bearer hidden-token\" --token pcp_1234567890 https://example.test && printf ok'",
|
||||
aggregatedOutput: CANARY,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(redacted.event).toBe("command_started");
|
||||
expect(redacted.command).toContain("/bin/bash -lc");
|
||||
expect(redacted.command).toContain("printf ok");
|
||||
expect(redacted.command).not.toContain("hidden-token");
|
||||
expect(redacted.command).not.toContain("pcp_1234567890");
|
||||
expect(JSON.stringify(redacted)).not.toContain(CANARY);
|
||||
});
|
||||
|
||||
it("collapses an unknown provider method rather than echoing it", () => {
|
||||
expect(redactCapabilityEvidenceData("provider_event", { method: `x/${CANARY}` })).toEqual({
|
||||
event: "other",
|
||||
|
||||
@@ -41,6 +41,7 @@ export type CapabilityEvidenceKind =
|
||||
|
||||
/** Disposition copy is rendered, so it is bounded rather than unbounded. */
|
||||
const MAX_DISPOSITION_CHARS = 500;
|
||||
const MAX_COMMAND_CHARS = 2_000;
|
||||
const MAX_FIELD_NAMES = 32;
|
||||
const MAX_ENTITY_REFS = 64;
|
||||
|
||||
@@ -130,6 +131,21 @@ function clamp(value: string, limit: number): string {
|
||||
return value.length <= limit ? value : `${value.slice(0, limit)}…`;
|
||||
}
|
||||
|
||||
function commandPreview(item: Record<string, unknown>): string | null {
|
||||
// `unifiedExecStartup` is the Codex app-server's structured shell item. Do
|
||||
// not start echoing arbitrary future provider item shapes just because they
|
||||
// happen to grow a field named `command`.
|
||||
if (asString(item.source) !== "unifiedExecStartup") return null;
|
||||
let command = clamp(asString(item.command), MAX_COMMAND_CHARS);
|
||||
if (command.length === 0) return null;
|
||||
command = command
|
||||
.replace(/bearer\s+[a-z0-9._-]+/gi, "Bearer [redacted]")
|
||||
.replace(/\b(?:sk|pcp)_[a-z0-9._-]{8,}\b/gi, "[redacted]")
|
||||
.replace(/((?:api[_-]?key|access[_-]?token|auth[_-]?token|password|secret)\s*=\s*)(?:"[^"]*"|'[^']*'|[^\s]+)/gi, "$1[redacted]")
|
||||
.replace(/(--(?:api-key|token|password|secret)\s+)(?:"[^"]*"|'[^']*'|[^\s]+)/gi, "$1[redacted]");
|
||||
return command;
|
||||
}
|
||||
|
||||
function copyFields(
|
||||
data: Record<string, unknown>,
|
||||
names: readonly string[],
|
||||
@@ -210,10 +226,17 @@ export function redactCapabilityEvidenceData(
|
||||
data: Record<string, unknown>,
|
||||
): Record<string, CapabilityJsonValue> {
|
||||
switch (kind) {
|
||||
case "provider_event":
|
||||
case "provider_event": {
|
||||
const event = capabilityProviderEventCategory(asString(data.method), asRecord(data.params));
|
||||
const item = asRecord(asRecord(data.params).item);
|
||||
const command = event === "command_started" || event === "command_completed"
|
||||
? commandPreview(item)
|
||||
: null;
|
||||
return {
|
||||
event: capabilityProviderEventCategory(asString(data.method), asRecord(data.params)),
|
||||
event,
|
||||
...(command === null ? {} : { command }),
|
||||
};
|
||||
}
|
||||
case "diagnostic":
|
||||
// A provider diagnostic is operator surface, not board surface. Only the
|
||||
// fact that one was recorded survives.
|
||||
@@ -343,6 +366,7 @@ export function capabilityEvidenceDetails(
|
||||
switch (kind) {
|
||||
case "provider_event":
|
||||
add("Event", data.event);
|
||||
add("Command", data.command);
|
||||
break;
|
||||
case "diagnostic":
|
||||
add("Visibility", data.diagnostic);
|
||||
|
||||
Reference in new issue
Block a user