Fail startup when NAD_POLICY names a file that is not there - #109
ColinkaMir wants to merge 2 commits into
Conversation
A missing policy file is only optional when nobody asked for that file. NAD_POLICY is an operator saying use this one, so a typo in it started the agent with no limits and no allowlist, which is the same silently-permit-everything state a malformed file already refuses to cause. loadPolicy could not tell the two cases apart because both arrived as the same path argument: process.env.NAD_POLICY || DEFAULT_PATH was resolved in the parameter default. The env override is now read inside the function, so ENOENT is refused only when a nonempty NAD_POLICY selected the file, and the default being absent still returns null. A dangling symlink needs no arm of its own: the open follows the link and misses the target, so it arrives as the same ENOENT, which a test now shows rather than a comment claiming it. It throws, and cli.mjs already catches a throw from loadPolicy, prints it and exits 1 before the wallet is touched. src/mcp.mjs follows the same rule and gets the behaviour with it. An explicit argument keeps its older meaning, because callers that pass a path have always owned the result and the suite depends on that. One existing harness had to move with this. test/native-tools-cli-exit.test.mjs pointed NAD_POLICY at an absent file to mean no policy, which is now an error; it writes an empty policy object instead, which keeps that harness's intent of a run with no rules. Three of the eight new tests fail on main: the refusal, the dangling symlink and the startup ordering. The other five pin behaviour that must not change, and they pass before and after.
portdeveloper
left a comment
There was a problem hiding this comment.
The missing-file guard works, but src/policy.mjs:32 trims the configured filesystem path before opening it. That can select a different policy and remove the intended rules.
I reproduced this with two files on Linux: policy.json contains {}, and policy.json (one trailing space) contains {"maxPerSend":"0.5"}. With NAD_POLICY set to the full path of the latter, loadPolicy() opens the former and hasRules() returns false. The old code reads the configured path literally. A whitespace-only override also now falls back to the default, although #108 requires a nonempty explicit override to fail if its file is missing.
Please preserve the exact nonempty environment value when selecting the file. Only an unset or empty override should use the optional default. Add a regression for a path with significant whitespace and cover a whitespace-only missing path; keep the missing default optional. The current "empty or blank override" test passes an explicit path argument, so it never exercises the environment-selection branch it claims to cover.
The existing 535 tests and Linux/Windows CI pass, but they do not cover this case.
… empty Review catch, and a real one: trimming NAD_POLICY before opening it can select a different file. On Linux 'policy.json ' and 'policy.json' are two paths, so a configured file carrying whitespace was swapped for its neighbour, and a policy with rules could quietly become one without any. The value is now used exactly as configured. The blank case moves with it. A whitespace-only override is not 'no override', it is a path somebody typed, so it takes the missing-file refusal like any other nonempty value. Only a completely empty or unset variable falls back to the optional default. The test that claimed to cover this passed an explicit path argument, which takes the other branch entirely and proved nothing about environment selection. It is replaced by three that drive the environment: an empty override falling back, a whitespace-only override refused, and the reviewer's own pair of files one trailing space apart, asserting that the file named is the file opened and that its rules survive. That last one skips on Win32, where the platform strips trailing spaces from path components. Both new cases fail on the previous commit.
|
Fixed in 2cf68e2, and you were right on all three points. The trim was mine and it was wrong for exactly the reason you give: the configured value is a filesystem path, and trimming it opens a different file. The blank case followed from the same mistake. A whitespace-only override is a path somebody typed, not an absence, so it now takes the missing-file refusal like any other nonempty value, which is what #108 asks for. On the vacuous test: you are right, it passed an explicit path argument and therefore never touched the environment branch it claimed to cover. It is replaced by three that drive the environment instead: an empty override falling back to the optional default, a whitespace-only override refused, and your own case with Both new cases fail on the previous commit, and the suite is 537 passing. |
Closes #108.
What changed
A missing policy file is only optional when nobody asked for that file.
NAD_POLICYis an operator saying "use this one", so a typo in it started the agent with no limits and no allowlist: the same silently-permit-everything state a malformed file already refuses to cause.loadPolicycould not tell the two cases apart, because both arrived as the samepathargument,process.env.NAD_POLICY || DEFAULT_PATHresolved in the parameter default. The override is now read inside the function, soENOENTis refused only when a nonemptyNAD_POLICYselected the file, and an absent default still returnsnull. A blank override counts as no override.A dangling symlink needs no arm of its own: the open follows the link and misses the target, so it arrives as the same
ENOENT. There is a test for that rather than a comment claiming it.It throws, and
cli.mjsalready catches a throw fromloadPolicy, prints it and exits 1 before the wallet is initialised, so nothing new is needed at the call site andsrc/mcp.mjsinherits the behaviour. An explicit argument keeps its older meaning: callers that pass a path have always owned the result, and the existing suite depends on it.One existing harness moved with it
test/native-tools-cli-exit.test.mjspointedNAD_POLICYat an absent file to mean "no policy", which is exactly what this change turns into an error. It now writes an empty policy object and points at that, which preserves the harness's intent, a run with no rules. I am flagging it rather than burying it, since it is the one place this change reaches outside its own tests.Verification
initializing walletline, because the ordering is the part that matters.os.tmpdir()andpath.join, and the symlink case skips rather than fails where the platform will not create one.npm test: 535 pass, 0 fail. Five consecutive runs of the new file, no flake.A correction from my last PR
In #107 I wrote that three failures in
test/native-tools-cli-exit.test.mjswere my local Node 20 against your Node 22. That was wrong. They were a staledist/: afternpm run buildthe whole suite passes here. The diagnosis did not affect that change, but the claim is in its description, so I would rather say so than leave it.Scope
Policy loading only: no spend-rule change, no transaction change, and the README sentence that said "no file means no policy" now spells out the override exception.