Skip to content

Commit 5c98043

Browse files
enderyildirimclaude
andcommitted
fix(cli): stop logging project env var values in "started attempt" debug log
The managed run controller logged the raw `startRunAttempt` response body, which carries `envVars` — the project environment variables injected into the run — along with the trigger payload and run metadata. That went to the runner container's stdout on every attempt, and to the webapp debug-log endpoint when `TRIGGER_SEND_RUN_DEBUG_LOGS` is enabled. The stdout path runs through `redact()`, but its deny-list matches whole lowercased key names, so it filters `payload`/`metadata` and leaves every entry inside the `envVars` map untouched: `DATABASE_URL` or `SUPABASE_SERVICE_ROLE_KEY` match nothing in the list, and only values shaped like `tr_*`, `sk-*` or `Bearer *` are caught by the value pattern. The debug-log HTTP sink applies no redaction at all. Log an explicit projection instead: run/snapshot identifiers, task, queue and machine preset, plus the environment variable *names*. Names are the part with debugging value ("did this var reach the runner?"); the values never are. Because `envVars` keys are user-defined, no name-based deny-list can classify them — so values are dropped wholesale rather than filtered. The remaining fields are an allow-list, so a new field on the API response cannot silently reintroduce a leak. Same shape as #4336, which fixed the equivalent leak in `taskRunProcess.ts`. Refs #3566 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 14824b0 commit 5c98043

4 files changed

Lines changed: 125 additions & 1 deletion

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"trigger.dev": patch
3+
---
4+
5+
Stop logging project environment variable values in the managed run controller's "started attempt" debug log. The entry now records run/snapshot identifiers and the environment variable **names** only.

packages/cli-v3/src/entryPoints/managed/execution.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { type SnapshotState, SnapshotManager } from "./snapshot.js";
2525
import type { SupervisorSocket } from "./controller.js";
2626
import { RunNotifier } from "./notifier.js";
2727
import type { TaskRunProcessProvider } from "./taskRunProcessProvider.js";
28+
import { startedAttemptLogProperties } from "./executionLogging.js";
2829

