Files
PaperClipAI/server/src/__tests__/redaction.test.ts
T
Dotta af3023f1e3 fix(runner): repair paid provider startup paths (#12769)
## Thinking Path

> - Paperclip manages AI agents that perform work.
> - Paperclip Runner connects durable task runs to local provider
processes.
> - The full-stack paid matrix exposed failures after the runner
integrity repair.
> - Verified JavaScript entrypoints lost their relative module graph
when Linux executed them through descriptor paths.
> - Returned provider startup errors also remained pending and became
indeterminate after recovery.
> - Sparse Codex tool lifecycle events lost the `write_document`
identity before task transcript projection.
> - This pull request repairs those three boundaries and makes the
structured-question fixture deterministic.
> - The benefit is repeatable provider startup, exact failure replay,
and correct inline Plan placement.

## Linked Issues or Issue Description

Refs #12721 and #12700.

**What happened?**

The paid runner matrix failed ACPX and OpenCode startup before provider
session creation. The runner journal then replaced the original startup
error with an indeterminate recovery result. Native Codex saved a Plan
but rendered it only as a fallback card. A legacy Claude waiting reply
could also echo the reserved terminal marker before the answer arrived.

**Expected behavior**

Verified JavaScript providers must start from immutable
descriptor-backed artifacts. Returned startup failures must persist as
terminal failed command results. Native tool lifecycle updates must
preserve the `write_document` boundary. Pre-answer fixture output must
not contain the reserved terminal marker.

**Steps to reproduce**

1. Run the local provider cells in the Runner Full-Stack E2E workflow.
2. Observe ACPX and OpenCode fail during `session.open` before provider
execution.
3. Observe recovery report `execution_indeterminate` instead of the
original startup error.
4. Run the native Codex Plan cell and observe the fallback Plan card
after the tool activity row.
5. Run the legacy Claude structured-question resume cell and observe an
early marker echo in waiting prose.

**Paperclip version or commit**

`0f9452101740835ce0b1488a204bf48acd5bafc3`

**Deployment mode**

Local development with the paid GitHub Actions acceptance workflow.

## What Changed

- Bundle the ACPX sidecar and OpenCode proxy as self-contained Node ESM
entrypoints before hashing and verified descriptor launch.
- Anchor ACPX dynamic provider package resolution at a
controller-derived provider-pack root and keep that root out of the
provider child environment.
- Persist executor-returned startup errors as redacted durable failed
command results while retaining indeterminate recovery for true process
death.
- Coalesce sparse native tool items by stable ID so a late
`write_document` name, input, and result reach the transcript boundary
once.
- Forbid the structured-question fixture from spelling or announcing its
reserved terminal marker before the user answers.

## Verification

- Rust and TypeScript regression tests cover durable failed replay, true
crash ambiguity, bundle closure, package-root derivation, environment
filtering, exact Codex tool lifecycle coalescing, and prompt
determinism.
- Local execution is intentionally limited to formatters and static diff
checks. GitHub Actions will run tests, type checks, builds, and security
checks.
- After ordinary CI is green, scoped paid cells will validate one ACPX
launch, one OpenCode launch, native Codex Plan projection, and legacy
Claude structured resume before a complete matrix rerun.
- Prior failing matrix:
https://github.com/paperclipai/paperclip/actions/runs/33682434315

## Risks

- Bundling changes the bytes covered by provider launch hashes.
Provider-pack generation already hashes the final built files.
- ACPX still loads qualified provider packages dynamically. The
controller supplies a normalized package root, while existing version,
digest, path, and descriptor checks remain active.
- Durable `failed` is terminal. Replays return the same redacted result
and do not execute the provider effect twice.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

OpenAI Codex based on GPT-5 with agentic reasoning, repository
inspection, code editing, Git, parallel subagents, and GitHub Actions
coordination. The exact deployed snapshot and context-window size are
not exposed to this task.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either linked related public work or described the bug in
this PR
- [x] I have not referenced internal or instance-local Paperclip issues
or links
- [x] My branch name describes the change and contains no internal
ticket id
- [ ] I have run tests locally and they pass (intentionally deferred to
GitHub Actions)
- [x] I have added or updated tests where applicable
- [x] No documentation change is required for this runtime repair
- [x] I have considered and documented the risks above
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge
2026-09-04 07:58:44 -05:00

701 lines
23 KiB
TypeScript

import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
import {
PRP_V1_EVENT_TYPES,
REDACTED_EVENT_VALUE,
redactAgentAdapterConfig,
redactEventPayload,
redactSensitiveText,
sanitizeRecord,
} from "../redaction.js";
describe("redaction", () => {
it("keeps the discriminator allowlist in exact PRP v1 schema parity", () => {
const schema = JSON.parse(
readFileSync(
new URL(
"../../../packages/paperclip-runner/protocol/schemas/event.schema.json",
import.meta.url,
),
"utf8",
),
) as { properties: { eventType: { enum: string[] } } };
expect([...PRP_V1_EVENT_TYPES]).toEqual(schema.properties.eventType.enum);
});
it("preserves every discriminator in the cross-language replay stream", () => {
const fixture = JSON.parse(
readFileSync(
new URL(
"../../../packages/paperclip-runner/protocol/fixtures/replay/duplicate-event.json",
import.meta.url,
),
"utf8",
),
) as { events: Array<Record<string, unknown>> };
for (const event of fixture.events) {
const sanitized = redactEventPayload({ prpEvent: event });
const envelope = sanitized?.prpEvent as Record<string, unknown>;
expect(envelope.eventType).toBe(event.eventType);
expect(envelope.sourceEventId).toBe(event.sourceEventId);
expect(envelope.payload).toEqual(event.payload);
}
});
it("redacts sensitive keys and nested secret values", () => {
const input = {
apiKey: "abc123",
nested: {
AUTH_TOKEN: "token-value",
safe: "ok",
},
env: {
OPENAI_API_KEY: "sk-openai",
OPENAI_API_KEY_REF: {
type: "secret_ref",
secretId: "11111111-1111-1111-1111-111111111111",
version: "aaa.bbb.ccc",
projectionAllowlistKey: "aaa.bbb.ccc",
token: "must-not-survive-reference-shape",
},
USER_API_KEY_REF: {
type: "user_secret_ref",
key: "OPENAI_API_KEY",
password: "must-not-survive-user-reference-shape",
},
OPENAI_API_KEY_PLAIN: {
type: "plain",
value: "sk-plain",
},
PAPERCLIP_API_URL: "http://localhost:3100",
},
};
const result = sanitizeRecord(input);
expect(result.apiKey).toBe(REDACTED_EVENT_VALUE);
expect(result.nested).toEqual({
AUTH_TOKEN: REDACTED_EVENT_VALUE,
safe: "ok",
});
expect(result.env).toEqual({
OPENAI_API_KEY: REDACTED_EVENT_VALUE,
OPENAI_API_KEY_REF: {
type: "secret_ref",
secretId: "11111111-1111-1111-1111-111111111111",
},
USER_API_KEY_REF: {
type: "user_secret_ref",
key: "OPENAI_API_KEY",
},
OPENAI_API_KEY_PLAIN: {
type: "plain",
value: REDACTED_EVENT_VALUE,
},
PAPERCLIP_API_URL: "http://localhost:3100",
});
});
it("redacts jwt-looking values even when key name is not sensitive", () => {
const jwt =
"eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";
const input = {
session: jwt,
opaque: "aaa.bbb.ccc",
normal: "plain",
};
const result = sanitizeRecord(input);
expect(result.session).toBe(REDACTED_EVENT_VALUE);
expect(result.opaque).toBe(REDACTED_EVENT_VALUE);
expect(result.normal).toBe("plain");
});
it("preserves Paperclip protocol schema identifiers", () => {
expect(
sanitizeRecord({
schema: "paperclip.question_set.v1",
nested: {
schema: "paperclip.question_response.v1",
runtimeSchema: "paperclip.runtime_request.v2",
arbitraryProviderValue: "paperclip.question_set.v1",
},
}),
).toEqual({
schema: "paperclip.question_set.v1",
nested: {
schema: "paperclip.question_response.v1",
runtimeSchema: "paperclip.runtime_request.v2",
arbitraryProviderValue: REDACTED_EVENT_VALUE,
},
});
});
it("preserves only known PRP v1 event discriminators inside validated envelopes", () => {
const payload = {
prpEvent: {
schema: "paperclip.prp.event.v1",
schemaVersion: 1,
eventType: "tool.execution.started",
payload: {
eventType: "run.result.accepted",
credential: "aaa.bbb.ccc",
},
},
unrelated: {
eventType: "workspace.file.referenced",
},
};
const sanitized = redactEventPayload(payload);
expect(sanitized).toEqual({
prpEvent: {
schema: "paperclip.prp.event.v1",
schemaVersion: 1,
eventType: "tool.execution.started",
payload: {
eventType: REDACTED_EVENT_VALUE,
credential: REDACTED_EVENT_VALUE,
},
},
unrelated: {
eventType: REDACTED_EVENT_VALUE,
},
});
expect(redactEventPayload(sanitized)).toEqual(sanitized);
});
it("redacts unknown dotted event values even in a PRP-shaped envelope", () => {
expect(
redactEventPayload({
schema: "paperclip.prp.event.v1",
schemaVersion: 1,
eventType: "attacker.supplied.token",
})?.eventType,
).toBe(REDACTED_EVENT_VALUE);
});
it("does not trust discriminators inside a forged unknown schema", () => {
expect(
redactEventPayload({
schema: "paperclip.attacker.control.v1",
runtimeSchema: "paperclip.attacker.runtime.v1",
schemaVersion: 1,
eventType: "tool.execution.started",
}),
).toEqual({
schema: REDACTED_EVENT_VALUE,
runtimeSchema: REDACTED_EVENT_VALUE,
schemaVersion: 1,
eventType: REDACTED_EVENT_VALUE,
});
});
it("preserves native run span identities without weakening hostname redaction", () => {
const spanNames = [
"environment.workspace.realize",
"native.coordinator.claim",
"runner.transport.selected",
"runner.prp.authenticate",
"runner.prp.route.register",
"runner.transport.connect",
"runner.session.bootstrap",
"runner.turn.submit",
"runner.session.startup",
"provider.turn.queue",
"native.session.execute",
"native.result.finalize",
"task.run.measured",
];
for (const span of spanNames) {
const input = {
schema: "paperclip.run-performance-span.v1",
span,
parentSpan: "native.session.execute",
providerHostname: "api.openai.com",
};
const sanitized = redactEventPayload(input);
expect(sanitized).toMatchObject({
schema: input.schema,
span,
parentSpan: "native.session.execute",
providerHostname: REDACTED_EVENT_VALUE,
});
expect(redactEventPayload(sanitized)).toEqual(sanitized);
}
expect(
redactEventPayload({
schema: "paperclip.run-performance-span.v1",
span: "api.openai.com",
})?.span,
).toBe(REDACTED_EVENT_VALUE);
expect(
redactEventPayload({
schema: "paperclip.run-performance-span.v1",
span: "runner.example.com",
})?.span,
).toBe(REDACTED_EVENT_VALUE);
});
it("redacts payload objects while preserving null", () => {
expect(redactEventPayload(null)).toBeNull();
expect(redactEventPayload({ password: "hunter2", safe: "value" })).toEqual({
password: REDACTED_EVENT_VALUE,
safe: "value",
});
});
it("redacts credentials in ordinary nested diagnostic strings", () => {
const sanitized = redactEventPayload({
message: "request failed with Authorization: Basic dXNlcjpwYXNz",
diagnostic: {
reason: "upstream returned Bearer live-provider-token",
details: [
"safe context",
"proxyAuthorization Basic nested-proxy-secret",
"aaa.bbb.ccc",
["Bearer nested-array-secret"],
],
},
});
expect(sanitized).toEqual({
message: `request failed with Authorization: ${REDACTED_EVENT_VALUE}`,
diagnostic: {
reason: `upstream returned Bearer ${REDACTED_EVENT_VALUE}`,
details: [
"safe context",
`proxyAuthorization ${REDACTED_EVENT_VALUE}`,
REDACTED_EVENT_VALUE,
[`Bearer ${REDACTED_EVENT_VALUE}`],
],
},
});
expect(redactEventPayload(sanitized)).toEqual(sanitized);
});
it("preserves authorization decision reasons in audit payloads", () => {
expect(
redactEventPayload({
authorizationReason: "allow_scoped_agent_write",
authorization: "Bearer secret",
surface: "issue.comment.create",
}),
).toEqual({
authorizationReason: "allow_scoped_agent_write",
authorization: REDACTED_EVENT_VALUE,
surface: "issue.comment.create",
});
});
/**
* A removal receipt (PAP-17119) has to show what it revoked, so a fixed set of
* count keys is exempt from the secret-key guard — but only while the value is
* a number. The second half of this test is the point: the same key carrying
* anything else is still blanked, so the exemption cannot be used to smuggle
* material out under a familiar name.
*/
it("keeps numeric removal-receipt counts but still redacts non-numeric values on the same keys", () => {
expect(
sanitizeRecord({
secretsRevoked: 2,
secretsRetainedShared: 0,
credentialRefsCleared: 3,
secretBindingsRemoved: 3,
tokenIssuanceHashesCleared: 1,
gatewayTokensRevoked: 0,
appProfile: "deleted",
}),
).toEqual({
secretsRevoked: 2,
secretsRetainedShared: 0,
credentialRefsCleared: 3,
secretBindingsRemoved: 3,
tokenIssuanceHashesCleared: 1,
gatewayTokensRevoked: 0,
appProfile: "deleted",
});
expect(
sanitizeRecord({
secretsRevoked: "pasted-api-key-value",
secretBindingsRemoved: { name: "tool_app.abc.headers_authorization" },
tokenIssuanceHashesCleared: Number.NaN,
gatewayTokensRevoked: ["pcgw_live_token"],
}),
).toEqual({
secretsRevoked: REDACTED_EVENT_VALUE,
secretBindingsRemoved: REDACTED_EVENT_VALUE,
tokenIssuanceHashesCleared: REDACTED_EVENT_VALUE,
gatewayTokensRevoked: REDACTED_EVENT_VALUE,
});
});
it("redacts common secret shapes from unstructured text", () => {
const jwt =
"eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";
const githubToken = "ghp_1234567890abcdefghijklmnopqrstuvwxyz";
const input = [
"Authorization: Bearer live-bearer-token-value",
`payload {"apiKey":"json-secret-value"}`,
`paperclip {"PAPERCLIP_API_KEY":"paperclip-json-secret"}`,
`escaped {\\"apiKey\\":\\"escaped-json-secret\\"}`,
`export PAPERCLIP_API_KEY='paperclip-shell-secret'`,
`GITHUB_TOKEN=${githubToken}`,
`session=${jwt}`,
].join("\n");
const result = redactSensitiveText(input);
expect(result).toContain(REDACTED_EVENT_VALUE);
expect(result).not.toContain("live-bearer-token-value");
expect(result).not.toContain("json-secret-value");
expect(result).not.toContain("paperclip-json-secret");
expect(result).not.toContain("escaped-json-secret");
expect(result).not.toContain("paperclip-shell-secret");
expect(result).not.toContain(githubToken);
expect(result).not.toContain(jwt);
});
it("redacts authorization variants and standalone bearer credentials from diagnostic text", () => {
const input = [
"Authorization: Basic dXNlcjpwYXNz",
"Authorization Basic uncolonized-secret",
"proxyAuthorization Basic compound-secret",
'Authorization Bearer abc"embedded-tail',
'Authorization "Bearer quoted-secret"',
'request failed with Bearer standalone"embedded-tail',
].join("\n");
const result = redactSensitiveText(input);
expect(result).toContain(`Authorization: ${REDACTED_EVENT_VALUE}`);
expect(result).toContain(`Authorization ${REDACTED_EVENT_VALUE}`);
expect(result).toContain(`proxyAuthorization ${REDACTED_EVENT_VALUE}`);
expect(result).toContain(`Authorization "${REDACTED_EVENT_VALUE}"`);
expect(result).toContain(`Bearer ${REDACTED_EVENT_VALUE}`);
expect(result).not.toContain("dXNlcjpwYXNz");
expect(result).not.toContain("uncolonized-secret");
expect(result).not.toContain("compound-secret");
expect(result).not.toContain("embedded-tail");
expect(result).not.toContain("quoted-secret");
expect(result).not.toContain("standalone-secret");
expect(redactSensitiveText(result)).toBe(result);
});
it("redacts raw and escaped quote-delimited authorization values without leaking nested content", () => {
const cases = [
{
input: 'prefix Authorization: Bearer "two word secret" suffix',
expected: `prefix Authorization: ${REDACTED_EVENT_VALUE} suffix`,
},
{
input: "prefix Authorization: Basic 'two word secret' suffix",
expected: `prefix Authorization: ${REDACTED_EVENT_VALUE} suffix`,
},
{
input: String.raw`prefix {\"Authorization\":\"Basic dXNlcjpwYXNz\"} suffix`,
expected: String.raw`prefix {\"Authorization\":\"***REDACTED***\"} suffix`,
},
{
input: String.raw`prefix Authorization: \"Bearer \\\"nested two word secret\\\"\" suffix`,
expected: String.raw`prefix Authorization: \"***REDACTED***\" suffix`,
},
];
for (const { input, expected } of cases) {
const result = redactSensitiveText(input);
expect(result).toBe(expected);
expect(result).not.toContain("two word secret");
expect(result).not.toContain("dXNlcjpwYXNz");
expect(redactSensitiveText(result)).toBe(result);
}
});
it("redacts raw and serialized standalone bearer values without leaking quoted content", () => {
const cases = [
{
input: 'prefix Bearer "two word secret" suffix',
expected: `prefix Bearer "${REDACTED_EVENT_VALUE}" suffix`,
},
{
input: "prefix Bearer 'two word secret' suffix",
expected: `prefix Bearer '${REDACTED_EVENT_VALUE}' suffix`,
},
{
input: String.raw`prefix Bearer \"two word secret\" suffix`,
expected: String.raw`prefix Bearer \"***REDACTED***\" suffix`,
},
{
input: String.raw`prefix Bearer \"outer \\\"nested secret\\\" value\" suffix`,
expected: String.raw`prefix Bearer \"***REDACTED***\" suffix`,
},
];
for (const { input, expected } of cases) {
const result = redactSensitiveText(input);
expect(result).toBe(expected);
expect(result).not.toContain("two word secret");
expect(result).not.toContain("nested secret");
expect(redactSensitiveText(result)).toBe(result);
}
});
it("redacts token tails attached to raw and escaped closing credential quotes", () => {
const cases = [
{
input: 'Authorization: Basic "abc"defg retry',
expected: `Authorization: ${REDACTED_EVENT_VALUE}`,
},
{
input: String.raw`Authorization: Basic \"abc\"defg retry`,
expected: `Authorization: ${REDACTED_EVENT_VALUE}`,
},
{
input: 'Bearer "abc"defg retry',
expected: `Bearer "${REDACTED_EVENT_VALUE}"`,
},
{
input: String.raw`Bearer \"abc\"defg retry`,
expected: String.raw`Bearer \"***REDACTED***\"`,
},
{
input: 'Authorization: Basic "abc"defg retry\nsafe context',
expected: `Authorization: ${REDACTED_EVENT_VALUE}`,
},
{
input:
String.raw`Authorization: Basic \"abc\"defg retry` + "\nsafe context",
expected: `Authorization: ${REDACTED_EVENT_VALUE}`,
},
{
input: 'authorization="Bearer abc\ndef" status=401',
expected: `authorization="${REDACTED_EVENT_VALUE}" status=401`,
},
{
input: String.raw`authorization=\"Bearer abc
def\" status=401`,
expected: String.raw`authorization=\"***REDACTED***\" status=401`,
},
{
input: 'authorization="Bearer abc"\ndef" status=401',
expected: `authorization="${REDACTED_EVENT_VALUE}" status=401`,
},
{
input: String.raw`authorization=\"Bearer abc\"
def\" status=401`,
expected: String.raw`authorization=\"***REDACTED***\" status=401`,
},
{
input: 'authorization="Bearer abc"\ndef status=401',
expected: `authorization="${REDACTED_EVENT_VALUE}"`,
},
{
input: String.raw`authorization=\"Bearer abc\"
def status=401`,
expected: String.raw`authorization=\"***REDACTED***\"`,
},
{
input:
'authorization="Bearer first-line"\nrequest failed with Bearer standalone"embedded-tail',
expected: `authorization="${REDACTED_EVENT_VALUE}"\nrequest failed with Bearer ${REDACTED_EVENT_VALUE}`,
},
{
input: String.raw`authorization=\"Bearer first-line\"
request failed with Bearer standalone\"embedded-tail`,
expected: String.raw`authorization=\"***REDACTED***\"
request failed with Bearer ***REDACTED***`,
},
{
input: String.raw`{\"authorization\":\"Bearer a\"b c\"} suffix`,
expected: String.raw`{\"authorization\":\"***REDACTED***\"} suffix`,
},
{
input: `{"authorization":"Bearer a"b c"} suffix`,
expected: `{"authorization":"${REDACTED_EVENT_VALUE}"} suffix`,
},
{
input: String.raw`{\"authorization\":\"Bearer a\" b c\"} suffix`,
expected: String.raw`{\"authorization\":\"***REDACTED***\"} suffix`,
},
{
input: `{"authorization":"Bearer a" b c"} suffix`,
expected: `{"authorization":"${REDACTED_EVENT_VALUE}"} suffix`,
},
];
for (const { input, expected } of cases) {
const result = redactSensitiveText(input);
expect(result).toBe(expected);
expect(result).not.toContain("abc");
expect(result).not.toContain("defg");
expect(result).not.toContain('a"b c');
expect(result).not.toContain('a" b c');
expect(redactSensitiveText(result)).toBe(result);
}
const sanitized = redactEventPayload({
message: 'Authorization: Basic "abc"defg retry',
malformedProviderText: '{"authorization":"Bearer a" b c"} suffix',
authorization: 'Bearer a"b c"',
nested: {
diagnostics: [
String.raw`Bearer \"abc\"defg retry`,
String.raw`{\"authorization\":\"Bearer a\"b c\"} suffix`,
String.raw`authorization=\"Bearer first-line
second-line\" status=401`,
String.raw`authorization=\"Bearer first-line\"
second-line\" status=401`,
],
},
});
expect(sanitized).toEqual({
message: `Authorization: ${REDACTED_EVENT_VALUE}`,
malformedProviderText: `{"authorization":"${REDACTED_EVENT_VALUE}"} suffix`,
authorization: REDACTED_EVENT_VALUE,
nested: {
diagnostics: [
String.raw`Bearer \"***REDACTED***\"`,
String.raw`{\"authorization\":\"***REDACTED***\"} suffix`,
String.raw`authorization=\"***REDACTED***\" status=401`,
String.raw`authorization=\"***REDACTED***\" status=401`,
],
},
});
expect(redactEventPayload(sanitized)).toEqual(sanitized);
});
it("redacts inline secrets from command metadata without hiding safe command text", () => {
const input = {
command:
"custom-acp --token ghp_example_secret env OPENAI_API_KEY=sk-live-example custom-acp",
commandArgs: [
"--safe",
"ok",
"--token",
"ghp_arg_secret",
"--api-key=sk-inline-example",
],
env: {
PAPERCLIP_RESOLVED_COMMAND:
"env OPENAI_API_KEY=sk-live-example custom-acp --token ghp_example_secret",
SAFE_VALUE: "visible",
},
};
const result = redactEventPayload(input);
expect(result?.command).toBe(
`custom-acp --token ${REDACTED_EVENT_VALUE} env OPENAI_API_KEY=${REDACTED_EVENT_VALUE} custom-acp`,
);
expect(result?.commandArgs).toEqual([
"--safe",
"ok",
"--token",
REDACTED_EVENT_VALUE,
`--api-key=${REDACTED_EVENT_VALUE}`,
]);
expect(result?.env).toEqual({
PAPERCLIP_RESOLVED_COMMAND: `env OPENAI_API_KEY=${REDACTED_EVENT_VALUE} custom-acp --token ${REDACTED_EVENT_VALUE}`,
SAFE_VALUE: "visible",
});
});
it("redacts non-string command args after secret flags", () => {
const result = redactEventPayload({
commandArgs: ["--api-key", { nested: "secret-value" }, "safe-next"],
});
expect(result?.commandArgs).toEqual([
"--api-key",
REDACTED_EVENT_VALUE,
"safe-next",
]);
});
it("does not treat bare args payloads as command args", () => {
const result = redactEventPayload({
args: ["--api-key", "not-a-command-secret"],
argv: ["--api-key", "command-secret"],
});
expect(result?.args).toEqual(["--api-key", "not-a-command-secret"]);
expect(result?.argv).toEqual(["--api-key", REDACTED_EVENT_VALUE]);
});
it("redacts every plaintext agent env binding while preserving secret references", () => {
const plaintextValue = "adapter-env-value-must-not-leak";
const result = redactAgentAdapterConfig({
command: "pnpm agent:run",
env: {
EXISTING_VALUE: plaintextValue,
NEW_VALUE: { type: "plain", value: plaintextValue },
SECRET_REFERENCE: {
type: "secret_ref",
secretId: "55555555-5555-4555-8555-555555555555",
version: "latest",
},
USER_SECRET_REFERENCE: {
type: "user_secret_ref",
key: "GITHUB_TOKEN",
},
},
});
expect(result).toEqual({
command: "pnpm agent:run",
env: {
EXISTING_VALUE: { type: "plain", value: REDACTED_EVENT_VALUE },
NEW_VALUE: { type: "plain", value: REDACTED_EVENT_VALUE },
SECRET_REFERENCE: {
type: "secret_ref",
secretId: "55555555-5555-4555-8555-555555555555",
version: "latest",
},
USER_SECRET_REFERENCE: {
type: "user_secret_ref",
key: "GITHUB_TOKEN",
},
},
});
expect(JSON.stringify(result)).not.toContain(plaintextValue);
});
it("redacts non-env adapter keys while leaving env binding shapes intact", () => {
const result = redactAgentAdapterConfig({
command: "pnpm agent:run",
apiKey: "adapter-level-secret",
env: {
API_KEY: "env-level-secret",
AUTH_TOKEN: { type: "plain", value: "another-env-secret" },
},
});
// Non-env keys still go through the shared payload sanitizer.
expect(result.apiKey).toBe(REDACTED_EVENT_VALUE);
expect(result.command).toBe("pnpm agent:run");
// Env bindings keep their binding shape rather than collapsing to a bare
// sentinel string, which is what a second sanitizer pass would produce for
// these sensitive-looking key names.
expect(result.env).toEqual({
API_KEY: { type: "plain", value: REDACTED_EVENT_VALUE },
AUTH_TOKEN: { type: "plain", value: REDACTED_EVENT_VALUE },
});
});
it("redacts adapter configs that have no env block", () => {
expect(redactAgentAdapterConfig({ command: "pnpm agent:run", apiKey: "secret" })).toEqual({
command: "pnpm agent:run",
apiKey: REDACTED_EVENT_VALUE,
});
});
});