Skip to content

feat(settings)!: resolve a relative secret_file against its file's directory - #60

Merged
andronics merged 1 commit into
mainfrom
feat/relative-secret-file
Nov 4, 2026
Merged

andronics merged 1 commit into
mainfrom
feat/relative-secret-file

Conversation

@andronics

Copy link
Copy Markdown
Contributor

What and why

Builds design record 0010 (Approved 2026-11-04), which amends 0009's secret_file rule. 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).

  • A relative path resolves against the directory of the file that holds the leaf: the config file's, or a section's _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.
  • New Layer.base_dir: Path | None = None. read_file() sets it to the config file's directory. With None, a relative path is a ConfigError naming the leaf, and the file is never opened.
  • The base is never resolved through symlinks (a ConfigMap mount's ..data layout keeps the mount as its base). A relative config path is made absolute once, with Path.absolute(), in read_file(), so a later chdir doesn't move it; where is now that absolute path.
  • A path whose first part starts with ~ is a ConfigError.
  • Read errors name the leaf, the path as written and, for a relative one, the resolved path, then the origin; never the contents.
  • Decision E still comes first: a layer without secret_files refuses every leaf, whatever base_dir is.

Tests cover each point (a different working directory, a section's _FILE elsewhere, list items and typed items, an indexed section field's _FILE, project layers with and without base_dir, ~, a ConfigMap-style symlinked mount and a config file linked from elsewhere, a relative <PREFIX>CONFIG with a later chdir, the messages, and decision E's ordering); all of them but two controls fail on main. 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

  • Tests cover the change, and fail without it
  • docs/wiki/ is updated (see .agents/stack.md "Docs map"), or this has no user-visible effect
  • Generated wiki sections regenerated (uv run scripts/gen-wiki.py)
  • No secrets, personal data or real hostnames/IPs in code, tests or docs
  • New dependencies are justified below, and actually exist on the index (none added)
  • Template problem found? Linked issue in that template's repository: none found

Reviewer checklist

Human review is required for every PR, and is the whole point for generated ones:
docs/ai/review-checklist.md.

  • I read every line of the diff, not only the summary
  • Behaviour matches the description; edge cases and errors are handled
  • Tests assert real behaviour (not just that the code runs)

…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 andronics left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
andronics merged commit 6ff1f14 into main Nov 4, 2026
13 checks passed
@andronics
andronics deleted the feat/relative-secret-file branch November 4, 2026 08:16

@andronics andronics left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 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.

  1. 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.
  2. 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 the BREAKING CHANGE: footer doesn't. A caller that compares where, 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).
  3. src/interpose/settings.py:1462 (Ruling 4): a relative base_dir from a project is joined as given, so it resolves against the working directory when it's read. A later chdir then moves it, which is the very problem 0010 fixes, here for project-built layers. Either say plainly in API-Reference that base_dir should be absolute, or make it absolute when the Layer is 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).

@andronics andronics mentioned this pull request Nov 4, 2026
6 of 9 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

settings: relative secret_file paths resolve against the working directory

1 participant