Repository navigation
Description
Relocated from dezren39/darwin-nix#129 on 2026-10-28 (placement audit): mcpx's single source lives in developing-today/code (pkgs/mcpx); re-home tracked at #309. Original body below; issue references inside it are historical (dezren39/nix numbering).
Parent: #99
Summary
When a bug is one instance of a class -- a declaration and its use live apart, and the same shape has shipped before -- the fix adds a guard test that fails for every member of the class, present and future, not just the instance found. mcpx already has more of these than the strategy doc lists; this issue inventories them, finds where the existing ones leak, picks how guards are written, and lists the guards worth adding next. It also sets the rule for when a bug "is a class". It owns #181 collision 15: a setting must be able to change behaviour, not merely be read. That guard is due before or with #172.
Original notes
- testing
- agents should make at least 1 simple, new test for every bug observed from any source, when cheap to do so-- aggresively test many permutations.
(Guards are the per-class extension of "at least 1 simple, new test for every bug": the per-instance policy is #128.)
Current state: guards that exist at 05c78b2
There are about twenty-five, not three. The ones that matter here, by mechanism:
| Guard | Class it closes | Mechanism | Tier |
|---|---|---|---|
TestEveryExecFlagChangesSomething (internal/e2e/e2e_test.go:2877, from #74/#75) |
a flag accepted and ignored | hand table + runtime probe (with vs without) | e2e |
TestEveryDeclaredRouteAnswersOverTheSocket (internal/e2e/v1ops_test.go:287) |
a declared /v1 op that 404s |
registry enumeration (api.Ops()) + runtime probe |
e2e |
TestEveryDaemonRouteIsDeclared, TestEveryDeclaredOpHasARoute, TestEveryNonStreamingOpIsReachableAsAnMCPTool (internal/api/parity_test.go:54,73,87) |
route/op/MCP-tool drift | source scan of internal/daemon/*.go + registry enumeration |
unit |
TestNoPackageRedeclaresADefault (internal/defaults/guard_test.go:92) |
a constant outside defaults.json |
regex source scan with a reasoned allowlist and an outOfScope list |
unit |
TestEverySettingIsReachableFromAllThreeSurfaces, TestEveryDefaultSurvivesItsOwnValidator, TestEverySettingSaysWhoReadsIt (internal/settings/registry_test.go:27,41,287) |
settings registry consistency | registry enumeration | unit |
TestEverySettingIsReadSomewhere, TestNoHandRolledSettingEnv, TestThePluginReadsNoUndeclaredSetting (internal/settings/consumed_test.go:73,145,209, from #171) |
a setting declared and never read; a registry-owned variable read by hand; the plugin honouring an undeclared variable | registry enumeration + regex source scan for a Set accessor call outside the settings package (accessorRead/readViaHelper, :54,60), one-entry allowlist with a reason (paths.config, :66) |
unit |
TestTheDocumentationListsEverySetting (internal/settings/docs_test.go:19) |
docs drift | registry vs docs/configuration.md |
unit |
TestEverySettingIsReachableOverV1 (internal/e2e/settings_test.go:173), TestEverySettingTheSchemaAdvertisesActuallyWorks (e2e_test.go:1879) |
registry vs served surfaces | runtime probe | e2e |
Nine effect tests in internal/e2e/unread_test.go:48-184 (from #171), e.g. TestPoolDefaultsAreReachableFromTheEnvironment, TestScriptEnvIsReachableFromTheEnvironment |
a wired setting that changes nothing | hand-picked runtime probes: set the value as a user would, assert an observable change | e2e |
TestNoOrphanProcessesAfterStop (e2e_test.go:534) |
MCP children surviving stop |
runtime probe | e2e |
TestEnvCleanupStopsTheDaemonItStarted (e2e_test.go:2630, from #182) |
a test env's daemon surviving its cleanup (#52) | runtime probe; falsified by reverting the fix | e2e |
TestEveryTuiViewProducesRowsFromRealData, TestEveryConsoleMethodIsCoveredAndNameIsKept, TestEveryModernRequestCarriesVersionAndCapabilities, ... |
various | mixed | mixed |
The "declared but never read" audit landed as #171. Its inverse ("read but never declared", 44 MCPX_* variables) is #172 and is in flight. TestEverySettingSaysWhoReadsIt checks only that a setting declares a scope; TestEverySettingIsReadSomewhere checks that something reads it. Nothing checks that reading it can change anything (leak 5 below).
Where the existing guards leak
TestEveryExecFlagChangesSomethingdoes not enumerate the flags. It has four rows (--export,--launcher,--prefix,--typecheck) and a comment "Add a row when you add a flag". The exec/run flag set has 23 flags (internal/cli/commands.go:435-481), including three that Update Rust crate proc-macro2 to 1.0.81 - autoclosed #75 fixed (--no-launcher,--allow-repeat,--no-capture-console) with no row. A new flag with no row passes. The class is not closed; it relies on memory, the thing the guard exists to replace. Its probe half is the model to copy: each row asserts the outcome with the flag and its absence without, and that half caught Update Rust crate proc-macro2 to 1.0.81 - autoclosed #75's own first--typecheckassertion, which matched output present either way.- Regex source scanning misses spellings. PR chore(deps): bump rustix from 0.37.19 to 0.37.25 in /sources/pavex/examples/realworld #56:
TestNoPackageRedeclaresADefaultmatched"5 * time.Minute"while gofmt writes5*time.Minutein call arguments, so nine live constants were invisible to it. A guard that silently passes is worse than none; it was found by accident. - Allowlists are one-way doors.
outOfScopeinguard_test.gostill exempts seven whole packages and one file (guard_test.go:86-90) "owned by work happening in parallel"; nothing makes it shrink. - Nothing fails when a test reaches the network (Update Rust crate axum to 0.6.20 #65 fixed the one instance via
MCPX_REGISTRY_URL,e2e_test.go:57). chore(deps): bump build/buck/preludes/buck2-prelude fromd956689toe7457f6#52 is fixed (Phase 5: tag event bodies must travel with the event #182): cleanup isstop --allwith the error logged, not discarded (e2e_test.go:71-85), andTestEnvCleanupStopsTheDaemonItStartedchecks one env's daemon dies. Still unguarded: a daemon started outsidenewEnv, andtestsupport'smcpx-bin-*/mcpx-fake-*dirs, which are never removed (internal/testsupport/build.go:27,57). consumed_test.goproves a setting is read, not that reading it matters.registry.pageSizeis read (internal/cli/registry.go:265,internal/daemon/ops.go:285) but consulted only insideif limit <= 0(internal/registry/registry.go:154-155). Every ordinary caller passes a positive limit:--limitdefaults to 20 (internal/cli/registry.go:26), and the daemon falls back toregistry.limit(internal/daemon/ops.go:275-276). So it passed Phase 2: delegated Manage cannot add members #171's sweep while inert (Phase 3: /id-sync/1 is used by both range and LogSync handshakes #179). The scan sees a read; it cannot see that the read is unreachable.unread_test.gohas the right shape, a with/without probe, but covers nine hand-picked settings out of 149. This is Phase 3: sync, blob gate, and relays #181 collision 15, owned here (next section).- A guard's name can overclaim.
TestEverySettingIsReachableFromAllThreeSurfaces(internal/settings/registry_test.go:27-39) checks that a flag name exists, that the variable isMCPX_-prefixed and that the setting is documented. That is name shape, not reachability. The e2eTestEverySettingIsReachableOverV1is the one that probes. - An allowlist entry's reason is prose nothing checks, and one is false.
consumed_test.go:67-70exemptspaths.configas "read by config.SearchPath fromMCPX_PATHS_CONFIGdirectly" (repeated indocs/declared-but-unread.md:93-96). Nothing readsMCPX_PATHS_CONFIG; the search path readsMCPX_CONFIG(internal/config/config.go:549). The exemption hid a setting that is dead on all three channels (Bump build/buck/preludes/buck2-prelude from3a02ea3to370cd4d#5). An allowlist reason that names a mechanism should be a mechanism a test can check (the variable is read somewhere, the function exists), or say "declared, not read" plainly.
Owns #181 collision 15: a setting must be able to change behaviour
Owns #181 collision 15: the stronger settings guard. Every registry setting must be able to change an observable outcome through an ordinary caller, not merely be read. #179 (registry.pageSize) is the instance; this issue owns the class.
- Model: Update Rust crate proc-macro2 to 1.0.81 - autoclosed #75's exec flag table (
e2e_test.go:2877) asserts, per row, the outcome with the flag and its absence without. Phase 2: delegated Manage cannot add members #171'sunread_test.goalready does the same for nine settings by hand (set the value the way a user would, as an env var or config key and not a Go field, then assert a change). - Shape (Approach C below, applied to
settings.Registry()): a unit-speed completeness test fails when a setting has neither an effect probe nor an exemption with a reason. Exemptions ratchet down (e.g. plugin-scoped settings the Go side cannot observe, andpaths.config, Bump build/buck/preludes/buck2-prelude from3a02ea3to370cd4d#5). The e2e probes set a non-default value through an ordinary caller and assert a with/without difference. A negative test plants an inert setting, read only behind an unreachable branch asregistry.pageSizeis, and expects the guard to fail. - Timing: apply it before or with Phase 2: membership, delegation, groups, and server config #172 (in flight), which will move up to 44 more
MCPX_*variables into the registry (its recommended option declares the inbound ones and allowlists the outbound). Each should arrive with a probe row or a reasoned exemption, not be audited afterwards. Of the whole tree, this is the one part of Bump unsafe-libyaml from 0.2.8 to 0.2.10 in /sources/pavex/examples/realworld #99 that is time-sensitive. - Scale: 149 settings at 05c78b2 (unique
Path:entries ininternal/settings/*.go), with more to come from Phase 2: membership, delegation, groups, and server config #172. A probe per setting in e2e is too slow for every PR. Keep the completeness half at unit speed (T3 in Update Rust crate tracing to v0.1.40 [SECURITY] #127). Group probes by surface, so that one daemon checks many settings. Update Rust crate tracing-subscriber to v0.3.20 [SECURITY] - autoclosed #124'ssettings.precedencegenerator can supply the non-default values.
When a bug "is a class"
Add a guard (in the fix PR, or file testing + implement and link it) when any of these holds:
- the same shape has shipped before (Update Rust crate petgraph to 0.6.4 - autoclosed #74 was the third "accepted and ignored");
- the bug is drift between a declaration and its use (registry vs reader, op table vs route, flag vs options struct, docs vs code);
- the fix edits a table, registry or allowlist;
- the bug depended on the environment the test ran in (network, OS, leftover processes).
Approaches: how guards are written
A -- source scanning with regexes (least recommended)
Walk .go files and match text, as TestNoPackageRedeclaresADefault, daemonRoutes and #171's consumed_test.go do. Quick to write and needs no registry. Risks: formatting changes the text (the gofmt miss above), comments and strings produce false hits, renames produce false passes, a match proves a call exists but not that it is reachable (leak 5), and failures read as "line 212 matched a pattern" rather than "setting X is never read". Keep only where nothing better exists, and if so parse with go/parser/go/ast (or golang.org/x/tools/go/packages for type info) rather than regexes.
B -- enumerate the registry (reflection over data)
Iterate settings.Registry(), api.Ops(), flag.FlagSet.VisitAll, the command table, and assert properties per entry. Unit-speed, exact, cannot be fooled by spelling -- most of the table above is this. Limitation: it proves declarations are consistent, not that they work; a registry entry nothing reads passes every B guard, and so does one that is read but cannot matter (registry.pageSize).
C -- enumerate, require a probe per entry, prove the guard can fail (recommended)
- Enumerate the registry (B) as the source of truth.
- Completeness: a unit test fails when an entry has neither a probe row nor an exemption with a reason (the
allowedmap style inguard_test.go). Fixes leak 1: a new flag without a row fails in milliseconds, locally, at T1/T3 of Update Rust crate tracing to v0.1.40 [SECURITY] #127. - Probe: each row proves an observable effect in both directions (the Update Rust crate proc-macro2 to 1.0.81 - autoclosed #75 with/without pattern); probes that need the binary live in e2e and run in CI.
- Ratchet: exemption lists may only shrink -- a test pins their length, or CI compares it to
main. - Negative test: every guard's checker is a function with its own test that feeds it a planted violation and expects a failure (fixes leak 2's lesson: a guard that cannot fail is not tested).
- Use AST scanning (not regex) only for classes with no registry. (Phase 2: delegated Manage cannot add members #171's "declared but never read" guard used a regex scan, which is enough to find a read but, as leak 5 shows, not to prove it matters; step 3's probe is what closes that.)
Recommended because it closes the gap the current exec guard has (enumeration), keeps the cheap half (completeness) in unit time so agents actually run it, and makes the guards themselves falsifiable.
Candidate new guards
| Guard | Class | Mechanism (per C) | Owner / coordination |
|---|---|---|---|
| Every setting can change an observable outcome | read but inert (#179, registry.pageSize) |
settings.Registry() vs a probe table; completeness at unit speed; with/without probes in e2e; ratcheted exemptions; planted-inert negative test |
here -- #181 collision 15; first implement task, before or with #172 |
| Every exec/run flag has a row or an exemption | accepted and ignored (#74) | VisitAll over the exec flag set vs the table |
here |
| Every command's flags, not just exec | same, other commands | command table enumeration; rows per command | here |
| declared, never read | done in #171 (consumed_test.go, regex scan for a Set accessor read) |
superseded by the row above for "read but inert" | |
| No two concurrent runs share a generated path | #69, #70, #73 (three instances in three PRs) | property over the runner's naming function (a content hash of every input that changes the program), plus a concurrent e2e probe per artifact kind | here; candidate |
| Every flag mcpx passes to a runtime has the effect it is passed for | an inert subprocess flag (#108) | An acceptance check ("the runtime exits 0 with this argv") would not have caught #108: Node 24 accepts --experimental-strip-types and it is simply inert. Use a pinned capability matrix instead: real argv against a fixture per TypeScript construct per runtime, failing when the matrix changes, so an inert flag shows up as the Node column losing its enum rows. Keep the cheap exit-0 check as a first half, since it still catches a typo or an upgrade that drops a flag. Deno's permission flags (--allow-*, --no-check) are the same exposure: no test file mentions them |
here; #108 (in flight) owns the fix, #100 the runtime matrix |
| Every documented capability exists | a documented capability that does not exist (#102, #106) | Split the ask. Mechanical where a registry is behind it: #102's tool list vs the generated client, #106's CLI vs /v1/exec resolution; those should not wait. Prose gets a marker table, which is probably #132's feature inventory with one more column (build it once). Prose-linting is rejected outright: there is no honest mechanism for it |
here + #132 (#97) |
Every CLI flag has non-empty usage and appears in mcpx --help/mcpx man |
undocumented knob | enumerate flag sets vs generated man (package.nix:88-89 builds it from the binary) |
docs tree for the book; here for the test |
Every /v1 op has a CLI command, or a listed exemption |
CLI parity (#76) | api.Ops() vs command table; exemption list starts as #76's table and ratchets down |
#76 |
| Every feature has an integration test | untested feature | reads #97's inventory file, checks each named test exists (go test -list) |
#97 owns the inventory; this issue owns only the rule that a guard reads it |
| No test reaches the network | #65 | see sub-choice below | here + #95 |
| No test leaves a process (or temp dir) behind | #52 (fixed in #182 per env) | TestMain in internal/e2e: after m.Run(), find processes whose argv contains this run's temp prefix, kill them and fail; remove mcpx-bin-*/mcpx-fake-*. #182 already stopped discarding the stop error and added the per-env test |
here (suite-wide backstop) |
| Quarantined tests have an issue link and an unexpired date | permanent quarantine | scan for the quarantine helper | #130 |
Sub-choice: enforcing "no network"
- Env only (weakest): set
HTTP_PROXY/HTTPS_PROXYto a dead port (http://127.0.0.1:9) innewEnvand a unitTestMain. Catches Go HTTP clients that honour proxy env (including the spawnedmcpxbinary); misses raw dials and anything inNO_PROXY. - In-process dialer guard: replace
http.DefaultTransport'sDialContextinTestMainto refuse non-loopback. Precise for unit tests; cannot reach themcpxsubprocess or the JS runtimes. - No network in the CI test step (recommended, plus the env layer locally): run tests in a network namespace (
unshare -rn/systemd-run --property=PrivateNetwork=yes/ a container with--network none) aftergo mod download. Catches everything, including deno/bun/node. Linux-only, so it lives in CI (Bump golang.org/x/crypto from 0.16.0 to 0.17.0 in /sources/launcher #95); macOS keeps the env layer.
Tasks
- Implement: settings effect guard (Phase 3: sync, blob gate, and relays #181 collision 15) -- completeness over
settings.Registry(), probes grouped by surface, a planted-inert negative test. Before or with Phase 2: membership, delegation, groups, and server config #172. - Implement: enumerate the exec/run flag set in
TestEveryExecFlagChangesSomething(completeness + exemptions), rows for the Update Rust crate proc-macro2 to 1.0.81 - autoclosed #75 flags. - Implement: negative tests for existing checker-style guards (
defaults,parity,consumed). - Implement: no-network (env layer now; namespace via Bump golang.org/x/crypto from 0.16.0 to 0.17.0 in /sources/launcher #95).
- Implement: process/temp-dir sweep
TestMain(chore(deps): bump build/buck/preludes/buck2-prelude fromd956689toe7457f6#52 itself is fixed by Phase 5: tag event bodies must travel with the event #182; this is the suite-wide backstop and the temp-dir cleanup). - Decide which other candidates become issues.
Open questions
- Should the completeness half of an e2e guard be a separate unit test (runs at T1), or stay inside the e2e test (runs only in CI)? Proposed: separate.
Does the "declared but never read" audit produce a registry-level guard (B/C) or an AST scan (A withAnswered by Phase 2: delegated Manage cannot add members #171: a regex scan (A's mechanism,go/ast)?consumed_test.go:54,60) over registry enumeration. The effect guard above is the C half it lacks.- Which settings cannot be probed from Go at all (plugin-scoped ones read only by TypeScript,
paths.config)? Do they get a TypeScript-side probe in the plugin'sbun test, which CI does not run yet (Bump dezren39/nix flake input to 3499ed3 — opencode now defaults to v2 #141)? - Where do exemption lists live: in the test file (current style) or in a data file several guards share?
Ease · Impact · Related
Ease M (policy S; each guard S-M) · Impact high
Depends on / related: #74, #75, #76, #65, #52 (closed, fixed in #182), #56 (the defaults-guard miss), #69/#70/#73 (the shared-path class), #97 (integration inventory), #95 (no-network CI step), #93 (generators for probe inputs); #171 (merged: the read guard), #172 (in flight: its inverse; lands with or after the effect guard), #179 (in flight: the registry.pageSize instance), #181 (collision 15, owned here); siblings #127 (T3 guards list), #128, #130, #131.
Security/auth deferred; see tracker.
Acceptance criteria
- The existing guard tests are inventoried on this issue, with where each leaks (class members it cannot catch) named.
- The rule for when a bug "is a class" is recorded, and the house style for writing guards is pinned.
- The Phase 3: sync, blob gate, and relays #181 collision-15 guard — a setting must be able to change behaviour, not merely be read — is landed, or scheduled before/with Phase 2: membership, delegation, groups, and server config #172 as the body requires.
- The guards worth adding next are listed, with follow-up issues filed for any not landed here.
- The work described in the Summary/Scope above is delivered via a branch + PR (no direct push to main); verification runs in GitHub Actions CI, not on the sandbox.
- Evidence (CI links or guard output as applicable) is recorded on this issue before it is closed.
Activity
developing-today commentedon Oct 28, 2026
CODE-896
dezren39 commentedon Oct 28, 2026
History carried over from dezren39/darwin-nix#129 (verbatim, oldest first):
@dezren39 · 2026-09-30T06:51:57Z
The in-flight "declared but never read" audit named here is now #171; its inverse ("read but never declared") is #172.
@dezren39 · 2026-09-30T07:21:19Z
Integration pass. Placed in the DAG rooted at #181.
@dezren39 · 2026-09-30T11:06:11Z
Collision ownership (issues pass, 2026-09-30, at 05c78b2). #181 listed these cross-theme collisions with no owner. Each now has one:
Where this issue is the owner, the decision is made here; where it is not, it adopts the owner's decision rather than making its own.
@dezren39 · 2026-09-30T12:03:34Z
Issues pass (2026-09-30, at 05c78b2).
unread_test.goprobes. It needs a completeness test at unit speed, ratcheted exemptions, and a negative test that plants an inert setting. Timing: before or with Phase 2: membership, delegation, groups, and server config #172 (in flight). It is now the first task.registry.pageSizeis read but consulted only insideif limit <= 0(registry.go:154-155). The CLI defaults--limitto 20, and the daemon falls back toregistry.limit, so it passed Phase 2: delegated Manage cannot add members #171's regex-scan guard while inert (Phase 3: /id-sync/1 is used by both range and LogSync handshakes #179).consumed_test.go(3 guards) andunread_test.go(9 effect tests) are added, and so is Phase 5: tag event bodies must travel with the event #182'sTestEnvCleanupStopsTheDaemonItStarted. Re-citede2e_test.go:2877/1879/534andcommands.go:435-481(23 flags). Fixedsettings.All()tosettings.Registry(): the former does not exist. chore(deps): bump build/buck/preludes/buck2-prelude fromd956689toe7457f6#52 is fixed, so the process-sweep row and task are now the suite-wide backstop only.d956689tocb5dc60#108 runtime capability matrix (an exit-0 check would not have caught Bump build/buck/preludes/buck2-prelude fromd956689tocb5dc60#108; Deno's permission flags have the same exposure) and "every documented capability exists" (registry parity where one exists, Update dependency @chenglou/pretext to ^0.0.9 [SECURITY] #132's inventory for prose, no prose-linting). Also added leak 6, which Bump unsafe-libyaml from 0.2.8 to 0.2.10 in /sources/pavex/examples/realworld #99 cites:TestEverySettingIsReachableFromAllThreeSurfaceschecks name shape, not reachability. And added a candidate for the Update Rust crate bacon to 2.14.0 #69/Update Rust crate globwalk to 0.9.0 #70/Update Rust crate libtest-mimic to 0.8.0 #73 shared-path class.@dezren39 · 2026-09-30T12:16:50Z
Issues pass (2026-09-30, architect follow-up). New leak 7: an allowlist entry's reason is unchecked prose, and
consumed_test.go:67-70's reason forpaths.configis false (nothing readsMCPX_PATHS_CONFIG; see #5, #159 item 19). Recommend reasons that name a checkable mechanism.dezren39 commentedon Oct 29, 2026
Intake review
🤖 Agent note · state: filed
64 remaining items
dezren39 commentedon Nov 4, 2026
🤖 Agent note · parked
dezren39 commentedon Nov 4, 2026
🤖 Agent note · handed off
dezren39 commentedon Nov 4, 2026
🤖 Agent note · handed off
dezren39 commentedon Nov 5, 2026
Additional concrete setting-effect case found in current mcpx audit
Please include the artifacts.enabled daemon-exec path in the guard remainder. docs/audit/surface.md:58 records a reproduction with mcpx daemon --artifacts-enabled=false, but internal/daemon/routes_exec.go execSettings() rebuilds exec settings and appears to drop the daemon setting. internal/settings/effect.go exempts the artifacts setting from the effect probe, so the class guard will not catch this instance unless the exemption is removed or paired with an explicit effect test.
Suggested regression: start the daemon with artifacts disabled, invoke daemon exec with a script that would otherwise produce an artifact, and assert the disabled mode creates/publishes no artifact while enabled mode still does. This fits #1971's existing “declared setting must change behavior” scope; no new issue is needed.
dezren39 commentedon Nov 5, 2026
🤖 Agent note · handed off