404 lines
18 KiB
Markdown
404 lines
18 KiB
Markdown
# ADR-0006 Findings 1–3 Remediation Plan
|
||
|
||
This document is the executable implementation plan for correcting Findings 1,
|
||
2, and 3 from the post-implementation review of ADR-0006:
|
||
|
||
1. unredacted resolved-pipeline data in the debug summary;
|
||
2. collision-prone output run directories; and
|
||
3. missing failure `run-report.json` artifacts.
|
||
|
||
Finding 4—the broad loss of CLI and configuration regression coverage—is
|
||
explicitly deferred. The focused regression tests required to prove these three
|
||
fixes are in scope; restoring or redesigning the general test suites is not.
|
||
|
||
The audience is an LLM coding agent. Implement the stages in order. Each stage
|
||
must finish with a compiling, tested repository.
|
||
|
||
## Execution Rules
|
||
|
||
Before every stage:
|
||
|
||
1. read [Development](../development.md), both documents under `docs/policy/`,
|
||
[ADR-0006](../adr/0006-separate-output-cache-and-debug-state.md), this plan,
|
||
and every stage-specific document listed below;
|
||
2. inspect `git status` and preserve user changes;
|
||
3. inspect the focused implementation and tests before editing; and
|
||
4. confirm all earlier stages are complete.
|
||
|
||
During every stage:
|
||
|
||
- keep the CLI as the run-identity and physical-path composition root;
|
||
- preserve durable logical output schemas, checkpoint identity and wire
|
||
compatibility, chunk-plan behavior, debug layout, exit codes, and current
|
||
configuration contracts unless this plan expressly changes them;
|
||
- never allow a secondary debug persistence error to hide the primary command
|
||
error;
|
||
- add focused tests for the changed behavior without expanding into Finding 4;
|
||
and
|
||
- update a canonical current-behavior document only if the implemented contract
|
||
changes or its existing statement is inaccurate.
|
||
|
||
At the end of every stage:
|
||
|
||
1. run the focused tests listed for that stage;
|
||
2. run `go test ./...`;
|
||
3. run `go vet ./...`;
|
||
4. run `go build ./cmd/notarius`;
|
||
5. run `git diff --check`; and
|
||
6. mark the stage complete here only after all checks pass.
|
||
|
||
If repository reality conflicts with this plan, update the plan before making a
|
||
materially different design choice. Do not silently weaken redaction, output
|
||
exclusivity, or failure reporting.
|
||
|
||
## Fixed Decisions
|
||
|
||
### Redacted summary boundary
|
||
|
||
- Every file under `summary/` is a redacted artifact. The fact that the adjacent
|
||
`trace/` may contain application data does not relax this boundary.
|
||
- `resolved-pipeline.json` retains its current logical shape and useful module,
|
||
validator, digest, capability, and reference-provenance data, but all module
|
||
and validator option maps pass through the same recursive sensitive-key
|
||
redaction used by `effective-config.json`.
|
||
- The debug-bundle writer must not accept an arbitrary resolved pipeline for a
|
||
method whose name promises redacted summary output. Introduce a narrow
|
||
payload interface, analogous to `RedactedSummaryPayload`, that can only be
|
||
satisfied by a producer responsible for redaction. `config.EffectiveConfig`
|
||
implements it and returns a deep-cloned, redacted
|
||
`pipeline.ResolvedPipeline`.
|
||
- The CLI passes the effective configuration to that method, not
|
||
`effective.ResolvedPipeline` directly. Do not expose the private redaction
|
||
helper merely to keep the unsafe call shape.
|
||
- Redaction must cover input, chunk, output, lane extract/merge/normalize,
|
||
resolved validator-chain bindings, deprecated lane validator bindings, and
|
||
recursively nested option maps and lists. Safe option values remain intact.
|
||
- Reference contents remain excluded through the existing `json:"-"`
|
||
`ReferenceSet` contract. Reference provenance and configured paths may remain
|
||
in the summary.
|
||
- Credentials and sensitive values must not be copied into errors while testing
|
||
this behavior.
|
||
|
||
### Run identity and output allocation
|
||
|
||
- The CLI owns one run ID before allocating debug or executing the pipeline.
|
||
Output, debug, manifests, metadata, stdout, and stderr use that same ID.
|
||
- The default ID format becomes:
|
||
|
||
```text
|
||
run-<started-at-unix-nanoseconds>-<32-lowercase-hex-characters>
|
||
```
|
||
|
||
The suffix is 16 bytes from `crypto/rand`. Time preserves useful ordering;
|
||
randomness makes IDs collision-resistant across processes and hosts sharing a
|
||
root.
|
||
- Add an injectable `RunIDGenerator func(time.Time) (string, error)` to
|
||
`internal/cli.Options`. The production default uses `crypto/rand.Reader`.
|
||
Tests inject deterministic valid IDs. Do not retain package-global clocks or
|
||
random sources for this path.
|
||
- Generate the ID after configuration and CLI validation but before debug
|
||
allocation and pipeline resolution. A generation failure is a normal command
|
||
failure with exit code `1` and cannot have a debug bundle because no run
|
||
identity exists yet.
|
||
- Change debug allocation to accept the CLI-generated run ID and `startedAt`.
|
||
It validates that the ID is one safe path component and creates that exact
|
||
bundle with `os.Mkdir`, failing on an existing entry. Debug no longer creates
|
||
a different ID from its own clock or retries by changing identity.
|
||
- Durable output allocation is exclusive. Validate every logical output name
|
||
first, create the output parent as needed, then create
|
||
`<output-root>/<run-id>` with `os.Mkdir`, not `os.MkdirAll`. An existing run
|
||
directory is an error; Notarius never writes into it.
|
||
- Nested logical output directories may still use `MkdirAll` after the exclusive
|
||
run directory is owned by the invocation. File writes remain atomic.
|
||
- If output allocation or a later output write fails, retain any newly created
|
||
partial run directory for inspection. Never remove or overwrite an existing
|
||
run directory. Document this only if current Operations wording needs
|
||
clarification.
|
||
- Keep output directory permissions and logical file permissions unchanged;
|
||
this remediation addresses identity and exclusivity, not the public output
|
||
permission contract.
|
||
|
||
### Terminal debug reports
|
||
|
||
- Every debug-enabled invocation whose bundle was successfully allocated makes
|
||
one best-effort attempt to write a terminal `run-report.json`, whether the
|
||
command succeeds or fails.
|
||
- A failure report uses `Succeeded: false`. Populate fields only from state that
|
||
is known at the failure point:
|
||
- `run_id`, requested `pipeline_id`, and `debug_path` are always known after
|
||
bundle allocation;
|
||
- `output_path` is included once the effective output root and run ID are
|
||
known;
|
||
- output, rejection, warning, and validation fields are populated when a
|
||
`pipeline.RunOutput` is available; otherwise their zero values are retained.
|
||
- `error.log` remains the human-readable primary error artifact. Do not add the
|
||
error string to `RunReport`; this avoids a second unstructured error surface
|
||
and preserves the existing report schema.
|
||
- Introduce one CLI-owned run-state value that accumulates terminal-report data
|
||
as resolution and execution progress. Success and failure reporting both
|
||
derive `debugbundle.RunReport` from this value rather than constructing
|
||
unrelated literals.
|
||
- Add a single summary terminalization operation that attempts
|
||
`run-report.json` and, on failure, `error.log`. On a successful command it
|
||
writes only the success report. On a failed command it writes the failure
|
||
report and error log, attempting both even if one write fails and joining
|
||
secondary persistence errors for stderr reporting.
|
||
- Terminalization is invoked at most once. If the primary failure is itself a
|
||
summary persistence failure, do not recursively retry the failed operation;
|
||
make one terminalization attempt that excludes the artifact already known to
|
||
have failed. In particular, a failure to write `run-report.json` must never
|
||
call itself again.
|
||
- Stderr prints the primary error first, then any terminalization error, then
|
||
the allocated debug path. Exit code remains `1`. A secondary error never
|
||
replaces or wraps away the primary error.
|
||
- Runs without debug perform no summary work and retain current concise stderr
|
||
behavior.
|
||
|
||
## Stage 1: Enforce redaction for resolved-pipeline summaries
|
||
|
||
**Status:** Not started
|
||
|
||
### Objective
|
||
|
||
Close the summary credential leak and make the redacted-payload boundary
|
||
difficult to bypass accidentally.
|
||
|
||
### Read first
|
||
|
||
- `internal/core/config/redaction.go`
|
||
- `internal/core/config/effective_config.go`
|
||
- `internal/core/debugbundle/summary.go`
|
||
- the summary-writing section of `internal/cli/run.go`
|
||
- `internal/core/config/v3_test.go`
|
||
- `internal/cli/state_hardening_test.go`
|
||
- [Architecture: State, Output, And Safety](../policy/architecture.md#state-output-and-safety)
|
||
|
||
### Implement
|
||
|
||
1. Add the resolved-pipeline redacted payload interface to `debugbundle` and
|
||
change `SummaryWriter.WriteResolvedPipeline` to accept it rather than `any`.
|
||
2. Implement the interface on `config.EffectiveConfig` using the existing deep
|
||
clone/redaction path.
|
||
3. Audit recursive option redaction. Extend it only where nested list/map values
|
||
can currently retain a value under a sensitive key.
|
||
4. Change the CLI call to pass `effective` through the redacted interface.
|
||
5. Keep `effective-config.json` behavior and `resolved-pipeline.json` logical
|
||
structure unchanged apart from required redaction.
|
||
|
||
### Focused tests
|
||
|
||
- Unit-test a resolved pipeline containing distinct sentinel secrets in every
|
||
module and validator binding listed under Fixed Decisions.
|
||
- Include nested maps and lists, safe neighboring values, and materialized
|
||
reference content. Assert secrets and reference contents are absent, redaction
|
||
markers are present, and safe provenance remains.
|
||
- Run a debug-enabled CLI invocation whose accepted fake module options include
|
||
sensitive keys. Inspect both `effective-config.json` and
|
||
`resolved-pipeline.json` and prove neither contains the sentinels.
|
||
- Assert mutation of a redacted payload cannot alter the effective configuration
|
||
or resolved pipeline.
|
||
|
||
Run at minimum:
|
||
|
||
```sh
|
||
go test ./internal/core/config ./internal/core/debugbundle ./internal/cli
|
||
```
|
||
|
||
### Exit criteria
|
||
|
||
No arbitrary resolved pipeline can be passed to the redacted summary writer;
|
||
all resolved bindings are recursively redacted; focused leak tests fail against
|
||
the old implementation and pass against the new one; and all repository checks
|
||
pass.
|
||
|
||
## Stage 2: Make run identity collision-resistant and output exclusive
|
||
|
||
**Status:** Not started
|
||
|
||
### Objective
|
||
|
||
Ensure separate invocations can never silently share or overwrite one durable
|
||
output run directory.
|
||
|
||
### Read first
|
||
|
||
- run initialization and output writing in `internal/cli/run.go`
|
||
- `internal/core/debugbundle/bundle.go`
|
||
- `internal/core/debugbundle/bundle_test.go`
|
||
- `internal/cli/state_surfaces_test.go`
|
||
- `internal/cli/state_hardening_test.go`
|
||
- [Operations: Output](../operations.md#output)
|
||
|
||
### Implement
|
||
|
||
1. Add the default cryptographic run-ID generator and inject it through
|
||
`cli.Options` as specified above. Validate generated IDs before any path use.
|
||
2. Generate one ID from `startedAt` before debug allocation. Remove the debug
|
||
allocator's package-global clock and identity generation.
|
||
3. Refactor `debugbundle.Allocate` to accept the run ID and start time and
|
||
exclusively create that exact bundle.
|
||
4. Refactor output writing so the run directory is created exclusively after
|
||
all logical file names validate. Return a contextual collision error without
|
||
touching the existing directory.
|
||
5. Preserve atomic per-file writes, partial-new-directory behavior on later
|
||
write failures, and the single ID across manifests and reported paths.
|
||
6. Update existing focused tests to inject deterministic run IDs rather than
|
||
relying on timestamps or the debug allocator's global clock.
|
||
|
||
### Focused tests
|
||
|
||
- Verify the production generator's format and validate many generated IDs for
|
||
uniqueness and path safety without asserting random bytes.
|
||
- Inject a deterministic ID and prove debug and output use the exact same run
|
||
directory name and manifest run ID.
|
||
- Pre-create the target output run directory with sentinel files. Run the
|
||
command and assert exit `1`, a collision error, and byte-for-byte preservation
|
||
of the existing tree.
|
||
- Execute two invocations with the same injected ID and root. The first may
|
||
succeed; the second must fail without changing the first output.
|
||
- Pre-create the target debug bundle and assert allocation fails without
|
||
changing it.
|
||
- Prove nested logical output paths still work and unsafe logical paths are
|
||
rejected before the run directory is created.
|
||
- Prove a later output-file failure retains only the newly allocated partial
|
||
directory and never affects a sibling run.
|
||
|
||
Run at minimum:
|
||
|
||
```sh
|
||
go test ./internal/core/debugbundle ./internal/cli
|
||
```
|
||
|
||
### Exit criteria
|
||
|
||
Every invocation has one collision-resistant ID, output and debug allocation
|
||
are exclusive, an existing run directory is never reused or overwritten, and
|
||
all repository checks pass.
|
||
|
||
## Stage 3: Write terminal run reports for failures
|
||
|
||
**Status:** Not started
|
||
|
||
### Objective
|
||
|
||
Make every allocated debug summary record a structured terminal outcome while
|
||
preserving primary errors and preventing recursive persistence failures.
|
||
|
||
### Read first
|
||
|
||
- `internal/core/debugbundle/summary.go`
|
||
- failure and summary paths in `internal/cli/run.go`
|
||
- `internal/cli/state_hardening_test.go`
|
||
- [Run State Internals](../internal/state.md)
|
||
- [Operations: Debug Bundles](../operations.md#debug-bundles)
|
||
- [Operations: Failures And Warnings](../operations.md#failures-and-warnings)
|
||
|
||
### Implement
|
||
|
||
1. Add the CLI-owned run-state/report builder described under Fixed Decisions.
|
||
Initialize it immediately after run-ID generation and update it as pipeline
|
||
identity, output path, and `RunOutput` become available.
|
||
2. Add a debug-summary terminalization method or focused helper that writes the
|
||
success report, or the failure report plus error log, exactly once.
|
||
3. Route every post-allocation failure through the common terminalization path:
|
||
catalog and reference resolution, pipeline resolution, profile validation,
|
||
reference materialization, registry and LLM construction, preparation, input
|
||
read, cache/checkpoint construction, pipeline execution, summary writes,
|
||
output allocation, and output writes.
|
||
4. Replace the existing success-only report literal with the same report
|
||
builder and terminalization path.
|
||
5. Preserve failures before run-ID generation or debug allocation as stderr-only
|
||
failures.
|
||
6. Keep explicit guards for failures caused by summary terminalization itself so
|
||
the command never recursively rewrites `run-report.json` or `error.log`.
|
||
|
||
### Focused tests
|
||
|
||
- For resolution, pipeline, and output-write failures, assert both
|
||
`run-report.json` and `error.log` exist, `succeeded` is false, and every known
|
||
field is correct.
|
||
- For a pipeline failure with partial `RunOutput`, assert available counts,
|
||
validation status, manifest, warnings, checkpoint events, and chunk-plan
|
||
summary are retained.
|
||
- Assert a success report uses `succeeded: true` and contains the same paths and
|
||
counts printed by the CLI.
|
||
- Inject independent failures for the run-report writer and error-log writer.
|
||
Assert each operation is attempted no more than once, the primary error is
|
||
printed first, the secondary error is reported, the debug path is printed,
|
||
and exit code is `1`.
|
||
- Assert a pre-allocation configuration failure creates no report or debug root.
|
||
- Assert a non-debug failure performs no summary writes.
|
||
|
||
Run at minimum:
|
||
|
||
```sh
|
||
go test ./internal/core/debugbundle ./internal/cli
|
||
```
|
||
|
||
### Exit criteria
|
||
|
||
Every successfully allocated debug bundle has a best-effort terminal report;
|
||
ordinary failures retain both structured and textual outcomes; summary failures
|
||
cannot recurse or mask the primary error; and all repository checks pass.
|
||
|
||
## Stage 4: Final audit and close the remediation plan
|
||
|
||
**Status:** Not started
|
||
|
||
### Objective
|
||
|
||
Verify the three findings are closed, align any affected current-behavior
|
||
documentation, and remove this completed planning artifact.
|
||
|
||
### Read first
|
||
|
||
- the final code and tests from Stages 1–3
|
||
- [Documentation Policy](../policy/documentation.md)
|
||
- [Architecture Policy](../policy/architecture.md)
|
||
- [CLI Reference](../cli.md)
|
||
- [Operations](../operations.md)
|
||
- [Run State Internals](../internal/state.md)
|
||
|
||
### Implement
|
||
|
||
1. Audit every `summary/` writer call and confirm each accepts or constructs a
|
||
redacted payload. Search for direct serialization of raw effective pipeline
|
||
bindings into summary files.
|
||
2. Audit every output path and confirm no code writes into a pre-existing run
|
||
directory. Confirm debug and output use one CLI-owned ID.
|
||
3. Audit every post-allocation return path and confirm it reaches terminal
|
||
reporting exactly once or is itself an explicitly guarded terminalization
|
||
failure.
|
||
4. Update Operations or Run State Internals only where the final implementation
|
||
adds a useful current-behavior clarification about run IDs, exclusive output
|
||
allocation, partial output after failure, or best-effort failure reports.
|
||
Do not duplicate volatile CLI or configuration contracts.
|
||
5. Confirm Finding 4 remains deferred. Do not restore unrelated deleted tests,
|
||
introduce a coverage threshold, or add a legacy-checkpoint fixture in this
|
||
remediation.
|
||
6. After all validation succeeds and documentation is current, delete this
|
||
implementation plan. The accepted ADR and canonical current-behavior
|
||
documents remain authoritative.
|
||
|
||
### Validation
|
||
|
||
Run:
|
||
|
||
```sh
|
||
go test ./...
|
||
go vet ./...
|
||
go build ./cmd/notarius
|
||
git diff --check
|
||
```
|
||
|
||
Manually inspect the changed debug artifacts from one success, one pipeline
|
||
failure, and one output collision. Confirm no test fixture contains real
|
||
credentials or private data.
|
||
|
||
### Exit criteria
|
||
|
||
Findings 1–3 are closed with focused regression tests; durable output cannot be
|
||
overwritten by a run-ID collision; failure bundles have terminal reports;
|
||
summary redaction is enforced by contract; documentation is accurate; Finding
|
||
4 remains deferred; the plan is removed; and all checks pass.
|