Context
Follow-up from the security hardening PR #488 (Codex review of bd84281). The follow-up review noted a remaining same-smell path: adapter doctor reads each manifest files[].path entry's on-disk content with a lexical join(cwd, entry.path) rather than through resolveWithinProject.
The manifest schema (RelativePosixPath) already rejects .. / absolute / drive-letter paths, so an entry cannot lexically point outside the project. But a manifest entry pointing at a path whose final component (or an ancestor) is a symlink to an outside file would still be followed on read — the same symlink-escape class the PR closed for readManifest/writeManifest and the context-pack reads.
Why this is a follow-up, not a blocker
adapter doctor does not echo file contents into its output (it hashes them and reports drift codes), so this is an information-* exposure-resistant* read, not a direct leak. Severity is lower than the PR's High findings. The PR deliberately scoped to the manifest file I/O + the install/upgrade trust paths to avoid widening the change.
Goal
Route doctor's per-entry manifest file reads (and any other join(cwd, manifestEntry.path) reads in the adapter command surface — e.g. adapter conformance, adapter doctor's listOwnedCandidates/detectContractDrift reads) through resolveWithinProject (or the same readWithinProject degrade helper used in src/core/pack/loaders.ts), so a symlinked manifest-tracked path is refused rather than followed.
Constraints
- Doctor must stay non-throwing / tolerant — a refused entry should become a diagnostic (e.g. an
ADAPTER_FILE_* issue or a dedicated containment issue), not an exit-3 throw.
- No behavior change for legitimate in-project files.
Acceptance
- A manifest entry whose path is a symlink to an outside file is NOT read through; doctor reports it as a diagnostic instead.
- Regression test with a symlinked manifest-tracked file.
- Same treatment audited for
adapter conformance.
Refs: PR #488, related ownership follow-up #487.
Context
Follow-up from the security hardening PR #488 (Codex review of
bd84281). The follow-up review noted a remaining same-smell path:adapter doctorreads each manifestfiles[].pathentry's on-disk content with a lexicaljoin(cwd, entry.path)rather than throughresolveWithinProject.The manifest schema (
RelativePosixPath) already rejects../ absolute / drive-letter paths, so an entry cannot lexically point outside the project. But a manifest entry pointing at a path whose final component (or an ancestor) is a symlink to an outside file would still be followed on read — the same symlink-escape class the PR closed forreadManifest/writeManifestand the context-pack reads.Why this is a follow-up, not a blocker
adapter doctordoes not echo file contents into its output (it hashes them and reports drift codes), so this is an information-* exposure-resistant* read, not a direct leak. Severity is lower than the PR's High findings. The PR deliberately scoped to the manifest file I/O + the install/upgrade trust paths to avoid widening the change.Goal
Route doctor's per-entry manifest file reads (and any other
join(cwd, manifestEntry.path)reads in the adapter command surface — e.g.adapter conformance,adapter doctor'slistOwnedCandidates/detectContractDriftreads) throughresolveWithinProject(or the samereadWithinProjectdegrade helper used insrc/core/pack/loaders.ts), so a symlinked manifest-tracked path is refused rather than followed.Constraints
ADAPTER_FILE_*issue or a dedicated containment issue), not an exit-3 throw.Acceptance
adapter conformance.Refs: PR #488, related ownership follow-up #487.