feat: first-class encrypted user values, honored at template and apply#222
Conversation
talm apply re-renders config from the modeline at apply time but, unlike
talm template, ignored every user value source on its template-rendering
path: it had no --values / --set* flags and did not read
templateOptions.{valueFiles,values,...} from Chart.yaml. A value supplied
via a value file rendered correctly under template yet was silently
dropped at apply (rendered empty, or a required helper failed), making it
impossible to inject user values that templates consume.
Add the six value-source flags to apply (--values, --set, --set-string,
--set-file, --set-json, --set-literal) and merge Chart.yaml's
templateOptions over them, mirroring template's ordering so CLI flags win
over config defaults. Thread the merged sources into the template-render
apply path (buildApplyRenderOptions). The direct-patch path does not
render chart templates (FullConfigProcess applies the file as a patch
onto a secrets-generated base and never calls loadValues), so value
sources have nothing to render into there and are intentionally left off.
Chart.yaml-declared value files are project-relative, so resolve them
against the detected project root (as withSecrets and template paths
already are) rather than the caller's CWD; CLI --values stay CWD-relative.
Apply the same resolution in template so the two commands point at the
same file.
Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
Operators need to inject arbitrary secret values that chart templates consume (a registry password, a KMS secret-id) while keeping them encrypted at rest in git. talm already encrypts its fixed secret set with the age key in talm.key, but had no way to do the same for user values. Teach loadValues to decrypt a value file named *.encrypted.yaml in memory: a new age.DecryptYAMLToMap reuses the existing per-leaf age envelope (ENC[AGE,data:...]) and the project's talm.key to return the plaintext map without ever writing it to disk. Detection is name-authoritative and content-validated — a *.encrypted.yaml carrying no envelope errors out (ErrNoEncryptedValues) instead of silently rendering as plaintext, and a plaintext value that merely mentions the prefix mid-string is never mistaken for ciphertext (prefix AND suffix). A missing talm.key surfaces the existing recovery hint plus the file name. Wire values-secret.yaml <-> values-secret.encrypted.yaml into init --encrypt/--decrypt and git-ignore the plaintext sibling so the operator has a turnkey way to produce the committed encrypted file and reference it from templateOptions.valueFiles. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Aleksei Sviridkin <f@lex.la>
…les and previews Templates that consume encrypted user values still exposed the rendered secret in three places: 'talm template -I' baked it into nodes/*.yaml, plain 'talm template' printed it to stdout, and 'talm apply --dry-run' printed it in the drift preview — defeating the point of encrypting at rest. Seal values that originate from an encrypted value file out of all three surfaces. 'template -I' omits them structurally: a secret that is a direct map value drops its key (apply's map merge leaves the rendered value authoritative); a secret nested in a sequence element drops the whole element (sequences without a merge key are matched by whole-element deep-equal, so a partial element would be appended as a duplicate at apply). The real value is re-injected only at apply, which re-renders from the encrypted file in memory. Plain 'talm template' to stdout redacts the values to a sentinel by default; --show-secrets prints them verbatim. The apply drift preview previously redacted only a static allowlist of Talos bootstrap field paths, so a user secret at any other path leaked verbatim on --dry-run. Extend the drift redactor with the user secret set (value-based, covering secrets nested in slices) gated by the existing --show-secrets-in-drift, so template and apply mask the same values symmetrically. Sealing and redaction are structural (YAML-node) and value-based, never substring, so they cannot corrupt quoting or match a value mid-string, and are deterministic so repeated -I runs produce no git churn. A round-trip test drives the omitted body back through MergeFileAsPatch and asserts the real secret survives exactly once. Value-based matching is exact across the whole config; a secret colliding with an ordinary string seals that field too — a documented sharp edge, pinned by test. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Aleksei Sviridkin <f@lex.la>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (2)
📝 WalkthroughWalkthroughAdds encrypted user values end to end: ChangesEncrypted user values
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 OpenGrep (1.23.0)pkg/commands/contract_apply_values_test.go┌──────────────┐ �[32m✔�[39m �[1mOpengrep OSS�[0m [00.20][ERROR]: unable to find a config; path pkg/commands/contract_value_path_resolution_test.go┌──────────────┐ �[32m✔�[39m �[1mOpengrep OSS�[0m [00.26][ERROR]: unable to find a config; path Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Code Review
This pull request introduces support for encrypted user value files (*.encrypted.yaml) in talm, allowing arbitrary secrets to be stored encrypted at rest and decrypted in memory during template rendering and application. It updates talm init to encrypt/decrypt values-secret.yaml and add it to .gitignore, implements secret-value sealing (omission in-place and redaction in stdout/drift previews), and aligns the value-source CLI flags of talm apply with talm template. The reviewer identified a potential issue in warnUnpersistedEncryptedFiles where relative CLI paths might cause false-positive warnings when compared against absolute project-resolved paths, and provided a robust path-suffix comparison fix.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| func warnUnpersistedEncryptedFiles(valueFiles, persistedValueFiles []string, w io.Writer) { | ||
| for _, filePath := range valueFiles { | ||
| if !strings.HasSuffix(filePath, age.EncryptedFileSuffix) { | ||
| continue | ||
| } | ||
|
|
||
| if slices.Contains(persistedValueFiles, filePath) { | ||
| continue | ||
| } | ||
|
|
||
| fmt.Fprintf(w, "warning: %s holds encrypted values that were omitted from the rendered node file, "+ | ||
| "but it is not in Chart.yaml templateOptions.valueFiles; `talm apply` will re-render WITHOUT these "+ | ||
| "secrets and the fields will be absent from the applied config. Add it to templateOptions.valueFiles "+ | ||
| "(or re-pass --values %s to apply).\n", filePath, filePath) | ||
| } | ||
| } |
There was a problem hiding this comment.
The path comparison in warnUnpersistedEncryptedFiles uses a direct equality check (slices.Contains(persistedValueFiles, filePath)). However, persistedValueFiles contains paths resolved against the project root (which are typically absolute), while valueFiles can contain CLI-supplied paths that are relative to the current working directory (CWD). This mismatch will cause false-positive warnings even when the file is correctly declared in Chart.yaml but passed via the CLI with a relative path.
To fix this robustly without introducing new imports, we can perform a path-suffix comparison that tolerates both forward and backward slash separators.
func warnUnpersistedEncryptedFiles(valueFiles, persistedValueFiles []string, w io.Writer) {
for _, filePath := range valueFiles {
if !strings.HasSuffix(filePath, age.EncryptedFileSuffix) {
continue
}
// Check if the file is persisted, tolerating relative vs absolute path differences
persisted := false
for _, p := range persistedValueFiles {
if p == filePath ||
strings.HasSuffix(p, "/"+filePath) || strings.HasSuffix(p, "\\"+filePath) ||
strings.HasSuffix(filePath, "/"+p) || strings.HasSuffix(filePath, "\\"+p) {
persisted = true
break
}
}
if persisted {
continue
}
fmt.Fprintf(w, "warning: %s holds encrypted values that were omitted from the rendered node file, "+
"but it is not in Chart.yaml templateOptions.valueFiles; `talm apply` will re-render WITHOUT these "+
"secrets and the fields will be absent from the applied config. Add it to templateOptions.valueFiles "+
"(or re-pass --values %s to apply).\n", filePath, filePath)
}
}There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@pkg/commands/contract_value_path_resolution_test.go`:
- Around line 81-83: The test mutates the global Config.RootDir and
Config.TemplateOptions.ValueFiles fields starting at line 81-83 but does not
restore these values after the test completes, potentially causing test
cross-contamination. Save the original values of Config.RootDir and
Config.TemplateOptions.ValueFiles immediately upon test entry, then use defer
statements to restore both fields to their original values before any mutations
occur. This ensures the global state is restored to its initial state after the
test finishes, maintaining test isolation and deterministic ordering.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 35620a55-94f9-421f-9c04-3e985cfe8064
📒 Files selected for processing (25)
README.mddocs/manual-test-plan.mdpkg/age/age.gopkg/age/contract_decrypt_to_map_test.gopkg/commands/apply.gopkg/commands/contract_apply_values_test.gopkg/commands/contract_chart_gitignore_test.gopkg/commands/contract_drift_user_secret_redaction_test.gopkg/commands/contract_init_values_secret_test.gopkg/commands/contract_template_redaction_test.gopkg/commands/contract_template_test.gopkg/commands/contract_value_path_resolution_test.gopkg/commands/init.gopkg/commands/preflight_apply_safety.gopkg/commands/preflight_apply_safety_redact.gopkg/commands/preflight_apply_safety_redact_test.gopkg/commands/preflight_apply_safety_test.gopkg/commands/root_detection.gopkg/commands/template.gopkg/commands/value_secret.gopkg/engine/contract_loadvalues_encrypted_test.gopkg/engine/contract_seal_list_element_test.gopkg/engine/contract_seal_test.gopkg/engine/engine.gopkg/engine/seal.go
| Config.RootDir = testProjectRoot | ||
| Config.TemplateOptions.ValueFiles = []string{"values-secret.encrypted.yaml", "/abs/extra.yaml"} | ||
|
|
There was a problem hiding this comment.
Restore mutated global Config fields to avoid test cross-contamination.
This test mutates Config.RootDir and Config.TemplateOptions.ValueFiles but only restores apply/template flag state. Please defer-restore both Config fields to keep test ordering deterministic.
Proposed fix
func TestTemplateAndApply_ResolveConfigValueFilesIdentically(t *testing.T) {
applyRestore := snapshotApplyValueState()
defer applyRestore()
origTmplValueFiles := templateCmdFlags.valueFiles
defer func() { templateCmdFlags.valueFiles = origTmplValueFiles }()
+ origRootDir := Config.RootDir
+ origConfigValueFiles := slices.Clone(Config.TemplateOptions.ValueFiles)
+ defer func() {
+ Config.RootDir = origRootDir
+ Config.TemplateOptions.ValueFiles = origConfigValueFiles
+ }()
Config.RootDir = testProjectRoot
Config.TemplateOptions.ValueFiles = []string{"values-secret.encrypted.yaml", "/abs/extra.yaml"}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| Config.RootDir = testProjectRoot | |
| Config.TemplateOptions.ValueFiles = []string{"values-secret.encrypted.yaml", "/abs/extra.yaml"} | |
| func TestTemplateAndApply_ResolveConfigValueFilesIdentically(t *testing.T) { | |
| applyRestore := snapshotApplyValueState() | |
| defer applyRestore() | |
| origTmplValueFiles := templateCmdFlags.valueFiles | |
| defer func() { templateCmdFlags.valueFiles = origTmplValueFiles }() | |
| origRootDir := Config.RootDir | |
| origConfigValueFiles := slices.Clone(Config.TemplateOptions.ValueFiles) | |
| defer func() { | |
| Config.RootDir = origRootDir | |
| Config.TemplateOptions.ValueFiles = origConfigValueFiles | |
| }() | |
| Config.RootDir = testProjectRoot | |
| Config.TemplateOptions.ValueFiles = []string{"values-secret.encrypted.yaml", "/abs/extra.yaml"} |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@pkg/commands/contract_value_path_resolution_test.go` around lines 81 - 83,
The test mutates the global Config.RootDir and Config.TemplateOptions.ValueFiles
fields starting at line 81-83 but does not restore these values after the test
completes, potentially causing test cross-contamination. Save the original
values of Config.RootDir and Config.TemplateOptions.ValueFiles immediately upon
test entry, then use defer statements to restore both fields to their original
values before any mutations occur. This ensures the global state is restored to
its initial state after the test finishes, maintaining test isolation and
deterministic ordering.
Drift preview doesn't redact encrypted-user-file values by valueHeads-up on a small gap I hit while using this branch (built on top of a local v1.14-machinery bump, but the redaction code path here is machinery-independent). A secret value sourced from an encrypted user value file ( Minimal repro
# values-secret.yaml
demoSecret: "high-entropy-value-do-not-collide-abcdef0123456789"A template rendering it into a non-allowlisted field (e.g. a static pod env): machine:
pods:
- apiVersion: v1
kind: Pod
metadata: {name: demo, namespace: kube-system}
spec:
containers:
- name: demo
image: busybox
env:
- name: DEMO
value: {{ .Values.demoSecret | quote }}Then:
Expected: the drift preview redacts it the same way |
talm apply --dry-run prints two diffs: talm's structured drift preview and the server-returned "Config diff:" block (Talos ModeDetails). Only the structured preview was redacted; the server diff was printed verbatim, leaking user encrypted-value secrets and Talos bootstrap key material (ca.key, token, encryption secrets) into stderr and CI logs. Redact ModeDetails by value before printing: collect the bootstrap allowlist values from the rendered config plus the user secret set from encrypted value files, then mask every occurrence. --show-secrets-in-drift governs this surface too, matching the structured drift preview. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Aleksei Sviridkin <f@lex.la>
|
Confirmed and fixed — thanks for the precise repro. You were right, and it's a surface my redaction didn't cover.
Fix: talm now redacts the Verified end-to-end against a live node: by default the user secret renders as |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@pkg/commands/preflight_apply_safety_modedetails.go`:
- Around line 163-218: The apply diff output is being written to the wrong
stream in emitApplyResults/printApplyResultsRedacted, which changes the CLI
contract from helpers.PrintApplyResults. Update these call sites so ModeDetails
is printed to stdout via the appropriate writer instead of os.Stderr, while
keeping warnings on cli.Warning/stderr and preserving the redaction behavior in
printApplyResultsRedacted and emitApplyResults.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 3088aa18-6b6d-4250-942a-8293574af117
📒 Files selected for processing (4)
docs/manual-test-plan.mdpkg/commands/apply.gopkg/commands/contract_modedetails_redaction_test.gopkg/commands/preflight_apply_safety_modedetails.go
🚧 Files skipped from review as they are similar to previous changes (2)
- docs/manual-test-plan.md
- pkg/commands/apply.go
| // printApplyResultsRedacted mirrors talosctl's helpers.PrintApplyResults | ||
| // (warnings to stderr, ModeDetails to w) but runs ModeDetails through | ||
| // redactValuesInText first. ModeDetails is the server-returned dry-run / | ||
| // apply-mode diff; talosctl prints it raw, which leaks any secret-bearing | ||
| // field that appears in or adjacent to a change hunk. w receives the (possibly | ||
| // redacted) diff; warnings keep going through cli.Warning for parity. | ||
| func printApplyResultsRedacted(resp *machineapi.ApplyConfigurationResponse, values map[string]struct{}, w io.Writer) { | ||
| for _, message := range resp.GetMessages() { | ||
| for _, warning := range message.GetWarnings() { | ||
| cli.Warning("%s", warning) | ||
| } | ||
|
|
||
| if details := message.GetModeDetails(); details != "" { | ||
| _, _ = fmt.Fprintln(w, redactValuesInText(details, values)) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // emitApplyResults is the redacting replacement for the two | ||
| // helpers.PrintApplyResults call sites on the apply paths. It collects the | ||
| // secret value set the dry-run diff must hide — Talos bootstrap material from | ||
| // the rendered config always, plus user secrets from encrypted value files when | ||
| // the path rendered them — and prints the results with those values masked. | ||
| // --show-secrets-in-drift collects nothing, so the diff prints verbatim. | ||
| // | ||
| // rendersUserValues mirrors buildDriftRedactor: only the template-rendering | ||
| // path feeds value files into the applied config, so only it can leak a user | ||
| // secret through ModeDetails; the direct-patch path renders none, so collecting | ||
| // them there would be pure overhead and would wrongly require a talm.key. | ||
| func emitApplyResults(resp *machineapi.ApplyConfigurationResponse, rendered []byte, rendersUserValues bool) error { | ||
| if applyCmdFlags.showSecretsInDrift { | ||
| printApplyResultsRedacted(resp, nil, os.Stderr) | ||
|
|
||
| return nil | ||
| } | ||
|
|
||
| values, err := collectConfigSecretValues(rendered) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| if rendersUserValues { | ||
| userSecrets, err := collectEncryptedValueLeaves(applyValueFilePaths(), Config.RootDir) | ||
| if err != nil { | ||
| return errors.Wrap(err, "collecting user secret values for dry-run diff redaction") | ||
| } | ||
|
|
||
| for value := range userSecrets { | ||
| values[value] = struct{}{} | ||
| } | ||
| } | ||
|
|
||
| printApplyResultsRedacted(resp, values, os.Stderr) | ||
|
|
||
| return nil | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor
🧩 Analysis chain
🌐 Web query:
Does talosctl helpers.PrintApplyResults print ModeDetails to stdout or stderr in siderolabs/talos 1.12.6?
💡 Result:
In siderolabs/talos 1.12.6, the talosctl helper function PrintApplyResults prints to stdout. Technical components of the talosctl command-line tool, including those within the cmd/talosctl/pkg/talos/helpers package, follow standard Go CLI conventions where operational feedback and command results—such as those processed by PrintApplyResults—are directed to standard output (stdout). Standard error (stderr) is typically reserved for error messages, warnings, and diagnostic information that occurs when a command fails or encounters an issue. The function PrintApplyResults is responsible for outputting the results of applied configurations, and consistent with the design of talosctl, this information is intended for the user's terminal session via stdout. [1][2][3][4]
Citations:
- 1: https://oneuptime.com/blog/post/2026-03-03-understand-endpoints-vs-nodes-in-talosctl/view
- 2: https://docs.siderolabs.com/talos/v1.13/reference/cli
- 3: https://docs.siderolabs.com/talos/v1.13/getting-started/talosctl
- 4: https://pkg.go.dev/github.com/siderolabs/talos/cmd/talosctl/pkg/talos/helpers
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate the file and surrounding apply-result call sites.
git ls-files 'pkg/commands/preflight_apply_safety_modedetails.go' 'cmd/talosctl/pkg/talos/helpers*' 'pkg/commands/*apply*' | sed 's#^`#FILE` #'
echo '--- outline preflight file ---'
ast-grep outline pkg/commands/preflight_apply_safety_modedetails.go --view expanded || true
echo '--- relevant symbols in preflight file ---'
rg -n 'emitApplyResults|printApplyResultsRedacted|helpers\.PrintApplyResults|os\.Stderr|ModeDetails|showSecretsInDrift' pkg/commands/preflight_apply_safety_modedetails.go
echo '--- talos helpers references ---'
rg -n 'func PrintApplyResults|PrintApplyResults\(' cmd/talosctl/pkg/talos/helpers* cmd/talosctl/pkg/talos -g '!**/*_test.go' || trueRepository: cozystack/talm
Length of output: 2787
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Read the relevant section of the file if it exists.
file='pkg/commands/preflight_apply_safety_modedetails.go'
if [ -f "$file" ]; then
wc -l "$file"
sed -n '130,240p' "$file" | cat -n
fi
# Inspect the talos helpers implementation if present.
helper='cmd/talosctl/pkg/talos/helpers/apply.go'
if [ -f "$helper" ]; then
wc -l "$helper"
sed -n '1,220p' "$helper" | cat -n
fiRepository: cozystack/talm
Length of output: 4029
Route ModeDetails to stdout. helpers.PrintApplyResults writes the apply diff to stdout, but these call sites send the redacted diff to os.Stderr, which changes the CLI contract for consumers capturing stdout.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@pkg/commands/preflight_apply_safety_modedetails.go` around lines 163 - 218,
The apply diff output is being written to the wrong stream in
emitApplyResults/printApplyResultsRedacted, which changes the CLI contract from
helpers.PrintApplyResults. Update these call sites so ModeDetails is printed to
stdout via the appropriate writer instead of os.Stderr, while keeping warnings
on cli.Warning/stderr and preserving the redaction behavior in
printApplyResultsRedacted and emitApplyResults.
The absolute-passthrough fixtures used bare /-rooted paths, which are not filepath.IsAbs on Windows (no volume), so resolveProjectValueFiles re-joined them against the project root and the assertions failed on windows-latest. Use the crossPlatformAbs helper for the absolute fixtures and filepath.Join for the expected joined paths so the host path separator matches what the resolver emits. Production behaviour is unchanged; a real C:-rooted path was already handled correctly. Signed-off-by: Aleksei Sviridkin <f@lex.la>
Summary
Adds first-class support for arbitrary encrypted user secret values that chart templates consume (a registry password, a KMS plugin's secret-id, etc.) — encrypted at rest in git with the existing
talm.key, decrypted in memory at render time, and kept out of committed node files and terminal output.Closes #221.
What changed
talm applyhonors all value sources. apply re-renders from the modeline at apply time but previously had no--values/--set*flags and ignoredtemplateOptions.{valueFiles,values,...}from Chart.yaml — so a value supplied at template time was silently dropped at apply. apply now registers the full value-source flag set and merges Chart.yaml's templateOptions, rendering identically totalm template. Chart.yaml-declared value files resolve against the project root, so they work from any CWD.Encrypted user value files, decrypted in memory. A value file named
*.encrypted.yaml(referenced fromtemplateOptions.valueFiles) is age-decrypted in memory viatalm.keyat both template and apply — plaintext never touches disk.talm init --encrypt/--decryptnow coversvalues-secret.yaml↔values-secret.encrypted.yaml, and the plaintext is git-ignored. A misnamed/envelope-less*.encrypted.yamlfails loudly; a missing key surfaces a recovery hint.Secrets kept out of committed and printed output.
talm template -Iomits secret-bearing fields fromnodes/*.yaml(the real value is re-injected only at apply, which re-renders from the encrypted file).talm templatestdout andtalm apply's drift preview redact them by default;--show-secrets/--show-secrets-in-driftreveal them. Omission is structural (whole map key or whole list element — never a partial list, which would clobber amerge:"replace"list at apply) and deterministic (no git churn).Testing
go build,go test ./...,golangci-lint runall clean. New contract tests cover the omit→merge round-trip (includingmerge:"replace"and empty-map cases), in-memory decryption, template↔apply value/path-resolution parity, drift redaction by value, the unpersisted-file warning, and the init encrypt/decrypt round-trip.docs/manual-test-plan.mdand README updated.Summary by CodeRabbit
New Features
values-secret.encrypted.yaml, supporting template and apply flows with in-memory decryption.talm apply --dry-rundrift preview and post-apply verification now redact user-encrypted secrets by default, with--show-secrets-in-driftto reveal them.--valuesfiles (*.encrypted.yaml) are decrypted in-memory during apply/template rendering.talm initencryption/decryption support forvalues-secret.yamlplus gitignore updates.Documentation