| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116 |
- /**
- * Internal sandbox-result classification helpers.
- *
- * @module @deepseek-ai/dsh-bash-sandbox/helpers
- */
- import { accessSync, constants, statSync } from 'node:fs'
- import type { ShellRunResult } from '@deepseek-ai/dsh-shell'
- import type { RunnerFailureRule } from '@deepseek-ai/dsh-sandbox'
- /** Node-local spawn codes proven to identify executable resolution or permission failure. */
- const EXECUTABLE_SPAWN_CODES = new Set(['EACCES', 'ENOENT'])
- /** Whether the caller-owned spawn cwd can be entered. */
- function isUsableWorkdir(path: string): boolean {
- try {
- if (!statSync(path).isDirectory()) return false
- accessSync(path, constants.X_OK)
- return true
- } catch {
- return false
- }
- }
- /**
- * Attribute only Node ENOENT/EACCES failures whose error path equals argv[0]
- * after independently ruling out the caller-owned cwd. A supplied error path
- * must exactly identify the runner; without one, the syscall must. With a
- * usable cwd, these codes describe resolution or execute permission for that
- * argv[0] or its shebang interpreter.
- * The workdir is checked at classification time, not atomically with spawn;
- * concurrent path replacement may change attribution but cannot permit an
- * unconfined execution.
- * @param error - the original spawn rejection.
- * @param runnerProgram - provider argv[0], the executable that establishes confinement.
- * @param workdir - the caller-owned spawn cwd, checked independently for usability.
- * @returns whether the rejection has executable-specific runner evidence.
- */
- export function isRunnerSpawnFailure(
- error: unknown,
- runnerProgram: string | undefined,
- workdir: string,
- ): boolean {
- if (runnerProgram === undefined || !isUsableWorkdir(workdir)) return false
- if (typeof error !== 'object' || error === null) return false
- const { code, path, syscall } = error as { code?: unknown; path?: unknown; syscall?: unknown }
- if (typeof code !== 'string' || !EXECUTABLE_SPAWN_CODES.has(code)) return false
- if (typeof syscall !== 'string') return false
- const exactSyscall = `spawn ${runnerProgram}`
- if (path === undefined) return syscall === exactSyscall
- if (typeof path !== 'string' || path.length === 0 || path !== runnerProgram) return false
- return syscall === 'spawn' || syscall === exactSyscall
- }
- /** Fatal runner evidence retained for infrastructure-error detail. */
- interface RunnerFailureMatch {
- /** The original stderr line that matched a fatal signature. */
- detail: string
- }
- /**
- * Classify a failed run against the selected backend's denial dialect.
- * @param result - settled foreground run.
- * @param signatures - case-insensitive denial substrings from the active wrap.
- * @returns whether the failed run matches that denial dialect.
- */
- export function classifyDenial(result: ShellRunResult, signatures: readonly string[]): boolean {
- return matchesSignature(result.exitCode, result.stderr.text, signatures)
- }
- /**
- * Classify one settled process against the selected backend's structured
- * runner-failure rules. Each rule requires a nonzero exit, its optional
- * exit-code gate, and a fatal signature on one stderr line after exact
- * informational lines are excluded.
- * @param exitCode - process exit code; null means signal termination.
- * @param stderr - collected stderr text, left unchanged.
- * @param rules - structured runner-failure rules from the active wrap.
- * @returns the first matching fatal line, or undefined when evidence is insufficient.
- */
- export function classifyRunnerFailure(
- exitCode: number | null,
- stderr: string,
- rules: readonly RunnerFailureRule[],
- ): RunnerFailureMatch | undefined {
- if (exitCode === null || exitCode === 0) return undefined
- const lines = stderr.split(/\r?\n/)
- for (const rule of rules) {
- if (rule.allowedExitCodes !== undefined && !rule.allowedExitCodes.includes(exitCode)) continue
- const informationalLines = new Set((rule.informationalLines ?? []).map(line => line.toLowerCase()))
- // An empty or whitespace-only substring is not meaningful runner evidence.
- // Ignore it while keeping any valid signatures beside it active.
- const fatalSignatures = rule.fatalSignatures
- .filter(signature => signature.trim().length > 0)
- .map(signature => signature.toLowerCase())
- for (const line of lines) {
- const lowered = line.toLowerCase()
- if (informationalLines.has(lowered)) continue
- if (fatalSignatures.some(signature => lowered.includes(signature))) return { detail: line }
- }
- }
- return undefined
- }
- /**
- * Match a non-zero exit against case-insensitive stderr signatures.
- * @param exitCode - process exit code; null means signal termination.
- * @param stderr - collected stderr text.
- * @param signatures - substrings identifying the selected backend's dialect.
- * @returns whether this is a non-zero exit whose stderr matches a signature.
- */
- export function matchesSignature(exitCode: number | null, stderr: string, signatures: readonly string[]): boolean {
- if (exitCode === null || exitCode === 0) return false
- const lowered = stderr.toLowerCase()
- return signatures.some(signature => lowered.includes(signature.toLowerCase()))
- }
|