fix(worker): materialize env files 0600, re-materialize per run, delete on stop

This commit is contained in:
Gabriel Brown
2026-07-11 11:36:03 -04:00
parent 424307c5af
commit 58c815a7a7
4 changed files with 105 additions and 7 deletions
+35
View File
@@ -0,0 +1,35 @@
import { mkdir, rm, writeFile } from 'node:fs/promises';
import path from 'node:path';
import { assertContainedRealPath } from './path-containment';
export type MaterializedSecret = { name: string; value: string };
// Format matches the historical `NAME="value"` env-file layout the agent CLIs
// source; values are JSON-quoted so newlines/quotes survive.
export const formatMaterializedEnv = (secrets: MaterializedSecret[]) =>
`${secrets
.map((secret) => `${secret.name}=${JSON.stringify(secret.value)}`)
.join('\n')}\n`;
// Resolves the contained env-file path inside the checkout and writes it 0600
// so plaintext secrets never sit world/group-readable in the persistent home.
export const writeMaterializedEnv = async (
repoDir: string,
envFilePath: string,
secrets: MaterializedSecret[],
): Promise<string> => {
const envPath = await assertContainedRealPath(repoDir, envFilePath, {
forWrite: true,
});
await mkdir(path.dirname(envPath), { recursive: true });
await writeFile(envPath, formatMaterializedEnv(secrets), { mode: 0o600 });
return envPath;
};
// Removes a previously materialized env file; no-ops when the path is unset or
// already gone so it is safe on every teardown path.
export const removeMaterializedEnv = async (envPath: string | undefined) => {
if (!envPath) return;
await rm(envPath, { force: true });
};
+10 -7
View File
@@ -30,6 +30,7 @@ import {
} from './git';
import { getInstallationToken, openDraftPullRequest } from './github';
import { createHeartbeatController } from './heartbeat-controller';
import { removeMaterializedEnv, writeMaterializedEnv } from './materialize-env';
import { assertContainedRealPath } from './path-containment';
import { createRedactor, truncate } from './redact';
import {
@@ -50,6 +51,8 @@ type ActiveWorkspace = AdapterWorkspace & {
cancelRequested?: boolean;
resolveTurn?: () => void;
lastRecordedDiffSignature?: string;
// Absolute path of the last materialized env file so teardown can delete it.
materializedEnvPath?: string;
};
type FileTreeNode = {
@@ -717,16 +720,11 @@ const recordChangedFiles = async (
const materializeEnvFile = async (workspace: ActiveWorkspace) => {
const { claim, repoDir } = workspace;
if (!claim.job.materializeEnvFile || !claim.job.envFilePath) return;
const envPath = await assertContainedRealPath(
workspace.materializedEnvPath = await writeMaterializedEnv(
repoDir,
claim.job.envFilePath,
{ forWrite: true },
claim.secrets,
);
await mkdir(path.dirname(envPath), { recursive: true });
const content = `${claim.secrets
.map((secret) => `${secret.name}=${JSON.stringify(secret.value)}`)
.join('\n')}\n`;
await writeFile(envPath, content);
await appendEvent(
claim.job._id,
'info',
@@ -1194,6 +1192,9 @@ export const sendWorkspaceMessage = async (
if (workspace.agentTurnActive) {
throw new Error('Wait for the current agent turn to finish or abort it.');
}
// Re-materialize the env file fresh each turn so it is present for the
// adapter and can be removed between turns / on teardown.
await materializeEnvFile(workspace);
if (options.recordUserMessage ?? true) {
await appendMessage({
jobId: claim.job._id,
@@ -1376,6 +1377,8 @@ const teardownActiveWorkspace = async (args: {
abortAgent: args.abortAgent
? async () => await abortWorkspaceAgentForWorkspace(workspace)
: undefined,
removeMaterializedEnv: async () =>
await removeMaterializedEnv(workspace.materializedEnvPath),
markStopped: async () =>
await markWorkspaceStopped(workspace.claim.job._id, args.workspaceStatus),
release: () => workspace.boxHandle.release(),
@@ -5,6 +5,7 @@ export const teardownWorkspace = async (steps: {
removeActive: TeardownStep;
appendEvent?: TeardownStep;
abortAgent?: TeardownStep;
removeMaterializedEnv?: TeardownStep;
markStopped?: TeardownStep;
closeAgent?: TeardownStep;
stopContainer?: TeardownStep;
@@ -27,6 +28,7 @@ export const teardownWorkspace = async (steps: {
await Promise.all(immediate);
await call(steps.appendEvent);
await call(steps.abortAgent);
await call(steps.removeMaterializedEnv);
await call(steps.markStopped);
await call(steps.closeAgent);
await call(steps.stopContainer);