tool-pwsh now renders sandbox denial facts (denial marker + same-turn escalation hint, runner-failed notice on background reads) and advertises the sandbox_permissions/justification escalation pair resolved through ctx.approval before execution — the shared fail-closed sequence from dsh-sandbox, with the tool name 'pwsh'. The description teaches the ConstrainedLanguage contract under the Windows sandbox (both confined modes run pwsh in CLM: Add-Type, non-core .NET statics, COM, and reflection fail; the mode cannot be lifted — probe-verified, pinned by LANGMODE assertions in the runner suite and description assertions in the tool suite). The stale pwsh-sandbox JSDoc ('the tool owns approval') and the parity notes' superseded 'minus sandbox' claims are corrected.
114 lines
4.8 KiB
TypeScript
114 lines
4.8 KiB
TypeScript
/**
|
|
* Model-facing result rendering for the pwsh tool — the PowerShell twin of
|
|
* `dsh-tool-bash`'s renderer: stdout, a marked stderr section, sandbox
|
|
* denial/runner-failure markers (with the same-turn escalation hint), and
|
|
* truncation notices with spill paths, then exit-status markers. Non-zero
|
|
* exits are reported, not errored — the model decides how to react; only
|
|
* infrastructure failures (spawn errors, aborts) surface as isError
|
|
* results.
|
|
*
|
|
* @module @deepseek-ai/dsh-tool-pwsh/render
|
|
*/
|
|
|
|
import type { BashProcessRead, BashSandboxInfo, CollectedOutput } from '@deepseek-ai/dsh-bash'
|
|
import type { SandboxMode } from '@deepseek-ai/dsh-sandbox'
|
|
import { escalationHintMarker, sandboxDenialMarker } from '@deepseek-ai/dsh-sandbox'
|
|
|
|
/* jscpd:ignore-start -- deliberate twin of dsh-tool-bash/render.ts (Agent Note). */
|
|
|
|
/** Append the truncation notice (with the full-output spill path) to a stream's text. */
|
|
function streamText(output: CollectedOutput): string {
|
|
if (!output.truncated) return output.text
|
|
return `${output.text}\n[output truncated; full output: ${output.spillPath ?? '(unavailable)'}]`
|
|
}
|
|
|
|
/** The renderable foreground result shape (the schema-derived value, no `kind`). */
|
|
export interface RenderablePwshResult {
|
|
exitCode: number | null
|
|
signal: string | null
|
|
timedOut: boolean
|
|
timeoutMs: number
|
|
stdout: CollectedOutput
|
|
stderr: CollectedOutput
|
|
sandbox?: BashSandboxInfo
|
|
}
|
|
|
|
/**
|
|
* Shape one finished run into the text the model sees: stdout, then a marked
|
|
* stderr section, then exit-status markers, matching the bash tool's story —
|
|
* a clean exit (0, no signal) produces no marker.
|
|
* @param result - the completed foreground run from the executor.
|
|
* @param escalationModes - the escalation targets this composition advertises;
|
|
* non-empty adds the same-turn escalation hint after a denial marker
|
|
* (default `[]`: no hint).
|
|
* @returns the model-facing text: output body (or `(no output)`), then any timeout/signal/exit markers, each on its own line.
|
|
*/
|
|
export function renderPwshResult(
|
|
result: RenderablePwshResult,
|
|
escalationModes: readonly SandboxMode[] = [],
|
|
): string {
|
|
const out = streamText(result.stdout)
|
|
const err = streamText(result.stderr)
|
|
|
|
let body = out
|
|
if (err.length > 0) {
|
|
// Single newline between sections (stdout usually ends with one already).
|
|
if (body.length > 0 && !body.endsWith('\n')) body += '\n'
|
|
body += `[stderr]\n${err}`
|
|
}
|
|
if (body.length === 0) body = '(no output)'
|
|
|
|
const markers: string[] = []
|
|
// Keep the exit marker last because parseExitStatus anchors there.
|
|
if (result.sandbox?.denied) {
|
|
markers.push(sandboxDenialMarker(result.sandbox.mode))
|
|
// Hint only when the composition exposes escalation, before the final exit marker.
|
|
if (escalationModes.length > 0) {
|
|
markers.push(escalationHintMarker('command'))
|
|
}
|
|
}
|
|
// A command may trap the termination and exit 0 after timeout; still report interruption.
|
|
if (result.timedOut) markers.push(`[timed out after ${result.timeoutMs}ms]`)
|
|
if (result.signal !== null) {
|
|
markers.push(`[killed by signal: ${result.signal}]`)
|
|
} else if (result.exitCode !== 0) {
|
|
markers.push(`[exit code: ${result.exitCode}]`)
|
|
}
|
|
if (markers.length === 0) return body
|
|
|
|
if (!body.endsWith('\n')) body += '\n'
|
|
return body + markers.join('\n')
|
|
}
|
|
|
|
/**
|
|
* Shape one background-process read into the `task_output` delta the model
|
|
* sees: the incremental delta, plus the lossy-read notice (with full-stream
|
|
* spill paths) when in-memory truncation dropped unread bytes.
|
|
* @param read - one incremental read from the process handle.
|
|
* @param sandbox - settled sandbox facts, when this was a confined process.
|
|
* @param escalationModes - escalation targets advertised by this composition.
|
|
* @returns the delta text with any loss or sandbox notice appended.
|
|
*/
|
|
export function renderPwshProcessRead(
|
|
read: BashProcessRead,
|
|
sandbox?: BashSandboxInfo,
|
|
escalationModes: readonly SandboxMode[] = [],
|
|
): string {
|
|
const notices: string[] = []
|
|
if (read.lossy) {
|
|
const paths = [read.stdoutSpillPath, read.stderrSpillPath].filter((path): path is string => path !== undefined)
|
|
notices.push(`[some output was dropped from memory; full output: ${paths.length > 0 ? paths.join(', ') : '(unavailable)'}]`)
|
|
}
|
|
if (sandbox?.runnerFailed) {
|
|
notices.push(`[sandbox: the sandbox runner itself failed under ${sandbox.mode} mode — the command did not run; this is a sandbox problem, not a command failure]`)
|
|
} else if (sandbox?.denied) {
|
|
notices.push(sandboxDenialMarker(sandbox.mode))
|
|
if (escalationModes.length > 0) {
|
|
notices.push(escalationHintMarker('command'))
|
|
}
|
|
}
|
|
if (notices.length === 0) return read.delta
|
|
return `${read.delta}${read.delta.length > 0 && !read.delta.endsWith('\n') ? '\n' : ''}${notices.join('\n')}`
|
|
}
|
|
/* jscpd:ignore-end */
|