|
| 1 | +--- |
| 2 | +id: "S-20260316-R119" |
| 3 | +title: "REQ-119: gitvault run/materialize support config-referenced sealed-source files" |
| 4 | +status: "active" |
| 5 | +owners: ["@aheissenberger"] |
| 6 | +mode: ["cli"] |
| 7 | +platforms: ["Linux", "macOS", "Windows"] |
| 8 | +scope: |
| 9 | + repoAreas: ["src/**", "docs/**", "specs/**"] |
| 10 | + touch: |
| 11 | + - "src/config.rs" |
| 12 | + - "src/repo/paths.rs" |
| 13 | + - "src/commands/effects.rs" |
| 14 | + - "src/commands/materialize.rs" |
| 15 | + - "src/commands/run_cmd.rs" |
| 16 | + - "src/structured/fields.rs" |
| 17 | + - "src/structured/env_values.rs" |
| 18 | + - "docs/reference.md" |
| 19 | + - "README.md" |
| 20 | + - "specs/2026-03-04-engineering-nfr/req-118.md" |
| 21 | + avoid: ["target/**"] |
| 22 | +breaking: false |
| 23 | +supersedes: [] |
| 24 | +acceptance: |
| 25 | + - id: "AC1" |
| 26 | + text: "`gitvault run` and `gitvault materialize` support two input source classes selected by unified runtime rules: (a) existing store artifacts under `.gitvault/store/<env>/**/*.age`, and (b) repository files containing sealed values. Both classes may be used in the same execution." |
| 27 | + - id: "AC2" |
| 28 | + text: "`[[run.rule]]` and `[[materialize.rule]]` are extended with optional `source = \"store\" | \"sealed\"`. Omitted `source` defaults to `store` for backward compatibility. Unknown `source` values fail config parsing with a usage error." |
| 29 | + - id: "AC3" |
| 30 | + text: "For `source = \"sealed\"`, `path` is matched against repository-relative working-tree paths (not store paths). Supported file formats are the same sealed formats as REQ-112: `.env`, `.env.<suffix>`, `<name>.env`, `.json`, `.yaml`, `.yml`, `.toml` (excluding `.envrc`)." |
| 31 | + - id: "AC4" |
| 32 | + text: "When a selected sealed-source file contains a mix of plaintext and sealed values, `gitvault run` and `gitvault materialize` emit final plaintext key/value pairs for both: plaintext values pass through unchanged; sealed values are decrypted in memory before flattening/export." |
| 33 | + - id: "AC5" |
| 34 | + text: "For `gitvault run`, `source = \"sealed\"` never writes plaintext files. Decryption, parsing, and flattening happen in memory only. REQ-21 and REQ-22 fileless guarantees remain unchanged." |
| 35 | + - id: "AC6" |
| 36 | + text: "Runtime `keys` filters and runtime prefix controls (`dir_prefix`, `path_prefix`, `custom_prefix`) continue to apply for both source classes in `run` and `materialize` using REQ-118 matcher semantics and deterministic prefix composition order." |
| 37 | + - id: "AC7" |
| 38 | + text: "Rule precedence remains REQ-118 compliant (file order, later match wins). For key collisions after flattening/prefixing, merge order is deterministic and documented: later processed entries override earlier entries; processing order is stable across repeated executions with identical inputs." |
| 39 | + - id: "AC8" |
| 40 | + text: "If a selected sealed-source file cannot be parsed, contains unsupported format, or contains an undecryptable sealed token, `gitvault run` and `gitvault materialize` fail closed with a non-zero exit and an actionable error that includes the file path context." |
| 41 | + - id: "AC9" |
| 42 | + text: "Existing run/materialize behavior for store sources is preserved unless `source = \"sealed\"` is explicitly configured. Existing configs that do not use the new field remain valid and behaviorally unchanged." |
| 43 | + - id: "AC10" |
| 44 | + text: "Tests cover: mixed plaintext+sealed `.env`; sealed JSON/YAML/TOML flattening; combined store+sealed sources in one run/materialize execution; deterministic collision handling; clear-env/keep-vars unchanged; fail-closed errors for bad sealed payloads and unsupported sealed-source files." |
| 45 | +verification: |
| 46 | + commands: |
| 47 | + - "cargo fmt --all -- --check" |
| 48 | + - "cargo clippy --all-targets --workspace -- -D warnings" |
| 49 | + - "cargo test --all" |
| 50 | + - "cargo xtask spec-verify" |
| 51 | +risk: |
| 52 | + level: "medium" |
| 53 | +links: |
| 54 | + related: "S-20260301-006" |
| 55 | + related2: "S-20260304-R112" |
| 56 | + related3: "S-20260305-R118" |
| 57 | + issue: "" |
| 58 | + pr: "" |
| 59 | +--- |
| 60 | + |
| 61 | +# REQ-119: `gitvault run`/`materialize` support config-referenced sealed-source files |
| 62 | + |
| 63 | +## Context |
| 64 | + |
| 65 | +Current runtime loading is centered on decrypting store artifacts. Teams also keep |
| 66 | +repository files with in-place sealed values and want to reference those files from |
| 67 | +runtime configuration without introducing intermediate plaintext files. |
| 68 | + |
| 69 | +REQ-118 already defines the unified rule engine and runtime key-prefix behavior. |
| 70 | +REQ-119 extends that model so runtime source selection can include sealed working-tree |
| 71 | +files for `run` and `materialize` while keeping each command's security guarantees. |
| 72 | + |
| 73 | +## Goal |
| 74 | + |
| 75 | +Allow `gitvault run` and `gitvault materialize` to load values from both store |
| 76 | +artifacts and sealed repository files through unified runtime rule configuration, |
| 77 | +including files that contain a |
| 78 | +mix of plaintext and sealed values. |
| 79 | + |
| 80 | +## Non-goals |
| 81 | + |
| 82 | +- No new CLI subcommand for source selection. |
| 83 | +- No reintroduction of legacy matcher schemas. |
| 84 | +- No plaintext materialization to disk. |
| 85 | +- No change to seal/unseal command semantics. |
| 86 | +- No change to the materialize output contract (`.env` generation remains explicit materialization behavior). |
| 87 | + |
| 88 | +## Constraints |
| 89 | + |
| 90 | +- Keep REQ-118 as the governing rule model; extend it additively. |
| 91 | +- Maintain deterministic merge behavior and strict config parsing. |
| 92 | +- Preserve existing store-only run/materialize behavior by default. |
| 93 | + |
| 94 | +## Decision |
| 95 | + |
| 96 | +Extend `[[run.rule]]` and `[[materialize.rule]]` with optional `source`: |
| 97 | + |
| 98 | +```toml |
| 99 | +[[run.rule]] |
| 100 | +action = "allow" |
| 101 | +source = "sealed" |
| 102 | +path = "services/api/.env.local" |
| 103 | +keys = ["DATABASE_URL", "API_*"] |
| 104 | + |
| 105 | +[[run.rule]] |
| 106 | +action = "allow" |
| 107 | +source = "store" |
| 108 | +path = ".gitvault/store/prod/services/api/runtime.json.age" |
| 109 | +path_prefix = true |
| 110 | +custom_prefix = "RUNTIME" |
| 111 | + |
| 112 | +[[materialize.rule]] |
| 113 | +action = "allow" |
| 114 | +source = "sealed" |
| 115 | +path = "services/web/.env.local" |
| 116 | +path_prefix = true |
| 117 | +custom_prefix = "MAT" |
| 118 | +``` |
| 119 | + |
| 120 | +When `source` is omitted, behavior is identical to existing store-source behavior. |
| 121 | + |
| 122 | +## Requirement Coverage |
| 123 | + |
| 124 | +- REQ-119. |
| 125 | + |
| 126 | +## Conflict Analysis |
| 127 | + |
| 128 | +| Existing requirement | Potential conflict | Resolution | |
| 129 | +|---|---|---| |
| 130 | +| REQ-21/22 (fileless run) | run sealed-source loading could imply temp files | Explicitly disallow file writes in AC5 | |
| 131 | +| REQ-112 (seal semantics) | runtime could redefine sealed format behavior | Reuse REQ-112 sealed formats and token handling rules | |
| 132 | +| REQ-118 AC2 (runtime rule keys) | schema extension might contradict strict keys | Extend AC2 additively with optional `source`; keep strict validation | |
| 133 | +| REQ-118 AC7 (order) | two source classes add merge ambiguity | AC7 defines deterministic collision precedence | |
| 134 | + |
| 135 | +No requirement is superseded by REQ-119. This is an additive extension. |
| 136 | + |
| 137 | +## Acceptance Criteria |
| 138 | + |
| 139 | +See frontmatter acceptance list (AC1-AC10). |
| 140 | + |
| 141 | +## Test Plan |
| 142 | + |
| 143 | +- Unit tests for run/materialize rule parsing with `source` defaulting and validation. |
| 144 | +- Integration tests covering mixed store+sealed runtime loading and error paths. |
| 145 | +- Regression tests to prove unchanged behavior for legacy configs without `source`. |
| 146 | + |
| 147 | +## Notes |
| 148 | + |
| 149 | +This requirement intentionally keeps policy expression inside the existing unified |
| 150 | +rule engine instead of introducing command flags or parallel config sections. |
0 commit comments