2930
class ExecutionAbortError extends Error {
3031
constructor(message: string) {
@@ -447,7 +448,7 @@ export class RunExecution {
447448
podScheduledAt: this.podScheduledAt?.getTime(),
448449
});
449450

450-
this.sendDebugLog("started attempt", { start: start.data });
451+
this.sendDebugLog("started attempt", startedAttemptLogProperties(start.data));
451452

452453
return { ...start.data, metrics };
453454
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { describe, expect, it } from "vitest";
2+
import type { WorkloadRunAttemptStartResponseBody } from "@trigger.dev/core/v3/workers";
3+
import { startedAttemptLogProperties } from "./executionLogging.js";
4+
5+
const projectSecret = "project-env-var-secret-value";
6+
const payloadSecret = "trigger-payload-secret-value";
7+
8+
function createStartResponse(): WorkloadRunAttemptStartResponseBody {
9+
return {
10+
snapshot: {
11+
id: "snapshot-1",
12+
friendlyId: "run_snapshot_1234",
13+
executionStatus: "EXECUTING",
14+
description: "Attempt started",
15+
createdAt: new Date(),
16+
},
17+
run: {
18+
id: "run-1",
19+
friendlyId: "run_1234",
20+
status: "EXECUTING",
21+
attemptNumber: 2,
22+
},
23+
execution: {
24+
run: {
25+
id: "run-1",
26+
payload: JSON.stringify({ apiKey: payloadSecret }),
27+
payloadType: "application/json",
28+
tags: [],
29+
isTest: false,
30+
isReplay: false,
31+
createdAt: new Date(),
32+
startedAt: new Date(),
33+
},
34+
attempt: { number: 2, startedAt: new Date() },
35+
task: { id: "test-task", filePath: "test.ts" },
36+
queue: { id: "queue-1", name: "test-queue" },
37+
environment: { id: "env-1", slug: "test", type: "PRODUCTION" },
38+
organization: { id: "org-1", slug: "test-org", name: "Test Org" },
39+
project: { id: "proj-1", ref: "proj_test", slug: "test", name: "Test" },
40+
machine: { name: "small-1x", cpu: 0.5, memory: 0.5, centsPerMs: 0 },
41+
},
42+
envVars: {
43+
DATABASE_URL: projectSecret,
44+
SOME_PROVIDER_API_KEY: projectSecret,
45+
},
46+
} as unknown as WorkloadRunAttemptStartResponseBody;
47+
}
48+
49+
describe("startedAttemptLogProperties", () => {
50+
it("does not include environment variable values or the run payload", () => {
51+
const serialized = JSON.stringify(startedAttemptLogProperties(createStartResponse()));
52+
53+
expect(serialized).not.toContain(projectSecret);
54+
expect(serialized).not.toContain(payloadSecret);
55+
});
56+
57+
it("keeps environment variable names so operators can confirm what reached the run", () => {
58+
const properties = startedAttemptLogProperties(createStartResponse());
59+
60+
expect(properties.envVarKeys).toEqual(["DATABASE_URL", "SOME_PROVIDER_API_KEY"]);
61+
});
62+
63+
it("logs the identifiers needed to debug an attempt", () => {
64+
const properties = startedAttemptLogProperties(createStartResponse());
65+
66+
expect(properties).toEqual({
67+
runId: "run-1",
68+
runFriendlyId: "run_1234",
69+
runStatus: "EXECUTING",
70+
attemptNumber: 2,
71+
snapshotId: "run_snapshot_1234",
72+
executionStatus: "EXECUTING",
73+
taskIdentifier: "test-task",
74+
queue: "test-queue",
75+
machinePreset: "small-1x",
76+
isTest: false,
77+
envVarKeys: ["DATABASE_URL", "SOME_PROVIDER_API_KEY"],
78+
});
79+
});
80+
81+
it("handles a response with no environment variables", () => {
82+
const start = createStartResponse();
83+
// Older platform versions can omit envVars entirely.
84+
delete (start as Partial<WorkloadRunAttemptStartResponseBody>).envVars;
85+
86+
expect(startedAttemptLogProperties(start).envVarKeys).toEqual([]);
87+
});
88+
});
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import type { WorkloadRunAttemptStartResponseBody } from "@trigger.dev/core/v3/workers";
2+
3+
/**
4+
* Builds the debug-log properties for the "started attempt" entry.
5+
*
6+
* The raw `startRunAttempt` response body carries `envVars` — the project environment
7+
* variables injected into the run — plus the trigger payload and run metadata. Logging the
8+
* response as-is wrote all of that to the runner's stdout, and to the webapp debug-log
9+
* endpoint when `TRIGGER_SEND_RUN_DEBUG_LOGS` is enabled.
10+
*
11+
* `envVars` keys are user-defined, so no name-based deny-list can reliably classify them.
12+
* Only the names are logged, never the values — enough to confirm which variables reached the
13+
* run. Everything else is an explicit allow-list of identifiers, so a new field on the API
14+
* response can't reintroduce a leak.
15+
*/
16+
export function startedAttemptLogProperties(start: WorkloadRunAttemptStartResponseBody) {
17+
return {
18+
runId: start.run.id,
19+
runFriendlyId: start.run.friendlyId,
20+
runStatus: start.run.status,
21+
attemptNumber: start.run.attemptNumber,
22+
snapshotId: start.snapshot.friendlyId,
23+
executionStatus: start.snapshot.executionStatus,
24+
taskIdentifier: start.execution.task.id,
25+
queue: start.execution.queue.name,
26+
machinePreset: start.execution.machine.name,
27+
isTest: start.execution.run.isTest,
28+
envVarKeys: Object.keys(start.envVars ?? {}),
29+
};
30+
}

0 commit comments

Comments
 (0)