Add failing tests for run-spec redaction corruption

The entropy redactor rewrites NAME=<hex> assignment pairs to a bare
REDACTED, and the worker rehydrates its executable RunSpec from the
projection folded from redacted stored events. Together these broke
Daytona snapshot builds for any run definition whose inline Dockerfile
pins a git SHA: the spec came back as `ARG REDACTED`, the build died on
the unset variable under `set -eu`, and the environment's snapshot
identity silently changed.

Pin the intended contracts with red tests:

- fabro-redact: an assignment whose value alone is below the entropy
  threshold survives redaction (pure hex cannot exceed 4.0 bits; only
  the name+value charset merge crosses 4.5), and a genuinely
  high-entropy value is redacted without destroying the key name.
- fabro-workflow: the spec that load_from_store rehydrates round-trips
  byte-identical through the store, including content that looks like
  a secret — event redaction must not reach execution.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-08-06 21:10:49 -04:00
parent 0abf2297c0
commit 4e24dcb68a
No known key found for this signature in database
2 changed files with 65 additions and 0 deletions

View file

@ -272,6 +272,50 @@ mod tests {
assert!(loaded.diagnostics().is_empty());
}
#[tokio::test]
async fn load_from_store_preserves_high_entropy_dockerfile_content() {
// The spec the worker executes must survive the store byte-identical.
// Event redaction is a storage/display concern; when it reaches the
// spec that `load_from_store` rehydrates, the sandbox builds a
// corrupted Dockerfile: `ARG NAME=<hex>` pairs come back as
// `ARG REDACTED`, the build's `set -eu` step fails on the unset
// variable, and the environment's snapshot identity silently changes.
let temp = tempfile::tempdir().unwrap();
let run_dir = temp.path().join("run");
std::fs::create_dir_all(&run_dir).unwrap();
let (graph, source) = graph_and_source();
// Two shapes that must both survive: the hex pins that triggered the
// production failure, and a token high-entropy enough that any
// detector will keep flagging it in stored events. The second keeps
// this test red until execution stops reading redacted content,
// independent of how the entropy heuristic evolves.
let dockerfile = "FROM buildpack-deps:noble\n\
ARG DOCKER_INSTALL_COMMIT=5ce20f2eef3615d08fea941eda5a109e949e8ebf\n\
ARG DOCKER_INSTALL_SHA256=b991f2806186f7287bb9e53362060c382e906d154599b2fb0982f34246bacfd4\n\
ENV CACHE_SALT=xK9mZ2vL8nQ5rT1wY4bC7dF0gH3jE6p\n\
RUN install-docker \"${DOCKER_INSTALL_COMMIT}\" \"${DOCKER_INSTALL_SHA256}\"\n";
let mut record = sample_record(different_graph());
record.graph = graph;
record.settings.run.environment.image.dockerfile = Some(
fabro_types::settings::run::DockerfileSource::Inline(dockerfile.to_string()),
);
let run_store = seeded_store(&record, Some(&source)).await;
let loaded = load_from_store(&run_store.clone().into(), &run_dir)
.await
.unwrap();
assert_eq!(
loaded.run_spec().settings.run.environment.image.dockerfile,
Some(fabro_types::settings::run::DockerfileSource::Inline(
dockerfile.to_string()
)),
"the executable run spec must round-trip through the store unredacted"
);
}
#[test]
fn persist_returns_error_on_io_failure() {
let temp = tempfile::tempdir().unwrap();

View file

@ -111,6 +111,27 @@ mod tests {
assert_eq!(result, "key=REDACTED");
}
#[test]
fn redact_string_keeps_assignment_with_low_entropy_value() {
// A pinned git SHA is pure hex, so the value alone can never exceed
// 4.0 bits of entropy. Only the merged NAME=value token crosses the
// 4.5-bit threshold, because the uppercase name widens the charset.
// Measuring the name together with the value redacts innocuous
// pins; the pair must survive.
let input = "ARG DOCKER_INSTALL_COMMIT=5ce20f2eef3615d08fea941eda5a109e949e8ebf";
assert_eq!(redact_string(input), input);
}
#[test]
fn redact_string_keeps_assignment_key_for_high_entropy_value() {
// The value alone is above the entropy threshold, so it is
// redacted either way — but the name says which setting was
// redacted and must survive, as the gitleaks layer already
// does for `key=REDACTED`.
let result = redact_string("BUILD_STAMP=xK9mZ2vL8nQ5rT1wY4bC7dF0gH3jE6p");
assert_eq!(result, "BUILD_STAMP=REDACTED");
}
#[test]
fn redact_string_overlapping_detections_produce_single_redacted() {
// A high-entropy string that also matches a gitleaks pattern