[agent] Filed by the scheduled architecture audit routine (CLI and core). Register: discussion #560 register.
Kind: bug. Source: new finding (register C43).
Problem
GlobalArgs::project_root states the rule: "Every command that reads more than one store must derive them from THIS root, so --manifest-path can never interleave two projects' state". CLI_CONTRACT's manifest_not_found row also says "All sources always come from the SAME project".
Only some commands follow it. Every command reads the manifest from resolved_manifest_path(), but the vendored ledger (.socket/vendor/state.json) is loaded from two different roots:
rollback is the sharpest case, as rollback.rs L1218-L1238 shows:
- It takes
apply.lock in the manifest's .socket/ through socket_dir_of(&manifest_path, &cwd).
- Under that lock it loads, reverts and saves the cwd project's vendored ledger.
- A concurrent default-manifest run in the cwd project locks a different file, so two processes can write the same ledger without mutual exclusion.
Proof by execution (debug build of 045d7ec, run twice; same results both times)
Setup: a/ is --cwd, and b/.socket/manifest.json is {"patches":{}}. Every command runs with --cwd a --manifest-path ../b/.socket/manifest.json (rollback and repair also get --offline).
| Command |
corrupt a/.socket/vendor/state.json |
corrupt b/.socket/vendor/state.json |
list --json |
success |
warning "unreadable vendor ledger … b/.socket/vendor/state.json" |
vex --json --output f |
error vendor_ledger_corrupt |
error no_patches (ledger never read) |
rollback --json |
partial_failure, warning vendor_state_unreadable naming a/… |
success |
repair --json |
partialFailure, event vendor_state_unreadable naming a/… |
success |
So on the same input, list reads project b's ledger, while vex, rollback and repair read project a's.
Symptoms / impact
- No existing issue covers this.
- Impact: with a foreign
--manifest-path, rollback and remove revert one project's agent-mode patches and the other project's vendored artifacts and ledger. vex attests a mix of both projects. rollback's lock doesn't cover the ledger it writes.
--manifest-path and SOCKET_MANIFEST_PATH are documented global options, so CI wrappers that pin the manifest location hit this.
Proposed change
- One rule: every vendored-ledger read and write derives its root from
GlobalArgs::project_root().
- Route
rollback, remove, repair, apply --check and vex through ProjectContext::new (or load_state(&common.project_root())).
- Delete the
ProjectContext::rooted(common, cwd) calls in scan and get, and the rooted constructor if it has no remaining caller.
scan/get vendored writes (scan/vendor_flow.rs, scan/hosted.rs, get.rs) save the ledger to the same root. Lockfile discovery stays where each command's contract puts it today. If a maintainer prefers to refuse a --manifest-path outside --cwd's project for multi-store commands, that is a contract change and should become a decision. The default fix just makes the code follow the rule it already documents.
Size and scope
- Files:
commands/{rollback,remove,repair,apply,vex,get,context}.rs, commands/scan/{mod,vendor_flow,hosted}.rs.
- Estimate: about 40–80 production lines changed, plus tests.
- Out of scope: consolidating
ProjectContext with a run-level context (C10).
Acceptance criteria
Dependencies
[agent] Filed by the scheduled architecture audit routine (CLI and core). Register: discussion #560 register.
Kind: bug. Source: new finding (register C43).
Problem
GlobalArgs::project_rootstates the rule: "Every command that reads more than one store must derive them from THIS root, so--manifest-pathcan never interleave two projects' state". CLI_CONTRACT'smanifest_not_foundrow also says "All sources always come from the SAME project".Only some commands follow it. Every command reads the manifest from
resolved_manifest_path(), but the vendored ledger (.socket/vendor/state.json) is loaded from two different roots:project_root()(follows--manifest-path)listandapplythroughProjectContext::new;vendor --check--cwdrollback;remove;repair;apply --check;vex(LoadedLedgers::load(&common.cwd, manifest_path));scanandgetthroughProjectContext::rooted(.., cwd)/ get, and the scan/get vendored paths (load_state(&common.cwd)inscan/vendor_flow.rs,scan/hosted.rs,get.rs)rollbackis the sharpest case, as rollback.rs L1218-L1238 shows:apply.lockin the manifest's.socket/throughsocket_dir_of(&manifest_path, &cwd).Proof by execution (debug build of
045d7ec, run twice; same results both times)Setup:
a/is--cwd, andb/.socket/manifest.jsonis{"patches":{}}. Every command runs with--cwd a --manifest-path ../b/.socket/manifest.json(rollback and repair also get--offline).a/.socket/vendor/state.jsonb/.socket/vendor/state.jsonlist --jsonb/.socket/vendor/state.json"vex --json --output fvendor_ledger_corruptno_patches(ledger never read)rollback --jsonpartial_failure, warningvendor_state_unreadablenaminga/…repair --jsonpartialFailure, eventvendor_state_unreadablenaminga/…So on the same input,
listreads project b's ledger, whilevex,rollbackandrepairread project a's.Symptoms / impact
--manifest-path,rollbackandremoverevert one project's agent-mode patches and the other project's vendored artifacts and ledger.vexattests a mix of both projects.rollback's lock doesn't cover the ledger it writes.--manifest-pathandSOCKET_MANIFEST_PATHare documented global options, so CI wrappers that pin the manifest location hit this.Proposed change
GlobalArgs::project_root().rollback,remove,repair,apply --checkandvexthroughProjectContext::new(orload_state(&common.project_root())).ProjectContext::rooted(common, cwd)calls inscanandget, and therootedconstructor if it has no remaining caller.scan/getvendored writes (scan/vendor_flow.rs,scan/hosted.rs,get.rs) save the ledger to the same root. Lockfile discovery stays where each command's contract puts it today. If a maintainer prefers to refuse a--manifest-pathoutside--cwd's project for multi-store commands, that is a contract change and should become a decision. The default fix just makes the code follow the rule it already documents.Size and scope
commands/{rollback,remove,repair,apply,vex,get,context}.rs,commands/scan/{mod,vendor_flow,hosted}.rs.ProjectContextwith a run-level context (C10).Acceptance criteria
load_state(/LoadedLedgers::load(call incrates/socket-patch-cli/srctakesproject_root()(or a value derived from it); a grep-style architecture test pins this.--cwdand a clean--manifest-path ../b/.socket/manifest.json,list,vex,rollback,removeandrepairall succeed; with the corruption moved tob, all of them report it.rollback --manifest-path ../b/.socket/manifest.jsonnever modifiesa/.socket/vendor/state.json.args.rsproject_root_*tests and thelistsame-project tests stay green.listrow to the--manifest-pathflag row, so it covers every command.Dependencies
--jsontop-levelerror(scan and get emit both a string and a {code, message} object) #704 (envelope) but doesn't conflict with them. Makes C10 (RunCtx) simpler.