feat: vendor permission system source

This commit is contained in:
云服务部-叶林立
2026-08-19 14:35:19 +08:00
parent 198584daf8
commit 410c50a3e5
809 changed files with 157793 additions and 139 deletions
+231
View File
@@ -0,0 +1,231 @@
import { lstatSync } from "node:fs";
import type { PathFlavor } from "#src/path/path-flavor";
import { AccessPath } from "./access-intent/access-path";
import {
canonicalNormalizePathForComparison,
normalizePathForComparison,
normalizePathPolicyLiteral,
} from "./access-intent/path-normalization";
import { isPathOutsideWorkingDirectory } from "./path/path-containment";
import { isPiInfrastructureRead } from "./path/pi-infrastructure-read";
/**
* The interpreted effect of a literal `cd` target on the effective base, under
* the host platform's (and, on win32, Git Bash's) semantics.
*
* - `absolute` — the target names a resolvable absolute base (`value`); an
* earlier unknown base is recovered.
* - `relative` — the target folds into the current base.
* - `unknown` — the target is not deterministically resolvable (a win32
* non-mount POSIX absolute like `cd /tmp`, or a device), so the base becomes
* conservatively unknown.
*/
export type BashCdTarget =
| { readonly kind: "absolute"; readonly value: string }
| { readonly kind: "relative" }
| { readonly kind: "unknown" };
/**
* Path-interpretation collaborator, constructed once at the session edge with
* the two ambient inputs — the resolved {@link PathFlavor} and the session
* `cwd` — baked in, and handed raw path tokens thereafter.
*
* The bash path pipeline and the per-tool/external-directory gates ask this
* object the platform-dependent questions ("is this path absolute *under our
* flavor*?", "resolve this `cd` offset *against our cwd*") and receive prepared
* {@link AccessPath} values, instead of reading `process.platform` ambiently or
* threading `cwd` through every call. All platform semantics live on the
* injected `flavor`; this class holds no platform discriminator and no
* `win32`/`posix` branch — it delegates to `flavor` and the flavor-parameterized
* `path-containment` / `path-normalization` / `AccessPath` primitives.
*/
export class PathNormalizer {
/** Canonical form of the baked cwd, resolved once (the symlink target is stable per session). */
private readonly canonicalCwd: string;
constructor(
readonly flavor: PathFlavor,
private readonly cwd: string,
) {
this.canonicalCwd = canonicalNormalizePathForComparison(cwd, cwd, flavor);
}
/** Build an AccessPath for a token, resolved against `resolveBase` (default cwd). */
forPath(pathValue: string, options?: { resolveBase?: string }): AccessPath {
return AccessPath.forPath(pathValue, {
cwd: this.cwd,
resolveBase: options?.resolveBase,
flavor: this.flavor,
});
}
/** Build a literal-only AccessPath (unknown base after a non-literal `cd`). */
forLiteral(literal: string): AccessPath {
return AccessPath.forLiteral(literal);
}
/**
* Build an AccessPath for a bash-command token, applying Git Bash/MSYS
* semantics on a win32 host.
*
* Pi core always executes bash through Git Bash on Windows, so a POSIX-shaped
* absolute token carries MSYS semantics, not `node:path.win32` semantics. The
* flavor classifies the token's shape: on win32 the recognized safe device
* paths (`/dev/null`, `/dev/std{in,out,err}`) are preserved verbatim as
* devices instead of being resolved into `c:\dev\null`, and MSYS drive mounts
* (`/c/…`) are translated to their Windows equivalent (`C:\…`) before
* resolution; every other token delegates to {@link forPath}. On POSIX every
* token is `plain`, so this is a straight delegation to {@link forPath}.
*/
forBashToken(token: string, options?: { resolveBase?: string }): AccessPath {
const shape = this.flavor.bashTokenShape(token);
switch (shape.kind) {
case "device":
return AccessPath.forDevice(token);
case "drive-mount":
return this.forPath(shape.windowsPath, options);
case "posix-absolute":
// A non-mount POSIX absolute (`/tmp`, `/usr`) has an install-dependent
// Windows target this package cannot know, so it is kept literal: always
// external, matched and displayed as typed, never fabricated into
// `c:\tmp` (#533). The win32 path matcher folds separators on both the
// rule and the value (#653), so a natural `/tmp/*` rule matches the
// as-typed literal directly.
return this.forLiteral(normalizePathPolicyLiteral(token));
case "plain":
return this.forPath(token, options);
}
}
/** Platform-aware absoluteness (`win32` vs `posix` rules). */
isAbsolute(pathValue: string): boolean {
return this.flavor.impl.isAbsolute(pathValue);
}
/**
* Interpret a literal `cd` target's effect on the effective base.
*
* On win32 the target carries Git Bash/MSYS semantics: a drive mount
* (`cd /c/x`) resolves to a translated Windows base (`C:\x`), a non-mount
* POSIX absolute (`cd /tmp`) is not deterministically resolvable and yields an
* `unknown` base, and a native/relative target is handled as usual. On POSIX
* every token is `plain`, so an absolute target is absolute and everything
* else is relative.
*/
interpretBashCdTarget(target: string): BashCdTarget {
const shape = this.flavor.bashTokenShape(target);
switch (shape.kind) {
case "drive-mount":
return { kind: "absolute", value: shape.windowsPath };
case "device":
case "posix-absolute":
return { kind: "unknown" };
case "plain":
return this.flavor.impl.isAbsolute(target)
? { kind: "absolute", value: target }
: { kind: "relative" };
}
}
/** Resolve a `cd`-folded offset against the baked cwd (platform-aware). */
resolveBase(offset: string): string {
return this.flavor.impl.resolve(this.cwd, offset);
}
/** Join a `cd` offset with a relative target (platform-aware), for cd-folding. */
joinBase(offset: string, target: string): string {
return this.flavor.impl.join(offset, target);
}
/** Containment of `pathValue` within `directory` (platform-aware). */
isWithinDirectory(pathValue: string, directory: string): boolean {
return this.flavor.isWithin(pathValue, directory);
}
/** Canonical (symlink-resolved) outside-cwd test against the baked cwd. */
isOutsideWorkingDirectory(pathValue: string): boolean {
const canonicalPath = canonicalNormalizePathForComparison(
pathValue,
this.cwd,
this.flavor,
);
return isPathOutsideWorkingDirectory(
canonicalPath,
this.canonicalCwd,
this.flavor,
);
}
/**
* Outside-cwd test for an already-canonical boundary value (from
* {@link AccessPath.boundaryValue}), against the baked cwd.
*
* Unlike {@link isOutsideWorkingDirectory}, it does not re-derive the
* canonical form — the caller passes a value the {@link AccessPath} already
* canonicalized, so a device's preserved `/dev/null` reaches the pure check's
* `isSafeSystemPath` exclusion intact.
*/
isBoundaryOutsideWorkingDirectory(canonicalPath: string): boolean {
return isPathOutsideWorkingDirectory(
canonicalPath,
this.canonicalCwd,
this.flavor,
);
}
/**
* Lexical (not symlink-resolved) comparison value, resolved against the baked
* cwd. Mirrors the as-typed absolute form used for skill-prompt matching;
* touches no filesystem, unlike {@link forPath}'s canonical alias.
*/
comparableValue(pathValue: string): string {
return normalizePathForComparison(pathValue, this.cwd, this.flavor);
}
/**
* Pi infrastructure-read containment for a read-only tool, decided against
* the canonical (symlink-resolved) path and the baked cwd/flavor. Takes the
* already-built {@link AccessPath} so the caller does not re-resolve it.
*/
isInfrastructureRead(
toolName: string,
accessPath: AccessPath,
infraDirs: readonly string[],
): boolean {
return isPiInfrastructureRead(
toolName,
accessPath.boundaryValue(),
infraDirs,
this.cwd,
this.flavor,
);
}
/**
* True when `absolutePath` names an existing filesystem entry.
*
* The existence probe that resolves an *unknown* bash token: a bare word is a
* path candidate iff it names something real (ADR 0009, #645). Uses `lstat`,
* not `stat`, so a symlink counts as an entry even when its target is
* dangling — the link is the operand the command names, and dropping it would
* reopen the bypass this probe closes.
*
* Any error (ENOENT, ENOTDIR, EACCES, ELOOP) answers `false`: an entry the
* gate cannot confirm is not promoted, leaving the token exactly as
* unrestricted as it is today.
*
* Lives here beside {@link forPath}'s canonicalization so the package keeps a
* single filesystem edge for path interpretation.
*/
entryExists(absolutePath: string): boolean {
if (!absolutePath) return false;
try {
lstatSync(absolutePath);
return true;
} catch {
return false;
}
}
}