refactor(#4653): replace the allowAbsolute flag with a named acceptance policy
Phase 3 of epic #4636, stage 3b. Satisfies #4653's criterion that `opts.allowAbsolute` become "a named acceptance policy on the predicate, not a per-call-site boolean". The flag was actively misleading at the call site. `{ allowAbsolute: true }` reads as "containment is relaxed here". It never was: an absolute path that resolves outside the root is rejected exactly as a traversal is. The flag only ever controlled whether an absolute candidate was CONSIDERED. On a security predicate that is the wrong thing for a reviewer to have to infer, and 31 call sites were asking them to infer it. PathAcceptance.RelativeOnly relative candidates only PathAcceptance.AbsoluteInsideRoot absolute accepted, containment unchanged The three exported wrappers take the policy and translate it inward. validatePath keeps its internal `{ allowAbsolute }` opts and its body untouched — the engine is not re-derived here either, only the exported surface is renamed. MEASURED, NOT ESTIMATED. 31 call sites across 10 files, counted by walking the AST with the repo's own @typescript-eslint/parser rather than grepping: a text match would have folded in the options-type declaration, default parameter values and comments. All 31 pass the literal `true`; none passes `false` or a dynamic value, so the migration is uniform and `RelativeOnly` is purely the existing default made nameable. audit.cts alone holds 18 of them. This migration is compiler-verified in a way the containment-value migration in the previous commit was not: the parameter type changed from an object to a string union, so any missed site is a build error rather than a silent behavioral difference. That is why a 31-site mechanical edit is acceptable in the phase whose stated risk is the width of mechanical change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -21,7 +21,7 @@ import { transitionCore } from './state-transition.cjs';
|
||||
import { writeSetComplete } from './write-set.cjs';
|
||||
import type { WriteSet } from './write-set.cjs';
|
||||
import { updateTableCell, resetQuickTaskRows, QUICK_TASKS_SECTION_ABSENT } from './markdown-table.cjs';
|
||||
import { requireSafePath } from './security.cjs';
|
||||
import { requireSafePath, PathAcceptance } from './security.cjs';
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports -- audit.cjs is an export= CommonJS module
|
||||
import auditMod = require('./audit.cjs');
|
||||
const { resolveQuickTaskSummaryFile } = auditMod;
|
||||
@@ -1640,7 +1640,7 @@ function listQuickTaskDirsForArchive(cwd: string): string[] {
|
||||
for (const entry of sourceEntries) {
|
||||
if (!entry.isDirectory()) continue; // excludes symlinks too — see MAJOR 3 note above
|
||||
try {
|
||||
requireSafePath(path.join(quickDir, entry.name), planningBase, 'quick task dir', { allowAbsolute: true });
|
||||
requireSafePath(path.join(quickDir, entry.name), planningBase, 'quick task dir', PathAcceptance.AbsoluteInsideRoot);
|
||||
} catch {
|
||||
continue; // symlink/escape attempt — never a candidate, in preview OR real run
|
||||
}
|
||||
@@ -1719,7 +1719,7 @@ function archiveQuickTaskDirectories(cwd: string, version: string): { archiveDir
|
||||
// rename are two separate filesystem observations, and an entry
|
||||
// that was a safe real directory at selection time could in theory
|
||||
// be swapped for a symlink before this loop reaches it.
|
||||
safeSrc = requireSafePath(src, planningBase, 'quick task dir', { allowAbsolute: true });
|
||||
safeSrc = requireSafePath(src, planningBase, 'quick task dir', PathAcceptance.AbsoluteInsideRoot);
|
||||
} catch {
|
||||
continue; // symlink/escape attempt — skip, not archived
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user