Repository navigation
Config loads run one snapshot of the directory; sandbox workers get its bytes (BACKLOG #2596, steps 1 partly, 2, 5) - #2556
wshallwshall wants to merge 18 commits into
Conversation
A load read the config directory several times: Python's source loader read each module and preferred a matching __pycache__ file to it, the code-set and connections.toml loaders read their own files, and the digest read everything again. A planted cache file ran under an unchanged fingerprint. Now load_config reads the directory once, after the trust check, into a ConfigSnapshot (config/snapshot.py). Every module and _ helper is compiled by the loader from the snapshot bytes, code sets and connections.toml are parsed from them, and the graph carries the snapshot. The fingerprint folds the same snapshot (scheme v3: code-set tables matched by lower-case suffix, so codesets/x.CSV is hashed on POSIX; each path hashed, so a linked helper changes the digest). A reload reads one strict snapshot, checks a dual-control approval against it, loads it, and records its digest, so the checked, run and recorded bytes are one set. The post-load re-checks that refused an edit after it ran are gone with the edit's effect. ADR 0041 D1 gains an amendment. The value file under [environments].base_dir is still not in the snapshot. BACKLOG #2596 (step 1, partly).
…irectory The boot frame carried config_dir, so each worker, and each respawn after a crash or a wall-cap kill, loaded whatever was on disk then, with no reload and no audit row. The boot frame now carries the snapshot the engine's graph was loaded from (files as base64 segments, paths checked against the closed set a snapshot may hold), and the worker loads it with load_snapshot, which reads nothing from disk. RegistryRunner hands each session registry.config_snapshot; a session given none reads config_dir once, under the load's trust check, and every respawn reuses those bytes. The child no longer refuses on its own: it simply runs the parent's verified bytes, the shape ADR 0087 allows. BACKLOG #2596 (step 2).
…t loader tests/test_config_snapshot.py: a planted timestamp-valid __pycache__ file does not run (with a control proving the ordinary source loader would run it), the loader never opens the cache folder, a helper runs snapshot bytes, a respawned sandbox worker runs the engine's bytes after an on-disk edit (control: a session with no snapshot is refused), the boot frame carries the snapshot and no directory, codesets/x.CSV is hashed, and a linked _ helper changes the digest. Also small /simplify cleanups (one covered-only rule, one suffix rule, a direct import). BACKLOG #2596 (step 5). Proposed PR title: Config loads run one snapshot of the directory; sandbox workers get its bytes (BACKLOG #2596, steps 1 partly, 2, 5) Proposed vault banner: PARTIAL -- steps 2 and 5 MET, step 1 MET EXCEPT THE VALUE FILE, step 3 MET before (engine PR 2239), step 4 read and holds. Step 1: load_config reads one ConfigSnapshot after the trust check and compiles every module and helper from it (no __pycache__ consulted); code sets and connections.toml parse from it; fingerprint scheme v3 folds it; a reload checks an approval against the snapshot it loads (commit 9ded29d). Remaining: the value file under [environments].base_dir is not in the snapshot. Step 2: the boot frame carries the snapshot and the worker never reads the directory (79e9b8e). Step 5: probe tests in tests/test_config_snapshot.py, shown failing on unfixed origin/main.
The engine's reload now calls load_config(path, snapshot=...). Two fakes patched over engine.load_config still took one positional argument, so their reload raised TypeError: test_passthrough's _patch_load_config and test_reference_sets' unsupporting-backend reload. Both now accept and ignore keywords. BACKLOG #2596
Four suites still named round one's old seams and failed on the branch: two fingerprint fakes took no snapshot keyword; two validate_config tests patched load_connections_file, which a load no longer calls (it is load_connections_data now); the path-join census still graded the gone fingerprint._require_listable and load_config rows and lacked the snapshot reader and load_snapshot; and the from-None census named load_policy where the raise moved to parse_policy. BACKLOG #2596
Two review findings on the snapshot's names. A name the closed-set check refused raised a bare ValueError out of every load: on POSIX a file such as codesets/a\b.csv crashed serve start and made validate_config raise. The check now asks whether each part is a plain name on this platform: POSIX accepts a backslash or a colon, which its listing can return, and Windows refuses a backslash or a drive (C:x.py), which joined to the directory would select another file's bytes from a boot frame. Modules and code-set tables ran in string order, case-sensitive, where the loader once sorted paths: without case on Windows. They sort by path again, so load order and the file a duplicate refusal names hold. BACKLOG #2596
The frame carried every table twice: as base64 file bytes in the snapshot and as the engine's parsed tables. That halved the table volume the 64 MiB frame cap admits, so a sandboxed inbound could fail to boot where mode=off served. When the engine publishes its tables, the frame now leaves the codesets/ files out, and load_snapshot takes the tables in place of parsing them, before any module runs. With none published, the files go and the worker parses them, as before. BACKLOG #2596
A session given no snapshot read its directory after Popen, inside a handler that killed the child only for three exception types, so any other fault left a live child, job handle and drain threads. The boot frame is now built before the child exists. A worker runs the snapshot it is sent and never reads the directory, so the refusal text and the sandbox_worker_refused alert no longer name an edit on disk as the cause, or a reload as the remedy for a graph that differs by name. The alert tests now send the worker edited bytes to provoke a content-only refusal, since a disk edit no longer reaches it. BACKLOG #2596
The identifier a load failure may name is vouched for by the source line that raised it. That line was read from the file on disk, which may hold other bytes by then, and a sandbox worker read the directory on this path. The loader now hands the diagnostic the snapshot's bytes for any file it compiled. BACKLOG #2596
validate_config counted an unreadable codesets_map.py, a module, as a table, so an empty graph drew a derived diagnostic beside its cause. It now matches the codesets folder and its files only. config_code_digest(path) read the whole directory strictly, so an unreadable value file took away a digest of modules alone. It reads non-strictly and refuses only for a module it could not read, naming it. The reload passes its snapshot to load_config unconditionally. BACKLOG #2596
Fixes from the second code-review round: - read_trusted_snapshot read a directory gone after its check as an empty one when lax, so a load ran an empty graph, or with allow_empty returned one, where it must answer as missing (BACKLOG #3157). - dec_snapshot caught only binascii.Error; a non-ASCII str segment raised a bare ValueError past the codec's own refusal type. - The reload's strict snapshot warning now says what follows a failed read, keeping its greppable prefix. - Docstrings no longer say a worker re-reads codesets/ or that a content-only refusal is a disk edit. BACKLOG #2596
|
Lander: ready for you. Code review ran at xhigh, 2 rounds (QA line in the body), covering head e33f5fb. Nine open findings are listed for filing; items 1 and 2 change behaviour (item 2 is an audit-label change with a Manager reading). Read the Linux, Postgres and SQL Server legs. |
|
Lander: do not merge yet. semgrep (project SAST rules) is red on e33f5fb: mf-no-eval-exec fires on the new compile+exec in messagefoundry/config/wiring.py. A Builder is replacing it with an in-memory importlib loader (no exemption) and will re-review the delta; I will post the new head and its QA line here. |
…pshot The snapshot loader compiled each config module and helper with compile() and ran it with a literal exec(). It now gives each module a spec whose loader is _SnapshotLoader, an importlib.abc.InspectLoader that serves the snapshot's bytes for that file, and runs it with spec.loader.exec_module, the import system's own path, as origin/main did with spec_from_file_location. Config modules are code-first by design and still execute through importlib; only where the bytes come from changed. Same semantics: modules, order, names, sys.modules handling, helper imports and error classes are unchanged. The code object is compiled from the snapshot bytes under the file's own path, the loader has no bytecode methods so no __pycache__ file is read or written, and importlib.reload still finds no spec for the mangled name. get_source now serves the same bytes. No get_data: pkgutil.get_data stays unavailable (open finding 3). Clears semgrep mf-no-eval-exec with no exemption. Test: the helper probe also checks both modules' loader, origin, __file__, __cached__ and source; a new test pins that reload refuses a config module. BACKLOG #2596
Review round 1 on the loader change. get_code calls compile() itself rather than InspectLoader.source_to_code, so a parser wall (MemoryError, RecursionError) is still judged as raised in this package and keeps its wording, as before the loader. get_source returns None: a served source let linecache cache a missing file's lines with no time to check, so a later load of the same path showed the old lines. The spec comment now says what it builds, and the reload test checks the module was not re-run and restores the spec reload cleared. BACKLOG #2596
Review round 2 on the loader change. The first get_code drops the snapshot, so runpy.run_module gets no code and refuses a loaded config module, as it did when the module had no loader, and a module kept alive after a reload holds no snapshot bytes (a linecache lazy entry holds only the path). The byte lookup is back inside the run, in _exec_module's try, so a missing file stays a WiringError. Tests: runpy refuses, and the compile-wall test checks the cause is MemoryError or RecursionError. BACKLOG #2596
Dismissed because a newer commit was pushed; Greptile will re-review the current head.
|
Lander: hold lifted. New head 02c1bc9 replaces the literal exec with an importlib InspectLoader (no exemption); semgrep shows 0 findings locally with CI's command. The delta e33f5fb..02c1bc9 was code-reviewed at xhigh, 2 rounds; QA line in the body under Round 3. Read semgrep, repo-scan, the pytest legs (Linux, Postgres, SQL Server) and CI gate on this head. |
|
Lander (session 8550b02b), 2026-10-27 ~13:55Z. Held at |
|
Lander: please hold PR 2556; do not merge 02c1bc9. The batch 437 Manager ruled on the two behaviour-changing findings after an independent adversarial review. Both are KEPT:
The review found one real gap:
|
Engine.reload_detail raises ConfigRefusedBeforeApprovalCheck, a WiringError keeping the refusal's message, cause and traceback, when a released reload is refused by the settings preflight or the source trust check before the snapshot's digest is checked. The config_reload_failed row then carries approved_fingerprint and approval_digest_check "not_reached", and keeps its reason. A load that fails after the check passed carries neither. The snapshot read's WARNING now says any other reload refuses every unreadable file the load reads. BACKLOG #2596
SECURITY.md no longer says a released reload checks the digest again after the load, or once more after an invalid-config failure. It says the trust check runs first and reads no contents, the digest is checked against the snapshot that loads, a tamper that also loosens permissions is audited invalid_config with the not-reached marker, and an unreadable helper refuses a start or reload. SERVICE.md and CONFIGURATION.md drop the claim that a __pycache__ file runs. Adds the changelog fragment. BACKLOG #2596
…re than the code does The docs and changelog say the marker comes with a WiringError refusal before the digest check, that a missing or disallowed directory gets neither field, that a refusal after the check can come from inputs the digest does not cover, and that the environment value file is read again after the check. The __pycache__ claim names config modules and helpers only. The engine WARNING names the environments folder, and a test assertion that could not fail is gone. BACKLOG #2596
… from disk The docs and changelog say the loader compiles config modules and the helpers an import statement reaches, and that a module reached through the ordinary import system can still run a cache file. The start check docstring drops the clause CONFIGURATION.md dropped. The limits list says a folder removed between the two listings is left out, and the trust-check test also asserts nothing ran, nothing swapped and the approval failed. BACKLOG #2596
BACKLOG #2596 (steps 1 partly, 2, 5). Batch 437. Source branch
b437-2596-snapshot-loader, head02c1bc98b57bf453694e96db577ff07bdc432750. Own PR because it changes a security control (what an approval binds, and what a sandbox worker runs).What changed
messagefoundry/config/snapshot.py:ConfigSnapshot/read_config_snapshotread the config directory once, after the trust check.__pycache__is never read.load_configcompiles every module and_helper from the snapshot bytes (compile+exec, notspec_from_file_location). Code sets andconnections.tomlparse from the same bytes. The snapshot rides on the graph asRegistry.config_snapshot.mefor-cfg-fp:v3folds the snapshot. A code-set table matches by lower-case suffix (codesets/x.CSVis hashed on POSIX; #3535 item 1), and the resolved-path dedup is gone, so a symlinked_helper changes the digest (#3535 item 2).engine.reload_detailreads one strict snapshot, checks the approval against it, loads it, and records its digest. The post-load re-checks and_require_unchanged_codeare gone.config_dir; the worker runsload_snapshotand reads no files. A session with no snapshot reads it before the child starts. The child never refuses on its own (ADR 0087).\and drive names); load diagnostics read the bytes that ran.Not done (remainder stays on the row)
[environments].base_diris still read by the serve provider, outside the snapshot.QA
The review covers head
e33f5fbc1, which holds the round-1 build (Builder 1) plus the review round's 8 commits (Builder 2). Each fix has a regression test intests/test_config_snapshot.py.Rejected (quality or bounded): environments bytes held in memory; a file deleted mid-read drops from the snapshot (the one that runs is the one hashed); directory listed twice;
validate_configduplicating trust-then-read; fingerprint branches kept for one-argument test fakes.Round 3: semgrep
mf-no-eval-execcleared (commits 1671d17, d1cc2bc, 02c1bc9)The one literal
exec(code, module.__dict__)the branch added (in_HelperImporter.run) is replaced by_SnapshotLoader(importlib.abc.InspectLoader): config modules run throughspec.loader.exec_module, as onorigin/main, with the code object compiled from the snapshot bytes. Config modules are code-first by design (CLAUDE.md section 1); only where the bytes come from changed. Nonosemgrep, no edit to.semgrep/or the workflow. The loader serves one code object then drops the bytes (runpy andimportlib.reloadstay refused), serves no source, has no bytecode methods, and keeps the compile-wall error wording. Semgrep 1.172.0 run locally with CI's command: red reproduced on e33f5fb, 0 findings on 02c1bc9.This round reviewed the delta e33f5fb..02c1bc9; rounds 1 and 2 above cover everything up to e33f5fb. Round-3 checks: ruff, mypy four legs, 67 related test files (2422 passed, 32 skipped), pre-commit including bandit. Full suite not finished locally.
Open findings (for the Lander to file; two change behaviour)
In scope:
_helper that no module imports now refuses the whole load, where before it only left the fingerprint partial.digest_mismatch; the test pinning the old classification was removed in round 1. Manager reading: the refusal is unchanged and the earlier check is the stronger one, so no control is weakened; only the audit label moves. Medium confidence; a ruling on whether the label must be kept is the owner's.pkgutil.get_dataandimportlib.resourcesstop working for config code.Outside scope:
7.
capture_start_provenancestill re-reads the directory instead of using the snapshot that ran (#2597 area).8. The dry-run
config_reload_checkrow inapi/app.pyre-reads the directory too.9. The step-1 value file (stays on the row).
10. (Round 3)
module.__spec__.cachedstill resolves to a__pycache__path while__cached__is None (already true at e33f5fb). Nothing reads it today, but Python 3.15 readsspec.cached. Finding 3 (pkgutil.get_data) was not restored: a snapshot-onlyget_datawould serve a subset of what disk did.Checks
test_gate_installed_parityandtest_installed_coord_hooksx2 are installed-hook drift on the Builder's machine, not this change. Not re-run in full after the fixes.test_a_posix_module_named_with_a_backslash_loadsruns only there;codesets/x.CSVdiscriminates only there; the symlink digest test may skip on Windows). Postgres and SQL Server legs (test_passthrough,test_reference_sets). windows-service-smoke.Proposed vault banner (the Lander's to flip)
PARTIAL -- steps 2 and 5 MET, step 1 MET EXCEPT THE VALUE FILE, step 3 MET before (engine PR 2239), step 4 read and holds. Step 1: load_config reads one ConfigSnapshot after the trust check and compiles every module and helper from it (no pycache consulted); code sets and connections.toml parse from it; fingerprint scheme v3 folds it; a reload checks an approval against the snapshot it loads. Remaining: the value file under [environments].base_dir is not in the snapshot. Step 2: the boot frame carries the snapshot and the worker never reads the directory. Step 5: probe tests in tests/test_config_snapshot.py, shown failing on unfixed origin/main. A second code-review round fixed 13 findings. Also closes #3535 items 1 and 2.
Merge conflicts expected
Open PRs 2539 and 2532 touch
wiring.py/engine.py; PR 2567 (#3234, same batch) toucheswiring.pyoutside the loader region.

Confidence Score: 5/5
The reviewed changes appear safe to merge; no additional actionable defects were found.Summary
Config loads now compile and parse one captured set of bytes. Reload approvals and reload fingerprints use that snapshot, and sandbox workers receive its bytes instead of reopening the directory.
Already acknowledged by
wshallwshall, and not reported again:Diagram
%%{init: {'theme': 'neutral'}}%% flowchart TD A[Config directory] --> B[Trust check] B --> C[Read ConfigSnapshot] C --> D[Check approval when required] D --> E[Compile modules and parse data] E --> F[Registry carries snapshot] F --> G[Reload fingerprint from snapshot] F --> H[Encode sandbox boot frame] H --> I[Worker loads supplied bytes]Reviews (1) · Last reviewed commit: "A vanished directory stays missing, and ..." · Reviewed by Greptile