mirror of
https://github.com/morluto/rea
synced 2026-10-30 07:39:58 -04:00
fix(environment): write exported capture copies readable by their owner only
The bundle's manifest.json and evidence.json are copies of captured content, which can include values an application returned such as session tokens. They are now written with mode 0600 regardless of the caller's umask. The Dockerfile already copies them with --chown for the runtime user, so the served environment can still read them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+2
-1
@@ -377,7 +377,8 @@ rea environment-export manifest.json --evidence capture.json ./grafana-example -
|
||||
|
||||
The folder holds a `README.md` with the bundle's provenance, an env folder
|
||||
`envs/<name>/` with a `Dockerfile` that installs this exact `rea-agents`
|
||||
version and serves the copied `manifest.json` and `evidence.json` with
|
||||
version and serves the copied `manifest.json` and `evidence.json` (written
|
||||
readable by their owner only, since they hold captured content) with
|
||||
`environment-serve`, a Python verifier under `artifacts/check_<name>/` and the
|
||||
smoke task `tasks/smoke.json`. `--package-tarball` copies a packed `rea-agents`
|
||||
tarball into the env folder and installs it instead of the registry package,
|
||||
|
||||
@@ -30,7 +30,8 @@ previous increment.
|
||||
nothing is ever overwritten.
|
||||
- Write `README.md`, `envs/<name>/Dockerfile`, `envs/<name>/env.toml`,
|
||||
`envs/<name>/manifest.json` and `envs/<name>/evidence.json` as byte-exact
|
||||
copies of the inputs, `artifacts/check_<name>/verify.py` and
|
||||
copies of the inputs (mode `0600`, so captured content stays readable by its
|
||||
owner only whatever the umask), `artifacts/check_<name>/verify.py` and
|
||||
`tasks/smoke.json`, where `<name>` defaults to the manifest's kebab-case name
|
||||
and `--name` must be a kebab-case label of at most 64 characters.
|
||||
- The Dockerfile reads `FROM node:24.18.0-bookworm-slim`, sets
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
||||
import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import * as v8 from "node:v8";
|
||||
|
||||
@@ -251,3 +251,22 @@ describe("exportEnvironmentBundle", () => {
|
||||
expect(await listFiles(directory)).toHaveLength(7);
|
||||
});
|
||||
});
|
||||
|
||||
describe("exportEnvironmentBundle file modes", () => {
|
||||
it.skipIf(process.platform === "win32")(
|
||||
"writes the captured manifest and Evidence copies readable by their owner only",
|
||||
async () => {
|
||||
const { input, directory } = await prepare(
|
||||
"rea-environment-export-mode-",
|
||||
);
|
||||
const exported = await exportEnvironmentBundle({ ...input, directory });
|
||||
if (!exported.ok) throw exported.error;
|
||||
for (const copy of ["manifest.json", "evidence.json"]) {
|
||||
const { mode } = await stat(
|
||||
join(directory, "envs/grafana-example", copy),
|
||||
);
|
||||
expect(mode & 0o777).toBe(0o600);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -265,15 +265,22 @@ export const readEnvironmentExportInputs = async (
|
||||
return ok({ manifest: manifest.value, evidence: evidence.value });
|
||||
};
|
||||
|
||||
/** Captured content (the manifest and Evidence copies) stays readable by its owner only, whatever the umask. */
|
||||
const CAPTURED_FILE_MODE = 0o600;
|
||||
|
||||
const writeNew = async (
|
||||
root: string,
|
||||
relative: string,
|
||||
content: string | Uint8Array,
|
||||
mode?: number,
|
||||
): Promise<WrittenBundleFile> => {
|
||||
const path = join(root, relative);
|
||||
try {
|
||||
await mkdir(dirname(path), { recursive: true });
|
||||
await writeFile(path, content, { flag: "wx" });
|
||||
await writeFile(path, content, {
|
||||
flag: "wx",
|
||||
...(mode === undefined ? {} : { mode }),
|
||||
});
|
||||
} catch (cause: unknown) {
|
||||
rethrowUnlessDenied(cause, path);
|
||||
}
|
||||
@@ -309,10 +316,20 @@ const writeBundle = async (
|
||||
for (const file of plan.files)
|
||||
written.push(await writeNew(input.directory, file.path, file.content));
|
||||
written.push(
|
||||
await writeNew(input.directory, plan.copies.manifest, input.manifest.bytes),
|
||||
await writeNew(
|
||||
input.directory,
|
||||
plan.copies.manifest,
|
||||
input.manifest.bytes,
|
||||
CAPTURED_FILE_MODE,
|
||||
),
|
||||
);
|
||||
written.push(
|
||||
await writeNew(input.directory, plan.copies.evidence, input.evidence.bytes),
|
||||
await writeNew(
|
||||
input.directory,
|
||||
plan.copies.evidence,
|
||||
input.evidence.bytes,
|
||||
CAPTURED_FILE_MODE,
|
||||
),
|
||||
);
|
||||
if (plan.copies.tarball !== null && plan.packageSource.kind === "tarball")
|
||||
written.push(
|
||||
|
||||
Reference in New Issue
Block a user