mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-08-28 16:45:22 +00:00
333 lines
27 KiB
Markdown
333 lines
27 KiB
Markdown
---
|
||
issue: 510
|
||
issue_title: "Thread an injected platform/path-semantics seam through the bash path pipeline"
|
||
---
|
||
|
||
# Thread a PathNormalizer collaborator through the bash path pipeline
|
||
|
||
## Release Recommendation
|
||
|
||
**Release:** ship independently
|
||
|
||
This issue is not part of a named roadmap batch.
|
||
It is a behavior-preserving `refactor:`, so it does not cut a release on its own — it lands on `main` and auto-batches into the next `feat:`/`fix:` release.
|
||
In practice that release is [#508]'s `fix:` (the Windows drive-letter gate), which is sequenced to land immediately after this refactor and depends on the seam it establishes.
|
||
The rationale must not claim this issue itself cuts a release.
|
||
|
||
## Problem Statement
|
||
|
||
The bash path pipeline reads `process.platform` ambiently — directly, or through the host-bound `node:path` import — in roughly six interior modules, instead of injecting the platform once at the edge and threading it through.
|
||
The package already discovered the injectable-platform pattern (`isPathWithinDirectory`, `rule.ts` `shouldFoldCase`) but applied it only to a few leaf predicates, never end-to-end.
|
||
This half-built seam is the root cause behind the recurring Windows-path bugs ([#382], [#345], [#418], [#508]): each fix either hand-rolls a platform check that drifts from `node:path` (the [#508] `startsWith("/")` drift) or reaches for a fragile module-level `vi.mock("node:path")` to test Windows behavior on a POSIX CI.
|
||
|
||
The correction is to stop threading platform *knobs* into the pipeline and re-deriving path forms *during* evaluation.
|
||
Instead, inject a single collaborator constructed at the edge with the platform (and the working directory) baked in, hand it raw path tokens, and let it return the normalized values the gates expect.
|
||
Path interpretation — "by which platform's rules, and against which working directory, do we read this path?"
|
||
— becomes one collaborator's job, prepared before evaluation rather than scattered through it.
|
||
|
||
## Goals
|
||
|
||
- Introduce a `PathNormalizer` collaborator constructed once at the edge, carrying the platform and the session working directory, that produces the pipeline's prepared path values (`AccessPath`s) and answers the platform-dependent routing questions (absoluteness, `cd`-base resolution, working-directory containment).
|
||
- Thread `PathNormalizer` through the bash path pipeline: `BashProgram.parse` → `cwd-projection.ts` → the per-tool and external-directory path gates, replacing the host-bound `node:path` import, the inline `process.platform` ternaries, and the ad-hoc `cwd` threading.
|
||
- Complete the half-built leaf seam: `normalizePathForComparison`, `canonicalNormalizePathForComparison`, `canonicalizePath`, and `AccessPath.forPath`/`forLiteral` accept the platform flavor instead of reading `process.platform` inline.
|
||
- Eliminate every interior `process.platform` read (`path-utils.ts`, `subagent-context.ts`, `rule.ts` defaults) so the only reader is the composition-root edge, and add a lint guard that makes that enforceable.
|
||
- Make Windows path behavior testable on a POSIX CI by injecting a `win32` `PathNormalizer` — no `vi.mock("node:path")`.
|
||
|
||
This is a **behavior-preserving refactor** (`refactor:`).
|
||
Every interior op converted to a `PathNormalizer` call already used the host-bound `node:path` (so it already behaved as `win32` on Windows and `posix` on POSIX); the one POSIX-hard-coded drift — `isRelativeCandidate`'s `startsWith("/")` — is intentionally **left as-is** and deferred to [#508] (see Non-Goals).
|
||
On the POSIX CI there is no observable change; the `win32` seam is newly exercised only by injected-platform unit tests.
|
||
|
||
## Non-Goals
|
||
|
||
- **The `isRelativeCandidate` → injected `isAbsolute` conversion.**
|
||
Converting `cwd-projection.ts`'s `isRelativeCandidate` from the hand-rolled `!candidate.startsWith("/")` to `!normalizer.isAbsolute(candidate)` changes Windows routing for drive-letter tokens — it is the *semantic* Windows correction, not a structural threading change.
|
||
It is deferred to [#508] (whose plan already folds it into its step 1), so this issue stays strictly behavior-preserving.
|
||
`isRelativeCandidate` remains pure host-independent string matching (no `process.platform`, no `node:path`), so it does not block the lint guard.
|
||
- **Recognizing Windows drive-letter shapes in the classifiers.**
|
||
That is the [#508] fix (`token-classification.ts`); this issue only builds the seam [#508] lands on.
|
||
- **Dissolving `path-utils.ts` into the access-intent domain.**
|
||
That relocation is Phase 7 Step 4 ([#505]).
|
||
`PathNormalizer` is a facade *over* the (now platform-parameterized) `path-utils` and `AccessPath` primitives; it does not move them.
|
||
[#505] can later relocate those internals behind the facade without changing it.
|
||
- **Config-pattern path handling and prompt-input paths.**
|
||
Out of scope ([#487] residual work); the gates' pattern matching stays as-is.
|
||
- **Extending the `process.platform` lint guard package-wide or to other packages.**
|
||
The guard is scoped to `pi-permission-system/src`; no concrete need is named for widening it.
|
||
|
||
## Background
|
||
|
||
Relevant modules and the platform reads they carry today:
|
||
|
||
- `src/path-utils.ts` — `normalizePathForComparison` and `canonicalNormalizePathForComparison` hard-code `process.platform === "win32"` inline; `isPathWithinDirectory` and `isPiInfrastructureRead` already take an injectable `platform: NodeJS.Platform = process.platform`.
|
||
- `src/canonicalize-path.ts` — `canonicalizePath` splits and rejoins on `/` only (POSIX-only); takes no flavor.
|
||
- `src/access-intent/access-path.ts` — `AccessPath.forPath`/`forLiteral` are static factories delegating to the three `path-utils` normalizers above; no platform option.
|
||
- `src/access-intent/bash/cwd-projection.ts` — imports `isAbsolute`/`join`/`resolve` from the host-bound `node:path`, hand-rolls `isRelativeCandidate` with `startsWith("/")`, and calls `isPathWithinDirectory(canonical, normalizedCwd)` **without** a platform argument (so even the one good seam falls back to the host).
|
||
- `src/access-intent/bash/program.ts` — `BashProgram.parse(command, cwd)` threads a bare `cwd` into the projection.
|
||
- `src/subagent-context.ts` — `normalizeFilesystemPath` and `isPathWithinDirectoryForSubagent` re-hand-roll `process.platform === "win32"` for case-folding and the separator.
|
||
- `src/rule.ts` — `evaluate` and `evaluateMostRestrictive`/`evaluateFirst`/`pathMatchOptions` decide case-insensitive matching via an injectable `platform: NodeJS.Platform = process.platform` (already injectable, but every caller relies on the default rather than supplying it).
|
||
|
||
Two distinct edges (this shapes the wiring):
|
||
|
||
- **`process.platform` is process-global** and known when the extension factory runs (`index.ts`).
|
||
It can be read **once** there and injected into `PermissionManager` (for `rule.ts` matching) and `PermissionSession` (to build the normalizer).
|
||
- **`cwd` is session-scoped.**
|
||
It is not available in the factory body; it arrives at `session_start` as `ctx.cwd` (`handlers/lifecycle.ts` → `session.resetForNewSession(ctx)`; `session.getRuntimeContext()?.cwd`).
|
||
So `PathNormalizer` is constructed when the session resets, not in the factory body.
|
||
|
||
Constraints from AGENTS.md / package skill:
|
||
|
||
- `code-design`: do not read `process.platform` inside library/utility functions — accept it as a parameter (or, here, bake it into the collaborator at the edge).
|
||
- The bash gates share a single `BashProgram.parse` per `evaluate` ([#308]); the seam must not reintroduce a re-parse.
|
||
- The pipeline already pulls session-scoped values (`getInfrastructureReadDirs`, `getToolPreviewLimits`) from `PermissionSession` via the `ToolCallGateInputs` interface; the normalizer follows that established pattern (`getPathNormalizer()`).
|
||
- `docs/architecture/architecture.md` inline-copies the `rule.ts` types; touching `rule.ts`'s signature means checking that listing.
|
||
|
||
## Design Overview
|
||
|
||
### The `PathNormalizer` collaborator
|
||
|
||
A single value-bound collaborator, constructed at the edge with the two ambient inputs baked in, and handed raw tokens thereafter.
|
||
|
||
```typescript
|
||
export class PathNormalizer {
|
||
constructor(
|
||
private readonly platform: NodeJS.Platform,
|
||
private readonly cwd: string,
|
||
) {}
|
||
|
||
/** Build an AccessPath for a token, resolved against `resolveBase` (default cwd). */
|
||
forPath(pathValue: string, options?: { resolveBase?: string }): AccessPath;
|
||
|
||
/** Build a literal-only AccessPath (unknown base after a non-literal `cd`). */
|
||
forLiteral(literal: string): AccessPath;
|
||
|
||
/** Platform-aware absoluteness (`win32` vs `posix` rules). */
|
||
isAbsolute(pathValue: string): boolean;
|
||
|
||
/** Resolve a `cd`-folded offset against the baked cwd (platform-aware). */
|
||
resolveBase(offset: string): string;
|
||
|
||
/** Join a `cd` offset with a relative target (platform-aware), for cd-folding. */
|
||
joinBase(offset: string, target: string): string;
|
||
|
||
/** Containment of `pathValue` within `directory` (platform-aware). */
|
||
isWithinDirectory(pathValue: string, directory: string): boolean;
|
||
|
||
/** Canonical (symlink-resolved) outside-cwd test against the baked cwd. */
|
||
isOutsideWorkingDirectory(pathValue: string): boolean;
|
||
}
|
||
```
|
||
|
||
The methods are intention-revealing domain operations ("is this path absolute *under our platform*", "resolve a `cd` offset *against our cwd*"), not a generic re-export of `node:path`.
|
||
Internally `PathNormalizer` selects `path.win32`/`path.posix` and the case-fold once, and delegates to the platform-parameterized `path-utils`/`canonicalize-path`/`AccessPath.forPath` primitives.
|
||
No consumer sees `platform`, selects a flavor, or threads `cwd`.
|
||
|
||
### Consumer call sites (Tell-Don't-Ask check)
|
||
|
||
Projection (`cwd-projection.ts`) — hands the normalizer a token, gets a prepared `AccessPath`; no `cwd`, no `node:path`:
|
||
|
||
```typescript
|
||
// buildRuleCandidatePath
|
||
if (base.kind === "unknown" && isRelativeCandidate(candidate)) {
|
||
return normalizer.forLiteral(normalizePathPolicyLiteral(candidate));
|
||
}
|
||
const resolveBase =
|
||
base.kind === "known" ? normalizer.resolveBase(base.offset) : undefined;
|
||
return normalizer.forPath(candidate, { resolveBase });
|
||
```
|
||
|
||
`foldCd` asks the normalizer the platform questions instead of importing them:
|
||
|
||
```typescript
|
||
if (normalizer.isAbsolute(target)) return { kind: "known", offset: target };
|
||
if (base.kind === "unknown") return UNKNOWN_BASE;
|
||
return { kind: "known", offset: normalizer.joinBase(base.offset, target) };
|
||
```
|
||
|
||
Per-tool path gate (`path.ts`) — the ambient `tcc.cwd` is gone; the session's normalizer already carries it:
|
||
|
||
```typescript
|
||
const accessPath = normalizer.forPath(filePath);
|
||
```
|
||
|
||
The normalizer reaches the gates the same way the infra-dir list does — the pipeline pulls it from the session per `evaluate`:
|
||
|
||
```typescript
|
||
// tool-call-gate-pipeline.evaluate
|
||
const normalizer = this.inputs.getPathNormalizer();
|
||
const bashProgram =
|
||
tcc.toolName === "bash" && command
|
||
? await BashProgram.parse(command, normalizer)
|
||
: null;
|
||
```
|
||
|
||
### Edge wiring
|
||
|
||
```typescript
|
||
// index.ts (factory body) — the single process.platform read
|
||
const hostPlatform = process.platform; // eslint guard exemption: composition root
|
||
const permissionManager = new PermissionManager({ agentDir, platform: hostPlatform });
|
||
session = new PermissionSession(/* …, */ hostPlatform);
|
||
```
|
||
|
||
```typescript
|
||
// PermissionSession.resetForNewSession(ctx) — cwd now known
|
||
this.pathNormalizer = new PathNormalizer(this.platform, ctx.cwd);
|
||
```
|
||
|
||
`PermissionSession` exposes `getPathNormalizer(): PathNormalizer`, added to the `ToolCallGateInputs` interface alongside the existing query methods.
|
||
|
||
### Lint guard
|
||
|
||
A flat-config block scoped to `packages/pi-permission-system/src/**/*.ts` forbids `process.platform`, exempting only the composition root (`index.ts`):
|
||
|
||
```javascript
|
||
{
|
||
files: ["packages/pi-permission-system/src/**/*.ts"],
|
||
ignores: ["packages/pi-permission-system/src/index.ts"],
|
||
rules: {
|
||
"no-restricted-syntax": ["error", {
|
||
selector: 'MemberExpression[object.name="process"][property.name="platform"]',
|
||
message: "Read process.platform only at the composition root; inject the platform (PathNormalizer / rule platform) into interior modules.",
|
||
}],
|
||
},
|
||
}
|
||
```
|
||
|
||
`process.env` (used legitimately by `subagent-context.ts` for subagent env hints) is untouched — the guard targets `process.platform` only.
|
||
|
||
### Edge cases preserved
|
||
|
||
- **Behavior parity per platform.**
|
||
Every converted op already used host `node:path` / `process.platform`; with the default platform = host, each produces the identical result.
|
||
`canonicalizePath` gains `win32`-aware splitting — a no-op on POSIX (splits on `/` as before) and a latent correctness gain on Windows that becomes observable only once [#508] feeds drive tokens through it; validated here by injected-`win32` unit tests.
|
||
- **[#393] literal-only guard.**
|
||
A relative candidate under an unknown `cd` base still routes to `forLiteral` — `isRelativeCandidate` is unchanged.
|
||
- **[#418] lexical-vs-canonical split.**
|
||
`AccessPath`'s accessors and the projection's "boundary uses canonical, returned value is lexical" logic are unchanged; only their construction is platform-parameterized.
|
||
- **[#308] single parse.**
|
||
`BashProgram.parse` still parses once; it gains the normalizer in place of `cwd`.
|
||
|
||
## Module-Level Changes
|
||
|
||
- `src/path-utils.ts` — add `platform: NodeJS.Platform` to `normalizePathForComparison` and `canonicalNormalizePathForComparison` (replacing the inline `process.platform === "win32"`), threading it to the private absolute/relative helpers and the existing `isPathWithinDirectory` call; remove the `= process.platform` *inline reads* (the injectable defaults on `isPathWithinDirectory`/`isPiInfrastructureRead` are removed in the lint-guard step once all callers supply it).
|
||
- `src/canonicalize-path.ts` — `canonicalizePath` accepts the platform flavor (or `PlatformPath`) and splits/rejoins on the platform separator (`win32`-aware), defaulting to host.
|
||
- `src/access-intent/access-path.ts` — `forPath`/`forLiteral` accept a `platform` option, threaded to the three normalizers.
|
||
- `src/path-normalizer.ts` — **new** `PathNormalizer` class (platform + cwd baked) wrapping `AccessPath.forPath`/`forLiteral` and exposing `isAbsolute`/`resolveBase`/`joinBase`/`isWithinDirectory`/`isOutsideWorkingDirectory`.
|
||
- `src/access-intent/bash/cwd-projection.ts` — drop the `import { isAbsolute, join, resolve } from "node:path"`; `projectExternalPaths`/`projectRuleCandidates`/`buildRuleCandidatePath`/`foldCd` take a `PathNormalizer` in place of the `cwd` parameter and call its methods; `isRelativeCandidate` stays `startsWith` (deferred to [#508]).
|
||
- `src/access-intent/bash/program.ts` — `BashProgram.parse(command, normalizer)` replaces `cwd`; thread `normalizer` into the projection calls.
|
||
- `src/handlers/gates/bash-path-extractor.ts` — `BashProgram.parse(command, normalizer)`.
|
||
- `src/handlers/gates/tool-call-gate-pipeline.ts` — pull `getPathNormalizer()` from `inputs`; pass it to `BashProgram.parse` and the path-gate producers; add `getPathNormalizer` to the `ToolCallGateInputs` interface.
|
||
- `src/handlers/gates/path.ts`, `src/handlers/gates/external-directory.ts`, `src/handlers/gates/bash-external-directory.ts`, `src/handlers/gates/bash-path.ts` — replace `AccessPath.forPath({ cwd: tcc.cwd })` with the injected `normalizer.forPath(...)`.
|
||
- `src/permission-session.ts` — constructor accepts `platform`; `resetForNewSession` builds the `PathNormalizer` from `{ platform, ctx.cwd }`; add `getPathNormalizer()`; declare it on the `ToolCallGateInputs` it implements.
|
||
- `src/permission-manager.ts` — constructor accepts `platform`; supply it to the `evaluate`/`evaluateFirst`/`evaluateAnyValue` calls.
|
||
- `src/rule.ts` — remove the `= process.platform` defaults on `evaluate` and `evaluateMostRestrictive`/`evaluateFirst` (thread `platform` from the manager); `pathMatchOptions`/`ruleMatches` already take it.
|
||
- `src/subagent-context.ts` — `normalizeFilesystemPath` and `isSubagentExecutionContext` accept `platform` (injected from the composition root via the lifecycle/registry caller); remove the hand-rolled `process.platform === "win32"` branches.
|
||
- `src/index.ts` — read `process.platform` once; inject into `PermissionManager`, `PermissionSession`, and the `subagent-context` caller path.
|
||
- `eslint.config.js` (repo root) — add the `process.platform` `no-restricted-syntax` block scoped to `pi-permission-system/src`, exempting `index.ts`.
|
||
- `packages/pi-permission-system/docs/architecture/architecture.md` — record the `PathNormalizer` seam and its relationship to Phase 7 [#505]; update the `rule.ts` type/signature listing if the `evaluate` signature note changes; add a roadmap entry referencing [#510] (the issue notes this is "probably a roadmap step").
|
||
- `.pi/skills/package-pi-permission-system/SKILL.md` — update the path/platform handling notes (the "check how pi-coding-agent solves it" debugging note and any `process.platform` guidance) to point at `PathNormalizer` as the single home; note the lint guard.
|
||
|
||
A grep of `src/`, `test/`, `architecture.md`, and the package SKILL confirms `process.platform` lives only in the modules listed above; `AccessPath.forPath`/`forLiteral` callers are `cwd-projection.ts` and the four gates listed (all migrated); `BashProgram.parse` callers are `tool-call-gate-pipeline.ts` and `bash-path-extractor.ts` (both migrated).
|
||
|
||
## Test Impact Analysis
|
||
|
||
1. **New tests enabled.**
|
||
Injected-`win32` unit tests across the seam **without** `vi.mock("node:path")`: `path-utils` normalizers and `canonicalizePath` driven with `platform: "win32"`; `AccessPath.forPath` with the `win32` option; a `PathNormalizer` unit suite (both flavors) covering `forPath`/`forLiteral`/`isAbsolute`/`resolveBase`/`joinBase`/containment; and an end-to-end projection/external-directory assertion driving a `win32` `PathNormalizer` through `BashProgram.parse` (the seam [#508] then exercises with drive tokens).
|
||
2. **Redundant tests.**
|
||
Any existing Windows-path test relying on a `process.platform` stub or `vi.mock("node:path")` for these modules can be simplified to inject a `win32` `PathNormalizer`/platform (none currently exist for the bash pipeline — `skill-prompt-sanitizer.test.ts`'s `node:path` mock is a different module, out of scope).
|
||
3. **Tests that must stay as-is.**
|
||
The existing POSIX projection / `bash-external-directory` / `program` / `access-path` / `path-utils` / `rule` suites are the regression guard that POSIX behavior is unchanged; they stay green untouched (modulo the mechanical signature migration — `BashProgram.parse(command, cwd)` → `(command, normalizer)`, `AccessPath.forPath({cwd})` call shape).
|
||
|
||
## Invariants at risk
|
||
|
||
This change touches surfaces earlier phases refactored; their documented outcomes must not regress:
|
||
|
||
- **[#418]** (lexical-vs-canonical conflation is a compile error) — pinned by `test/access-intent/access-path.test.ts` and the projection's lexical-return/canonical-boundary assertions in `bash-external-directory.test.ts`.
|
||
Preserved: only construction is platform-parameterized.
|
||
- **[#393]** (relative candidate under unknown `cd` base stays literal-only) — pinned by the unknown-base projection tests in `program.test.ts`/`bash-external-directory.test.ts`.
|
||
Preserved: `isRelativeCandidate` unchanged.
|
||
- **[#308]** (single `BashProgram.parse` per `evaluate`) — pinned by `program.test.ts` and the pipeline tests.
|
||
Preserved: parse count unchanged.
|
||
- **[#382]** (`win32` boundary values lowercased) — pinned by the `win32` `path-utils`/`AccessPath` tests; the `PathNormalizer` `win32` suite extends this coverage.
|
||
- **[#478]** (single resolver/manager resolution entry point) — the manager's new `platform` field does not add a resolution method; pinned by the manager/resolver suites.
|
||
|
||
## TDD Order
|
||
|
||
1. `refactor:` Thread the platform flavor through the leaf normalizers (preparatory, additive).
|
||
Red: `path-utils.test.ts` cases asserting `normalizePathForComparison`/`canonicalNormalizePathForComparison` with `platform: "win32"` lowercase and use `win32` resolution; `canonicalize-path.test.ts` cases asserting `win32`-separator splitting.
|
||
Green: add the `platform` parameter (defaulting to host) to `normalizePathForComparison`, `canonicalNormalizePathForComparison`, and `canonicalizePath`, threading to the private helpers.
|
||
Commit: `refactor(pi-permission-system): accept platform flavor in path normalizers (#510)`.
|
||
|
||
2. `refactor:` Add the platform option to `AccessPath.forPath`/`forLiteral`.
|
||
Red: `access-path.test.ts` cases building a `win32` `AccessPath` and asserting `value`/`matchValues`/`boundaryValue`.
|
||
Green: add the `platform` option, threaded to the normalizers.
|
||
Commit: `refactor(pi-permission-system): thread platform option through AccessPath factory (#510)`.
|
||
|
||
3. `feat:` Introduce the `PathNormalizer` collaborator (not yet wired).
|
||
Red: `path-normalizer.test.ts` (both flavors) covering `forPath`/`forLiteral`/`isAbsolute`/`resolveBase`/`joinBase`/`isWithinDirectory`/`isOutsideWorkingDirectory`.
|
||
Green: add `src/path-normalizer.ts` wrapping the platform-parameterized primitives.
|
||
Commit: `feat(pi-permission-system): add PathNormalizer collaborator (#510)`.
|
||
|
||
4. `refactor:` Build the normalizer at the session edge and expose it.
|
||
Red: `permission-session` test asserting `getPathNormalizer()` returns a normalizer bound to the reset cwd; composition-root test asserting the single platform read flows to session + manager.
|
||
Green: `index.ts` reads `process.platform` once and injects it; `PermissionSession` constructor takes `platform`, `resetForNewSession` builds the normalizer, `getPathNormalizer()` added to the class and the `ToolCallGateInputs` interface.
|
||
Commit: `refactor(pi-permission-system): construct PathNormalizer at the session edge (#510)`.
|
||
|
||
5. `refactor:` Migrate the bash projection and `BashProgram.parse` onto the normalizer.
|
||
Red/Green together (signature change breaks call sites in one commit): `cwd-projection.ts` and `BashProgram.parse` take a `PathNormalizer` in place of `cwd`; drop the `node:path` import; update the two `parse` call sites (`tool-call-gate-pipeline.ts`, `bash-path-extractor.ts`) and migrate `program.test.ts`/`bash-external-directory.test.ts` fixtures (lift-and-shift: pass a host-default `PathNormalizer`).
|
||
Add the end-to-end `win32`-normalizer projection assertion.
|
||
Commit: `refactor(pi-permission-system): drive bash path projection through PathNormalizer (#510)`.
|
||
|
||
6. `refactor:` Migrate the per-tool and external-directory path gates onto the session normalizer.
|
||
Red/Green: `path.ts`, `external-directory.ts`, `bash-external-directory.ts`, `bash-path.ts` use `getPathNormalizer()`/the threaded normalizer instead of `AccessPath.forPath({ cwd: tcc.cwd })`; update gate tests.
|
||
Commit: `refactor(pi-permission-system): route path gates through the session PathNormalizer (#510)`.
|
||
|
||
7. `refactor:` Inject the platform into `rule.ts` matching.
|
||
Red/Green: remove the `= process.platform` defaults on `evaluate`/`evaluateMostRestrictive`/`evaluateFirst`; `PermissionManager` takes `platform` and supplies it; update `rule.test.ts`/`permission-manager` tests and the `architecture.md` `rule.ts` listing if the signature note changes.
|
||
Commit: `refactor(pi-permission-system): inject platform into rule evaluation (#510)`.
|
||
|
||
8. `refactor:` Inject the platform into `subagent-context.ts`.
|
||
Red/Green: `normalizeFilesystemPath`/`isSubagentExecutionContext` accept `platform` from the composition-root caller; remove the hand-rolled branches; update `subagent-context.test.ts` and the composition-root wiring.
|
||
Commit: `refactor(pi-permission-system): inject platform into subagent context detection (#510)`.
|
||
|
||
9. `build:` Add the `process.platform` lint guard and remove the last interior defaults.
|
||
Red: confirm the guard fires on a temporary interior `process.platform` (sanity), then remove the now-unused injectable `= process.platform` defaults on `isPathWithinDirectory`/`isPiInfrastructureRead` (all callers supply it).
|
||
Green: add the scoped `no-restricted-syntax` block to `eslint.config.js`; run `pnpm run lint` + the full package suite to confirm `index.ts` is the only reader.
|
||
Commit: `build(pi-permission-system): forbid interior process.platform reads (#510)`.
|
||
|
||
10. `docs:` Record the seam.
|
||
Update `architecture.md` (the `PathNormalizer` seam, its relationship to Phase 7 [#505], a roadmap entry for [#510]) and `SKILL.md` (path/platform handling points at `PathNormalizer`; note the lint guard).
|
||
Commit: `docs(pi-permission-system): document the PathNormalizer platform seam (#510)`.
|
||
|
||
## Risks and Mitigations
|
||
|
||
- **`cwd` source change (per-call → baked).**
|
||
The pipeline currently reads `ctx.cwd` on every tool call; baking it into the session normalizer assumes `cwd` is stable within a session.
|
||
In Pi a session is bound to one project directory and `ctx.cwd` is that directory on every event (the package already treats `session.getRuntimeContext()?.cwd` as the session cwd), so this holds.
|
||
Mitigation: build/refresh the normalizer in `resetForNewSession` (which already runs on every `session_start`, including `/new`/`/resume`/`/fork`), so a session switch rebinds it; pin with a composition-root test that the normalizer's cwd tracks the reset ctx.
|
||
- **Accidental Windows behavior change.**
|
||
The point of the refactor is parity; the only POSIX-hard-coded drift (`isRelativeCandidate`) is deliberately left for [#508].
|
||
Mitigation: the converted ops all previously used host `node:path`; the POSIX suite stays green untouched, and the new `win32` tests assert the seam, not a host-default change.
|
||
- **`canonicalizePath` `win32` branch is newly reachable.**
|
||
Mitigation: it is a no-op on POSIX; the `win32` unit tests validate the new branch in isolation before [#508] exercises it end-to-end.
|
||
- **Large multi-step migration.**
|
||
Mitigation: lift-and-shift — additive seam first (steps 1–3), edge wiring (step 4), then consumer migration (steps 5–8) one surface per commit, each leaving the suite green; the lint guard (step 9) lands only after the last interior read is gone.
|
||
- **Overlap with Phase 7 [#505] (path-utils dissolution).**
|
||
Mitigation: `PathNormalizer` is a facade over `path-utils`, not a relocation; [#505] can later move the internals behind it without re-touching the seam.
|
||
|
||
## Open Questions
|
||
|
||
None blocking.
|
||
The collaborator shape (single `PathNormalizer` owning construction + routing, name confirmed), the `cwd`-baked construction edge, the behavior-preserving scope (defer `isRelativeCandidate` to [#508]), and the full enforcement scope (lint guard + `rule.ts`/`subagent-context` cleanup) were confirmed with the operator during planning.
|
||
|
||
[#308]: https://github.com/gotgenes/pi-packages/issues/308
|
||
[#345]: https://github.com/gotgenes/pi-packages/issues/345
|
||
[#382]: https://github.com/gotgenes/pi-packages/issues/382
|
||
[#393]: https://github.com/gotgenes/pi-packages/issues/393
|
||
[#418]: https://github.com/gotgenes/pi-packages/issues/418
|
||
[#478]: https://github.com/gotgenes/pi-packages/issues/478
|
||
[#487]: https://github.com/gotgenes/pi-packages/issues/487
|
||
[#505]: https://github.com/gotgenes/pi-packages/issues/505
|
||
[#508]: https://github.com/gotgenes/pi-packages/issues/508
|
||
[#510]: https://github.com/gotgenes/pi-packages/issues/510
|