Repository navigation
feat(settings)!: resolve a relative secret_file against its file's directory - #60
Conversation
…rectory
A {secret_file: ...} leaf with a relative path now resolves against the
directory of the file that holds it (design 0010, amending 0009): the config
file's, or a section's _FILE's own, whatever layer the rest came from. List
items resolve against their list's file. The base is the directory as named,
never resolved through symlinks, so a Kubernetes ConfigMap mount's ..data link
stays out of it. read_file() makes a relative config path absolute once, so a
later chdir doesn't move it.
Layer gains base_dir (a Path, default None), which read_file() sets. With no
base_dir, a relative path is a ConfigError and the file is never opened. A
path starting with ~ is a ConfigError. Decision E still comes first. Read
errors name the path as written and, for a relative one, the resolved path.
BREAKING CHANGE: a relative secret_file path now resolves against the
directory of the file that names it, not the working directory, and a
secret_file path starting with ~ is refused with a ConfigError.
Fixes: #50
andronics
left a comment
There was a problem hiding this comment.
Reviewed _secret_target (~ refused, absolute unchanged, relative under base_dir or refused, no symlink resolution), read_file/_FILE base_dir wiring, decision E ordering, and CI. Rulings 1-4 upheld.
andronics
left a comment
There was a problem hiding this comment.
🔍 Reviewer · 4e91789 · notes
CI is green: tests on Python 3.13 and 3.14, checks, docs, the personal-data guard, the secret scan, and markdown and workflow lint. This PR was already merged when I read it. Brief: network-tools interpose-0010-code. I read the brief, evidence/ and report.md.
The diff builds record 0010 §1–§3 as the brief says. Layer.base_dir is set by read_file(), and read_file() makes the path absolute with absolute(), not resolve(). A section's _FILE uses its own directory. A leading ~ is refused, and a relative path with no base is refused without opening the file. Decision E's check still runs first. The read errors name the path as written and the resolved one. The commit is feat(settings)! with a BREAKING CHANGE: footer, and the body has Fixes: metalayerlab/interpose#50. evidence/without-change.txt shows the new tests fail on the old code, apart from the two stated controls. The verdict "5 of 5" matches Done means, and Rulings 1 to 4 each have a date, a basis and an undo. Nothing touches mitm-proxy or sluice, and there's no AI credit or AI section.
report.md, For you: "Moving it to Implemented after merge needs the brain coordinator's approval". Since the arbiter's cut-over (record 0023), a design record's status change goes to the arbiter. Send the Implemented PR for record 0010 there.src/interpose/settings.py:678(Ruling 1):Layer.where, and every error that names it, is now absolute when the config path was relative. The wiki says so, but theBREAKING CHANGE:footer doesn't. A caller that compareswhere, or a log alert that matches the old text, changes silently. Add it to the 0.5.0 release notes (edit the release-please PR's notes).src/interpose/settings.py:1462(Ruling 4): a relativebase_dirfrom a project is joined as given, so it resolves against the working directory when it's read. A laterchdirthen moves it, which is the very problem 0010 fixes, here for project-built layers. Either say plainly in API-Reference thatbase_dirshould be absolute, or make it absolute when theLayeris built. Low: no caller passes a relative one today.
Not checked: I didn't run the tests or checks locally (CI and evidence/checks.txt stand for them).
What and why
Builds design record 0010 (Approved 2026-11-04), which amends 0009's
secret_filerule. Until now a relative{secret_file: ...}path was read from the process's working directory, so the same config file found its secrets or not depending on where the service was started (#50)._FILE's own directory, whatever layer the rest came from. A list's items resolve against their list's file.${VAR}expansion comes first; an absolute path is unchanged.Layer.base_dir: Path | None = None.read_file()sets it to the config file's directory. WithNone, a relative path is aConfigErrornaming the leaf, and the file is never opened...datalayout keeps the mount as its base). A relative config path is made absolute once, withPath.absolute(), inread_file(), so a laterchdirdoesn't move it;whereis now that absolute path.~is aConfigError.secret_filesrefuses every leaf, whateverbase_diris.Tests cover each point (a different working directory, a section's
_FILEelsewhere, list items and typed items, an indexed section field's_FILE, project layers with and withoutbase_dir,~, a ConfigMap-style symlinked mount and a config file linked from elsewhere, a relative<PREFIX>CONFIGwith a laterchdir, the messages, and decision E's ordering); all of them but two controls fail onmain. Wiki: Security, API-Reference, Troubleshooting, Architecture-Internals and Testing-Guide. TODO.md's "Decided against" gains the working directory and resolving through symlinks, as the record's Consequences say.Breaking, so this ships as 0.5.0. mitm-proxy and sluice take it through their tag check.
Fixes: #50
Lessons
0009 named a path without saying what it was relative to, and the code took the working directory by default. A design record that adds a path-valued setting should say what a relative path resolves against.
Author checklist
docs/wiki/is updated (see .agents/stack.md "Docs map"), or this has no user-visible effectuv run scripts/gen-wiki.py)Reviewer checklist
Human review is required for every PR, and is the whole point for generated ones:
docs/ai/review-checklist.md.