mirror of
https://github.com/wu736139669/hapi.git
synced 2026-10-26 22:30:50 +00:00
feat(session): validate agents and browse workspace directories
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
import { realpathSync } from 'node:fs'
|
||||
import { realpath } from 'node:fs/promises'
|
||||
import { homedir } from 'node:os'
|
||||
import {
|
||||
basename,
|
||||
dirname,
|
||||
isAbsolute,
|
||||
join,
|
||||
relative,
|
||||
resolve,
|
||||
} from 'node:path'
|
||||
|
||||
export function normalizeWindowsDriveRoot(path: string): string {
|
||||
return /^[A-Za-z]:$/.test(path) ? `${path}\\` : path
|
||||
}
|
||||
|
||||
function canonicalizeExistingPathSync(path: string): string {
|
||||
return normalizeWindowsDriveRoot(realpathSync.native(path))
|
||||
}
|
||||
|
||||
function normalizeRoots(paths: readonly string[]): string[] {
|
||||
return Array.from(new Set(paths.map((path) => {
|
||||
try {
|
||||
return canonicalizeExistingPathSync(path)
|
||||
} catch {
|
||||
return normalizeWindowsDriveRoot(resolve(path))
|
||||
}
|
||||
})))
|
||||
}
|
||||
|
||||
function isPathWithinRoots(path: string, roots: readonly string[]): boolean {
|
||||
return roots.some((root) => {
|
||||
const child = relative(root, path)
|
||||
return child === '' || (!child.startsWith('..') && !isAbsolute(child))
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Single authority for machine-scoped path access.
|
||||
*
|
||||
* Spawn paths are unrestricted when the runner has no configured workspace
|
||||
* roots, preserving the legacy manual-entry contract. Browse paths instead
|
||||
* fall back to the runner's home directory so native autocomplete/pickers can
|
||||
* remain useful without exposing the whole filesystem.
|
||||
*/
|
||||
export class MachinePathPolicy {
|
||||
readonly workspaceRoots: readonly string[]
|
||||
readonly browseRoots: readonly string[]
|
||||
|
||||
constructor(options: {
|
||||
workspaceRoots?: readonly string[]
|
||||
homeDirectory?: string
|
||||
} = {}) {
|
||||
this.workspaceRoots = normalizeRoots(options.workspaceRoots ?? [])
|
||||
this.browseRoots = this.workspaceRoots.length > 0
|
||||
? this.workspaceRoots
|
||||
: normalizeRoots([options.homeDirectory ?? homedir()])
|
||||
}
|
||||
|
||||
hasWorkspaceRoots(): boolean {
|
||||
return this.workspaceRoots.length > 0
|
||||
}
|
||||
|
||||
isWithinSpawnRoots(path: string): boolean {
|
||||
return !this.hasWorkspaceRoots() || isPathWithinRoots(path, this.workspaceRoots)
|
||||
}
|
||||
|
||||
isWithinBrowseRoots(path: string): boolean {
|
||||
return isPathWithinRoots(path, this.browseRoots)
|
||||
}
|
||||
|
||||
async resolveForCheck(path: string): Promise<string> {
|
||||
const absolute = resolve(path)
|
||||
try {
|
||||
return normalizeWindowsDriveRoot(await realpath(absolute))
|
||||
} catch {
|
||||
const missing: string[] = []
|
||||
let cursor = absolute
|
||||
while (cursor !== dirname(cursor)) {
|
||||
missing.unshift(basename(cursor))
|
||||
cursor = dirname(cursor)
|
||||
try {
|
||||
return join(normalizeWindowsDriveRoot(await realpath(cursor)), ...missing)
|
||||
} catch {
|
||||
// Continue to the nearest existing ancestor. This resolves
|
||||
// symlinks in the existing prefix before adding a missing tail.
|
||||
}
|
||||
}
|
||||
return normalizeWindowsDriveRoot(absolute)
|
||||
}
|
||||
}
|
||||
|
||||
async allowsSpawn(path: string): Promise<boolean> {
|
||||
return this.isWithinSpawnRoots(await this.resolveForCheck(path))
|
||||
}
|
||||
|
||||
async allowsBrowse(path: string): Promise<boolean> {
|
||||
return this.isWithinBrowseRoots(await this.resolveForCheck(path))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user