@hydraharness/harness-tool-pwsh 0.1.1-rc.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +127 -0
- package/lib/index.js +458 -0
- package/lib/invariant.js +23 -0
- package/lib/types/background.d.ts +20 -0
- package/lib/types/index.d.ts +38 -0
- package/lib/types/invariant.d.ts +16 -0
- package/lib/types/render.d.ts +45 -0
- package/package.json +75 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 DeepSeek
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# @hydraharness/harness-tool-pwsh
|
|
2
|
+
|
|
3
|
+
The model-facing `pwsh` tool registered over the `ctx.shell` executor seam. Intended for Windows compositions where a PowerShell executor (e.g. `@hydraharness/harness-pwsh-local`) backs `ctx.shell`; the tool contract is PowerShell-dialect: native `C:\...` paths and `$env:NAME` variables. Behavior mirrors `@hydraharness/harness-tool-bash` call-for-call — foreground and `run_in_background` execution through the generic job runtime, the managed `HYDRA_*` environment through the shared `shell-env` registry, the sandbox denial rendering with the same-turn `sandbox_permissions` escalation surface, and the bash marker/truncation rendering story (a clean exit produces no marker).
|
|
4
|
+
|
|
5
|
+
Requires a loaded executor implementation and the `shell-env` plugin; the tool stays pending until both exist (`inject: ['tools', 'bash', 'systemPrompt', 'bashEnv']`).
|
|
6
|
+
|
|
7
|
+
The package root exposes only the Cordis plugin contract (`name`, `inject`, `Config`, `apply`); result rendering (`src/render.ts`) and background-job adaptation (`src/background.ts`) mirror the bash tool's structure and stay reachable through the package's `./src/*` export.
|
|
8
|
+
|
|
9
|
+
The plugin also contributes the `tool:pwsh` prompt section (order 105): non-zero exits are reported as `[exit code: N]` markers, and Windows interruption settles as exit 1 without a signal marker.
|
|
10
|
+
|
|
11
|
+
## Tools
|
|
12
|
+
|
|
13
|
+
### `pwsh`
|
|
14
|
+
|
|
15
|
+
| Arg | Type | Notes |
|
|
16
|
+
|---|---|---|
|
|
17
|
+
| `command` | string (required) | Run via `pwsh -Command`. No state persists between calls — use `workdir`, not `cd`. |
|
|
18
|
+
| `description` | string (required) | One-line, active-voice summary of the command (5-10 words), for UI/log display only — no effect on execution. |
|
|
19
|
+
| `timeoutMs` | number | Timeout override in milliseconds. The executor applies its configured default and cap. |
|
|
20
|
+
| `workdir` | string | Working directory for this call. Defaults to the calling agent's session cwd (`session.header.cwd`) so each session runs in its own workspace; a relative `workdir` is resolved against that same identity. |
|
|
21
|
+
| `changed_paths` | string[] | Optional explicit paths changed by a successful foreground command; used only to refresh workspace instructions, never inferred from PowerShell text. |
|
|
22
|
+
| `run_in_background` | boolean | Return a job id immediately; no timeout applies. |
|
|
23
|
+
| `sandbox_permissions` | string enum | Advertised only when a sandboxing executor is mounted (`ctx.shell.sandboxMode` defined). The wider sandbox mode for a one-shot retry of a command the sandbox just denied — the narrowest wider mode that suffices, requiring `justification` and user approval through `ctx.approval` BEFORE execution. A non-widening or unapprovable request fails closed without running anything. |
|
|
24
|
+
| `justification` | string | Required with `sandbox_permissions`: one sentence for the user explaining why this exact command needs the wider access. |
|
|
25
|
+
|
|
26
|
+
`command`, `workdir`, and `timeoutMs` are resolved against the executor's config defaults via `ctx.shell.resolve()` before execution. The workdir default is applied in the tool layer from the calling agent's `session.header.cwd` BEFORE `resolve()` — the per-session cwd must come from `exec.agent`, since N sessions share one executor; only when no session cwd is available does the executor fall back to its own config / `process.cwd()`.
|
|
27
|
+
|
|
28
|
+
### Managed shell environment
|
|
29
|
+
|
|
30
|
+
Every foreground and background model pwsh call receives a freshly collected trusted `HYDRA_*` environment through the shared [`@hydraharness/harness-shell-env`](../shell-env/) registry: `HYDRA_HOME` (the absolute Harness home), `HYDRA_SHELL=1`, the agent's `HYDRA_SESSION_ID`, and `HYDRA_SESSION_JSONL` when the active persistence backend locates one. Plugins contributing `HYDRA_*` facts to `ctx.shellEnv` apply to pwsh calls exactly as they do to bash calls. The snapshot passes through the dedicated `ShellExecRequest.hydraEnv` channel; `process.env` is never modified. The description teaches the generic `$env:HYDRA_*` convention rather than naming persistence-specific variables.
|
|
31
|
+
|
|
32
|
+
Result text contains stdout, an optional `[stderr]` section, then applicable truncation, sandbox-denial (with the same-turn escalation hint when the composition advertises escalation), timeout, signal, and exit markers. A clean exit (0, no signal) produces no marker; an empty body renders as `(no output)`. Truncation links a safe complete spill file or reports it unavailable. Timeout is reported independently of final exit status; nonzero exit remains a model-interpreted result rather than `isError`. Windows reports forced termination as exit 1 without a signal, so `[killed by signal: …]` is POSIX-only there. Only infrastructure failures — spawn errors and aborts (`tool call aborted`) — produce `isError`.
|
|
33
|
+
|
|
34
|
+
The canonical success is `{ kind: 'foreground', ...ShellRunResult }` for a completed foreground process (with the executor's `sandbox` facts — `mode`/`denied`, optional `enforcement`/`runnerFailed` — projected when present) or `{ kind: 'background', jobId }` for a published task. The renderer preserves exactly `started background job <id>` for background acks; programmatic consumers use the typed fields without parsing the rendered text.
|
|
35
|
+
|
|
36
|
+
A foreground call may declare `changed_paths`. After a successful foreground result, `agent-instructions` resolves those exact strings against the effective workdir and uses them as context-refresh hints; it never infers paths from PowerShell text. Failed, aborted, and background calls do not refresh instructions, and these hints are not mutation records.
|
|
37
|
+
|
|
38
|
+
When `run_in_background` is true, this plugin preflights `ctx.jobs.start()` before spawning, registers the calling agent as owner, and adapts the returned `ShellProcess` handle into generic cancel/done/incremental-output hooks. The job runtime owns ids, cross-session isolation, completion notices, waiting, and disposal cleanup; this plugin only maps pwsh exit facts into job output and outcome detail. `enableRunInBackground: false` removes the parameter and rejects a forced background call at execution time.
|
|
39
|
+
|
|
40
|
+
## UI presentation
|
|
41
|
+
|
|
42
|
+
The tool owns its `presentCall`/`presentResult` render intent. A foreground call is a `terminal` card carrying command, description, and optional cwd; a `run_in_background` call is a `generic` card with the raw command, mirroring the bash tool's background presentation. A completed foreground result is a `terminal` card too: the exit marker becomes the card's exit-status pill (`exitCode`/`signal`), and the marker-free body is the card's output — exactly the bash tool's terminal-card story, via the shared exit-status parse from `@hydraharness/harness-shell`. Background acks and execution errors stay `generic` cards with the rendered output in a `console` fence. These presenters are pure and replay-safe.
|
|
43
|
+
|
|
44
|
+
## Model Experience
|
|
45
|
+
|
|
46
|
+
### System prompt
|
|
47
|
+
|
|
48
|
+
#### What the model sees
|
|
49
|
+
|
|
50
|
+
Every request in this plugin's registration scope contains the pwsh guidance below. Scoped tool restrictions can hide the schema without removing this independently registered section.
|
|
51
|
+
|
|
52
|
+
##### Pwsh guidance
|
|
53
|
+
|
|
54
|
+
```markdown
|
|
55
|
+
Non-zero exits are reported as `[exit code: N]` markers; investigate failures before moving on. On Windows a killed process settles as `[exit code: 1]` without a signal marker; treat a bare exit 1 after an interruption as a termination, not a command failure.
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
#### Token effect
|
|
59
|
+
|
|
60
|
+
Small fixed input cost per request while the plugin is active.
|
|
61
|
+
|
|
62
|
+
#### KV Cache effect
|
|
63
|
+
|
|
64
|
+
Prefix-stable while the registration scope and prompt text are unchanged. Plugin activation or disposal may invalidate reuse from this prompt section.
|
|
65
|
+
|
|
66
|
+
### Tool schemas
|
|
67
|
+
|
|
68
|
+
#### What the model sees
|
|
69
|
+
|
|
70
|
+
The model sees the generated [`pwsh` schema](../../../docs/tool-catalog.md#hydraharness-tool-pwsh). Agent-scoped tool restrictions can remove the definition for that agent.
|
|
71
|
+
|
|
72
|
+
#### Token effect
|
|
73
|
+
|
|
74
|
+
Fixed schema cost on every request where the tool is visible.
|
|
75
|
+
|
|
76
|
+
#### KV Cache effect
|
|
77
|
+
|
|
78
|
+
Prefix-stable while visibility and the tool definition are unchanged. A restriction or config change may invalidate reuse from the first changed token.
|
|
79
|
+
|
|
80
|
+
### Foreground result
|
|
81
|
+
|
|
82
|
+
#### What the model sees
|
|
83
|
+
|
|
84
|
+
The renderer emits the data-dependent stdout tail, then optional `[stderr]` and the stderr tail. Conditional lines are exactly `[output truncated; full output: <path>]`, `[sandbox: file access denied under <mode> mode]` plus the escalation hint `[sandbox: escalation available — …]` (only when the composition advertises escalation), `[timed out after <timeoutMs>ms]`, `[killed by signal: <signal>]`, and `[exit code: <exitCode>]` (nonzero exits only); an empty body renders as `(no output)`.
|
|
85
|
+
|
|
86
|
+
#### Token effect
|
|
87
|
+
|
|
88
|
+
Zero result tokens before a call. Output is bounded per stream, while each emitted line remains in history until compaction.
|
|
89
|
+
|
|
90
|
+
#### KV Cache effect
|
|
91
|
+
|
|
92
|
+
Append-only; newly visible content follows the reusable request prefix and does not invalidate existing KV-cache entries.
|
|
93
|
+
|
|
94
|
+
### Background result
|
|
95
|
+
|
|
96
|
+
#### What the model sees
|
|
97
|
+
|
|
98
|
+
A background start renders exactly `started background job <id>`; subsequent reads and status flow through the generic `job_output`/`job_kill` tools, including the lossy-read spill notice when in-memory truncation dropped unread bytes.
|
|
99
|
+
|
|
100
|
+
#### Token effect
|
|
101
|
+
|
|
102
|
+
The ack is a fixed short line; job output is bounded per read.
|
|
103
|
+
|
|
104
|
+
#### KV Cache effect
|
|
105
|
+
|
|
106
|
+
Append-only; newly visible content follows the reusable request prefix and does not invalidate existing KV-cache entries.
|
|
107
|
+
|
|
108
|
+
### Tool errors
|
|
109
|
+
|
|
110
|
+
#### What the model sees
|
|
111
|
+
|
|
112
|
+
Validation and infrastructure failures are normalized as `Error: <message>`. This package's stable messages are `invalid command: expected a non-empty string`, `invalid description: expected a non-empty string`, `invalid timeoutMs: expected a positive number, got <value>`, `invalid escalation: sandbox_permissions requires a justification`, `invalid escalation: justification is only valid together with sandbox_permissions`, `invalid justification: expected a non-empty sentence`, `sandbox_permissions is not available in this composition (no sandboxing executor to escalate)`, the shared escalation failures (not strictly wider / no approval service / no agent to route / no approval channel / user rejected / was cancelled), `run_in_background is disabled for this deployment (enableRunInBackground: false)`, `background jobs unavailable: load @hydraharness/harness-jobs and @hydraharness/harness-tool-jobs`, and `tool call aborted`.
|
|
113
|
+
|
|
114
|
+
#### Token effect
|
|
115
|
+
|
|
116
|
+
Only the failing call adds these retained tokens; an aborted call adds no command output.
|
|
117
|
+
|
|
118
|
+
#### KV Cache effect
|
|
119
|
+
|
|
120
|
+
Append-only; newly visible content follows the reusable request prefix and does not invalidate existing KV-cache entries.
|
|
121
|
+
|
|
122
|
+
## Known Limitations and Deferred Work
|
|
123
|
+
|
|
124
|
+
- **Language mode and named-pipe capture under the Windows sandbox** — under the [Windows ACL sandbox](../../sandbox/sandbox-windows-acl/README.md), read-only pwsh starts in ConstrainedLanguage because its temp write denial makes PowerShell's AppLocker probe fail closed: `Add-Type`, non-core .NET statics (`[System.IO.*]::`, `[math]::`), COM objects, and reflection fail with "only core types" errors, and the mode cannot be lifted from inside. Workspace-write's private temp lets the probe complete, so it stays in FullLanguage unless host policy says otherwise. Both confined modes deny named-pipe opens, so a piped-stdio spawn inside a confined command fails with EPERM. The tool description teaches both contracts to the model; the backend README owns the full limitations.
|
|
125
|
+
- **No persistent shell** — every call starts a fresh `pwsh -Command`; the persistent-shell counterpart is [`@hydraharness/harness-tool-pwsh-persistent`](../tool-pwsh-persistent/README.md), which keeps one owner-scoped pwsh alive across calls on Windows (ConPTY) and POSIX hosts with pwsh.
|
|
126
|
+
- **PowerShell-dialect contract** — the model must write PowerShell (native paths, `$env:` variables), not bash; there is no dialect translation.
|
|
127
|
+
- **Session-cwd identity is not canonicalized** — the workdir base is the session header cwd as-is, unlike the bash tool's sandbox-root-canonicalized identity. Under a confining executor the policy's workspace root IS canonicalized (by the shared policy service), so the workdir and the confinement root can diverge when the raw session cwd differs from its canonical form — a parity gap deferred to the shared shell-tool base extraction.
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
import { isAbsolute, resolve } from "node:path";
|
|
2
|
+
import z from "@hydraharness/schemastery";
|
|
3
|
+
import { TOOL_ABORTED, defineTool } from "@hydraharness/harness-tools";
|
|
4
|
+
import { HarnessError } from "@hydraharness/harness-llm";
|
|
5
|
+
import { ESCALATION_TARGETS, approveEscalation, escalationHintMarker, sandboxDenialMarker, validateEscalationArgs } from "@hydraharness/harness-sandbox";
|
|
6
|
+
import { parseExitStatus } from "@hydraharness/harness-shell";
|
|
7
|
+
//#region lib/types/background.js
|
|
8
|
+
/**
|
|
9
|
+
* Generic-task adaptation for background pwsh process handles — the shell-agnostic
|
|
10
|
+
* twin of `@hydraharness/harness-tool-bash`'s background adaptation.
|
|
11
|
+
*
|
|
12
|
+
* @module @hydraharness/harness-tool-pwsh/background
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Map a settled background process onto the generic task-outcome vocabulary:
|
|
16
|
+
* `killed` stays `killed` (detail: the signal when one is known), everything
|
|
17
|
+
* else is `completed` with the exit code as detail. A nonzero command exit is
|
|
18
|
+
* reported, not failed, exactly like the foreground rendering.
|
|
19
|
+
* @param proc - the settled process handle.
|
|
20
|
+
* @returns the outcome for the `ctx.jobs` registration.
|
|
21
|
+
*/
|
|
22
|
+
function processOutcome(proc) {
|
|
23
|
+
if (proc.status === "killed") return {
|
|
24
|
+
status: "killed",
|
|
25
|
+
detail: proc.signal !== null ? `signal: ${proc.signal}` : "killed before exit"
|
|
26
|
+
};
|
|
27
|
+
return {
|
|
28
|
+
status: "completed",
|
|
29
|
+
detail: `exit code: ${proc.exitCode ?? 0}`
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
//#endregion
|
|
33
|
+
//#region lib/types/render.js
|
|
34
|
+
/**
|
|
35
|
+
* Model-facing result rendering for the pwsh tool — the PowerShell twin of
|
|
36
|
+
* `@hydraharness/harness-tool-bash`'s renderer: stdout, a marked stderr section, sandbox
|
|
37
|
+
* denial/runner-failure markers (with the same-turn escalation hint), and
|
|
38
|
+
* truncation notices with spill paths, then exit-status markers. Non-zero
|
|
39
|
+
* exits are reported, not errored — the model decides how to react; only
|
|
40
|
+
* infrastructure failures (spawn errors, aborts) surface as isError
|
|
41
|
+
* results.
|
|
42
|
+
*
|
|
43
|
+
* @module @hydraharness/harness-tool-pwsh/render
|
|
44
|
+
*/
|
|
45
|
+
/** Append the truncation notice (with the full-output spill path) to a stream's text. */
|
|
46
|
+
function streamText(output) {
|
|
47
|
+
if (!output.truncated) return output.text;
|
|
48
|
+
return `${output.text}\n[output truncated; full output: ${output.spillPath ?? "(unavailable)"}]`;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Shape one finished run into the text the model sees: stdout, then a marked
|
|
52
|
+
* stderr section, then exit-status markers, matching the bash tool's story —
|
|
53
|
+
* a clean exit (0, no signal) produces no marker.
|
|
54
|
+
* @param result - the completed foreground run from the executor.
|
|
55
|
+
* @param escalationModes - the escalation targets this composition advertises;
|
|
56
|
+
* non-empty adds the same-turn escalation hint after a denial marker
|
|
57
|
+
* (default `[]`: no hint).
|
|
58
|
+
* @returns the model-facing text: output body (or `(no output)`), then any timeout/signal/exit markers, each on its own line.
|
|
59
|
+
*/
|
|
60
|
+
function renderPwshResult(result, escalationModes = []) {
|
|
61
|
+
const out = streamText(result.stdout);
|
|
62
|
+
const err = streamText(result.stderr);
|
|
63
|
+
let body = out;
|
|
64
|
+
if (err.length > 0) {
|
|
65
|
+
if (body.length > 0 && !body.endsWith("\n")) body += "\n";
|
|
66
|
+
body += `[stderr]\n${err}`;
|
|
67
|
+
}
|
|
68
|
+
if (body.length === 0) body = "(no output)";
|
|
69
|
+
const markers = [];
|
|
70
|
+
if (result.sandbox?.denied) {
|
|
71
|
+
markers.push(sandboxDenialMarker(result.sandbox.mode));
|
|
72
|
+
if (escalationModes.length > 0) markers.push(escalationHintMarker("command"));
|
|
73
|
+
}
|
|
74
|
+
if (result.timedOut) markers.push(`[timed out after ${result.timeoutMs}ms]`);
|
|
75
|
+
if (result.signal !== null) markers.push(`[killed by signal: ${result.signal}]`);
|
|
76
|
+
else if (result.exitCode !== 0) markers.push(`[exit code: ${result.exitCode}]`);
|
|
77
|
+
if (markers.length === 0) return body;
|
|
78
|
+
if (!body.endsWith("\n")) body += "\n";
|
|
79
|
+
return body + markers.join("\n");
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Shape one background-process read into the `job_output` delta the model
|
|
83
|
+
* sees: the incremental delta, plus the lossy-read notice (with full-stream
|
|
84
|
+
* spill paths) when in-memory truncation dropped unread bytes.
|
|
85
|
+
* @param read - one incremental read from the process handle.
|
|
86
|
+
* @param sandbox - settled sandbox facts, when this was a confined process.
|
|
87
|
+
* @param escalationModes - escalation targets advertised by this composition.
|
|
88
|
+
* @returns the delta text with any loss or sandbox notice appended.
|
|
89
|
+
*/
|
|
90
|
+
function renderPwshProcessRead(read, sandbox, escalationModes = []) {
|
|
91
|
+
const notices = [];
|
|
92
|
+
if (read.lossy) {
|
|
93
|
+
const paths = [read.stdoutSpillPath, read.stderrSpillPath].filter((path) => path !== void 0);
|
|
94
|
+
notices.push(`[some output was dropped from memory; full output: ${paths.length > 0 ? paths.join(", ") : "(unavailable)"}]`);
|
|
95
|
+
}
|
|
96
|
+
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]`);
|
|
97
|
+
else if (sandbox?.denied) {
|
|
98
|
+
notices.push(sandboxDenialMarker(sandbox.mode));
|
|
99
|
+
if (escalationModes.length > 0) notices.push(escalationHintMarker("command"));
|
|
100
|
+
}
|
|
101
|
+
if (notices.length === 0) return read.delta;
|
|
102
|
+
return `${read.delta}${read.delta.length > 0 && !read.delta.endsWith("\n") ? "\n" : ""}${notices.join("\n")}`;
|
|
103
|
+
}
|
|
104
|
+
//#endregion
|
|
105
|
+
//#region lib/types/index.js
|
|
106
|
+
/**
|
|
107
|
+
* Model-facing PowerShell Consumer of the `ctx.shell` capability seam. Intended for
|
|
108
|
+
* Windows compositions where a PowerShell executor (e.g.
|
|
109
|
+
* `@hydraharness/harness-pwsh-local`) backs `ctx.shell`; the tool contract is
|
|
110
|
+
* PowerShell-dialect: native `C:\...` paths and `$env:NAME` variables.
|
|
111
|
+
*
|
|
112
|
+
* Behavior mirrors `@hydraharness/harness-tool-bash` call-for-call: foreground and
|
|
113
|
+
* `run_in_background` execution (background handles register with the
|
|
114
|
+
* generic `ctx.jobs` runtime), the managed `HYDRA_*` environment through the
|
|
115
|
+
* shared `shell-env` registry, the per-call sandbox policy resolution (the
|
|
116
|
+
* calling session's mode and cwd travel to the confining executor), the
|
|
117
|
+
* sandbox-denial rendering with the same-turn escalation surface
|
|
118
|
+
* (`sandbox_permissions` + `justification` resolved through
|
|
119
|
+
* `ctx.approval`), and the bash marker/truncation rendering story. UI
|
|
120
|
+
* presentation mirrors the bash tool's too: a completed foreground call is
|
|
121
|
+
* a terminal card with the parsed exit-status pill, using the shared
|
|
122
|
+
* exit-status parse from `@hydraharness/harness-shell`.
|
|
123
|
+
*
|
|
124
|
+
* @module @hydraharness/harness-tool-pwsh
|
|
125
|
+
*/
|
|
126
|
+
const name = "tool-pwsh";
|
|
127
|
+
const inject = [
|
|
128
|
+
"tools",
|
|
129
|
+
"shell",
|
|
130
|
+
"systemPrompt",
|
|
131
|
+
"shellEnv"
|
|
132
|
+
];
|
|
133
|
+
/** Runtime configuration schema for the pwsh tool plugin. */
|
|
134
|
+
const Config = z.object({ enableRunInBackground: z.boolean().default(true) });
|
|
135
|
+
function validatePwshArgs(args) {
|
|
136
|
+
if (args.command.trim().length === 0) throw new Error("invalid command: expected a non-empty string");
|
|
137
|
+
if (args.description.trim().length === 0) throw new Error("invalid description: expected a non-empty string");
|
|
138
|
+
if (args.timeoutMs !== void 0 && (!Number.isFinite(args.timeoutMs) || args.timeoutMs <= 0)) throw new Error(`invalid timeoutMs: expected a positive number, got ${JSON.stringify(args.timeoutMs)}`);
|
|
139
|
+
validateEscalationArgs(args.sandbox_permissions, args.justification);
|
|
140
|
+
if (args.changed_paths?.some((path) => path.length === 0)) throw new Error("invalid changed_paths: expected non-empty file paths");
|
|
141
|
+
}
|
|
142
|
+
function pwshDescription(backgroundEnabled, escalationModes) {
|
|
143
|
+
const base = "Execute a PowerShell command (`pwsh -Command`) and return its stdout/stderr. Each call runs in a fresh pwsh process: no state (cwd, variables, functions) persists between calls — pass `workdir` instead of using `cd`. Paths use native Windows form (`C:\\...`); read environment variables with `$env:NAME`. Non-zero exits are reported as `[exit code: N]`. Current harness environment facts are exposed through managed `$env:HYDRA_*` variables; inspect them when needed. Commands may run under a file sandbox; a blocked file operation is reported as `[sandbox: file access denied under <mode> mode]` — a policy denial, not a bug in the command; do not retry another way. Long output is truncated to its tail; the full output is saved to a file whose path is reported when available. On Windows a force-killed command settles as `[exit code: 1]` without a signal marker — treat it as an interruption, not a command failure. " + (backgroundEnabled ? "Set `run_in_background: true` for long-running commands: the call returns a job id immediately; read its output with `job_output` and stop it with `job_kill`." : "Background execution is not available; long-running commands must finish within the timeout.");
|
|
144
|
+
if (escalationModes.length === 0) return base;
|
|
145
|
+
return base + " Under the Windows sandbox, read-only pwsh runs in PowerShell ConstrainedLanguage mode, while workspace-write stays in FullLanguage unless host policy says otherwise. In read-only, prefer cmdlets and core types (`[string]`, `[datetime]`, `[regex]`, `[guid]`); .NET static calls (`[System.IO.*]::`, `[math]::`), `Add-Type`, COM objects, and reflection fail with \"only core types\" errors. `-f` formatting, property access, and core cmdlets work. In both confined modes, programs cannot open named pipes, so a command that captures another program's output through piped stdio (Node.js `child_process.spawn`/`exec` with the default `stdio: 'pipe'`) fails with EPERM, while `stdio: 'inherit'` and `stdio: 'ignore'` spawns work and PowerShell's own pipelines are unaffected. That EPERM is the documented boundary: do not retry the command another way — escalate the exact command once or restructure it to avoid capturing output. Attempting a command the sandbox may deny is safe and expected: run it and read the marker rather than assuming the denial. When a command is denied and a wider mode would let it succeed, escalate immediately in the same turn — the one sanctioned exception to a denial: retry the exact same command once with `sandbox_permissions` (the narrowest wider mode that suffices) plus a one-sentence `justification`. Do not detour through chat to ask permission first — the approval prompt raised by that retry is how the user consents. If the session states approval prompts are disabled, there is no exception: a denial is final — do not set `sandbox_permissions`. Never escalate speculatively: ground the request in a real denial — normally the one this command just hit; escalating up front is fine only when this session already denied the same access. A rejected escalation is final for that command — stop and explain, never work around it — but it does not forbid attempting or escalating other commands later.";
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Resolve an explicit workdir first, making a relative one session-workspace-relative;
|
|
149
|
+
* otherwise use the session header cwd and leave executor defaulting as the fallback.
|
|
150
|
+
*/
|
|
151
|
+
function resolveWorkdir(modelWorkdir, exec) {
|
|
152
|
+
const headerCwd = exec.agent?.session.header.cwd;
|
|
153
|
+
if (modelWorkdir === void 0) return headerCwd;
|
|
154
|
+
if (headerCwd !== void 0 && !isAbsolute(modelWorkdir)) return resolve(headerCwd, modelWorkdir);
|
|
155
|
+
return modelWorkdir;
|
|
156
|
+
}
|
|
157
|
+
/** Detach the executor DTO from readonly Service Definition types into plain JSON data. */
|
|
158
|
+
function canonicalPwshResult(result) {
|
|
159
|
+
const output = (stream) => ({
|
|
160
|
+
text: stream.text,
|
|
161
|
+
truncated: stream.truncated,
|
|
162
|
+
...stream.spillPath !== void 0 ? { spillPath: stream.spillPath } : {}
|
|
163
|
+
});
|
|
164
|
+
return {
|
|
165
|
+
kind: "foreground",
|
|
166
|
+
exitCode: result.exitCode,
|
|
167
|
+
signal: result.signal,
|
|
168
|
+
timedOut: result.timedOut,
|
|
169
|
+
aborted: result.aborted,
|
|
170
|
+
timeoutMs: result.timeoutMs,
|
|
171
|
+
stdout: output(result.stdout),
|
|
172
|
+
stderr: output(result.stderr),
|
|
173
|
+
...result.sandbox !== void 0 ? { sandbox: {
|
|
174
|
+
mode: result.sandbox.mode,
|
|
175
|
+
denied: result.sandbox.denied,
|
|
176
|
+
...result.sandbox.enforcement !== void 0 ? { enforcement: result.sandbox.enforcement } : {},
|
|
177
|
+
...result.sandbox.runnerFailed !== void 0 ? { runnerFailed: result.sandbox.runnerFailed } : {}
|
|
178
|
+
} } : {}
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
/** Canonical background-handle properties shared by the pwsh output union. */
|
|
182
|
+
const BACKGROUND_OUTPUT_PROPERTIES = {
|
|
183
|
+
kind: {
|
|
184
|
+
type: "string",
|
|
185
|
+
required: true,
|
|
186
|
+
const: "background"
|
|
187
|
+
},
|
|
188
|
+
jobId: {
|
|
189
|
+
type: "string",
|
|
190
|
+
required: true
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
function apply(ctx, config = {}) {
|
|
194
|
+
const backgroundEnabled = config.enableRunInBackground ?? true;
|
|
195
|
+
const defaultMode = ctx.shell.sandboxMode;
|
|
196
|
+
const escalationModes = defaultMode === void 0 ? [] : ESCALATION_TARGETS;
|
|
197
|
+
const sandboxPolicy = defaultMode === void 0 ? void 0 : ctx.get("sandboxPolicy");
|
|
198
|
+
if (defaultMode !== void 0 && sandboxPolicy === void 0) throw new Error("tool-pwsh: the mounted bash executor confines but ctx.sandboxPolicy is missing");
|
|
199
|
+
/** Resolve the complete standing policy for this call when a confining executor is mounted. */
|
|
200
|
+
const resolveSandboxPolicy = (exec) => sandboxPolicy?.resolve(exec.agent === void 0 ? {} : { session: exec.agent.session });
|
|
201
|
+
/**
|
|
202
|
+
* Resolve a sandbox-escalation request through `ctx.approval` BEFORE
|
|
203
|
+
* anything executes, delegating the shared fail-closed sequence (strict
|
|
204
|
+
* widening, channel resolution, outcome mapping) to
|
|
205
|
+
* {@link approveEscalation}. This tool contributes only the composition
|
|
206
|
+
* guard (the fields are unadvertised without a sandboxing executor, yet
|
|
207
|
+
* schema validation checks advertised keys only, so an unadvertised
|
|
208
|
+
* `sandbox_permissions` still reaches execute) and the approval
|
|
209
|
+
* ingredients. The shared policy resolver is required whenever the
|
|
210
|
+
* executor advertises confinement, so a split composition fails at
|
|
211
|
+
* tool-plugin load.
|
|
212
|
+
*/
|
|
213
|
+
const approvePwshEscalation = (mode, justification, exec, standingPolicy) => {
|
|
214
|
+
if (escalationModes.length === 0) throw new Error("sandbox_permissions is not available in this composition (no sandboxing executor to escalate)");
|
|
215
|
+
const effectiveMode = standingPolicy.mode;
|
|
216
|
+
return approveEscalation({
|
|
217
|
+
requestedMode: mode,
|
|
218
|
+
justification,
|
|
219
|
+
effectiveMode,
|
|
220
|
+
subject: "command"
|
|
221
|
+
}, {
|
|
222
|
+
approver: ctx.get("approval"),
|
|
223
|
+
agent: exec.agent,
|
|
224
|
+
callId: exec.callId,
|
|
225
|
+
toolName: "pwsh",
|
|
226
|
+
signal: exec.signal
|
|
227
|
+
});
|
|
228
|
+
};
|
|
229
|
+
ctx.systemPrompt.section({
|
|
230
|
+
name: "tool:pwsh",
|
|
231
|
+
order: 105,
|
|
232
|
+
text: "Non-zero exits are reported as `[exit code: N]` markers; investigate failures before moving on. On Windows a killed process settles as `[exit code: 1]` without a signal marker; treat a bare exit 1 after an interruption as a termination, not a command failure."
|
|
233
|
+
});
|
|
234
|
+
ctx.tools.register(defineTool({
|
|
235
|
+
name: "pwsh",
|
|
236
|
+
description: pwshDescription(backgroundEnabled, escalationModes),
|
|
237
|
+
parameters: {
|
|
238
|
+
command: {
|
|
239
|
+
type: "string",
|
|
240
|
+
required: true,
|
|
241
|
+
description: "The PowerShell command to execute."
|
|
242
|
+
},
|
|
243
|
+
description: {
|
|
244
|
+
type: "string",
|
|
245
|
+
required: true,
|
|
246
|
+
description: "Clear, concise description of what this command does in active voice, 5-10 words (shown in the UI). Examples: \"ls\" → \"List files in current directory\"; \"git status\" → \"Show working tree status\"; \"Get-Process\" → \"List running processes\"."
|
|
247
|
+
},
|
|
248
|
+
timeoutMs: {
|
|
249
|
+
type: "number",
|
|
250
|
+
description: "Timeout in milliseconds. The executor applies its configured default and cap, and kills the command on expiry."
|
|
251
|
+
},
|
|
252
|
+
workdir: {
|
|
253
|
+
type: "string",
|
|
254
|
+
description: "Working directory for this command. Defaults to the session workspace; a relative path is resolved against it."
|
|
255
|
+
},
|
|
256
|
+
changed_paths: {
|
|
257
|
+
type: "array",
|
|
258
|
+
items: { type: "string" },
|
|
259
|
+
description: "Foreground files this command changed, declared explicitly for workspace instruction refresh."
|
|
260
|
+
},
|
|
261
|
+
...backgroundEnabled ? { run_in_background: {
|
|
262
|
+
type: "boolean",
|
|
263
|
+
description: "Run in the background and return a job id immediately (collect with job_output, stop with job_kill). No timeout applies."
|
|
264
|
+
} } : {},
|
|
265
|
+
...escalationModes.length > 0 ? {
|
|
266
|
+
sandbox_permissions: {
|
|
267
|
+
type: "string",
|
|
268
|
+
enum: [...escalationModes],
|
|
269
|
+
description: "The wider sandbox mode this command needs. Only valid as a one-shot retry of a command the sandbox just denied; requires justification and user approval."
|
|
270
|
+
},
|
|
271
|
+
justification: {
|
|
272
|
+
type: "string",
|
|
273
|
+
description: "Required with sandbox_permissions: one sentence for the user explaining why this exact command needs the wider access."
|
|
274
|
+
}
|
|
275
|
+
} : {}
|
|
276
|
+
},
|
|
277
|
+
output: {
|
|
278
|
+
schema: { oneOf: [{
|
|
279
|
+
type: "object",
|
|
280
|
+
additionalProperties: false,
|
|
281
|
+
properties: BACKGROUND_OUTPUT_PROPERTIES
|
|
282
|
+
}, {
|
|
283
|
+
type: "object",
|
|
284
|
+
additionalProperties: false,
|
|
285
|
+
properties: {
|
|
286
|
+
kind: {
|
|
287
|
+
type: "string",
|
|
288
|
+
required: true,
|
|
289
|
+
const: "foreground"
|
|
290
|
+
},
|
|
291
|
+
exitCode: {
|
|
292
|
+
required: true,
|
|
293
|
+
oneOf: [{ type: "integer" }, { type: "null" }]
|
|
294
|
+
},
|
|
295
|
+
signal: {
|
|
296
|
+
required: true,
|
|
297
|
+
oneOf: [{ type: "string" }, { type: "null" }]
|
|
298
|
+
},
|
|
299
|
+
timedOut: {
|
|
300
|
+
type: "boolean",
|
|
301
|
+
required: true
|
|
302
|
+
},
|
|
303
|
+
aborted: {
|
|
304
|
+
type: "boolean",
|
|
305
|
+
required: true
|
|
306
|
+
},
|
|
307
|
+
timeoutMs: {
|
|
308
|
+
type: "number",
|
|
309
|
+
required: true
|
|
310
|
+
},
|
|
311
|
+
stdout: {
|
|
312
|
+
type: "object",
|
|
313
|
+
additionalProperties: false,
|
|
314
|
+
required: true,
|
|
315
|
+
properties: {
|
|
316
|
+
text: {
|
|
317
|
+
type: "string",
|
|
318
|
+
required: true
|
|
319
|
+
},
|
|
320
|
+
truncated: {
|
|
321
|
+
type: "boolean",
|
|
322
|
+
required: true
|
|
323
|
+
},
|
|
324
|
+
spillPath: { type: "string" }
|
|
325
|
+
}
|
|
326
|
+
},
|
|
327
|
+
stderr: {
|
|
328
|
+
type: "object",
|
|
329
|
+
additionalProperties: false,
|
|
330
|
+
required: true,
|
|
331
|
+
properties: {
|
|
332
|
+
text: {
|
|
333
|
+
type: "string",
|
|
334
|
+
required: true
|
|
335
|
+
},
|
|
336
|
+
truncated: {
|
|
337
|
+
type: "boolean",
|
|
338
|
+
required: true
|
|
339
|
+
},
|
|
340
|
+
spillPath: { type: "string" }
|
|
341
|
+
}
|
|
342
|
+
},
|
|
343
|
+
sandbox: {
|
|
344
|
+
type: "object",
|
|
345
|
+
additionalProperties: false,
|
|
346
|
+
properties: {
|
|
347
|
+
mode: {
|
|
348
|
+
type: "string",
|
|
349
|
+
required: true
|
|
350
|
+
},
|
|
351
|
+
denied: {
|
|
352
|
+
type: "boolean",
|
|
353
|
+
required: true
|
|
354
|
+
},
|
|
355
|
+
enforcement: { type: "string" },
|
|
356
|
+
runnerFailed: { type: "boolean" }
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}] },
|
|
361
|
+
render: (_args, value) => [{
|
|
362
|
+
type: "text",
|
|
363
|
+
text: value.kind === "background" ? `started background job ${value.jobId}` : renderPwshResult(value, escalationModes)
|
|
364
|
+
}]
|
|
365
|
+
},
|
|
366
|
+
async execute(args, exec) {
|
|
367
|
+
validatePwshArgs(args);
|
|
368
|
+
const standingPolicy = resolveSandboxPolicy(exec);
|
|
369
|
+
const approvedMode = args.sandbox_permissions !== void 0 && args.justification !== void 0 ? await approvePwshEscalation(args.sandbox_permissions, args.justification, exec, standingPolicy) : void 0;
|
|
370
|
+
const policy = approvedMode === void 0 ? standingPolicy : {
|
|
371
|
+
...standingPolicy,
|
|
372
|
+
mode: approvedMode
|
|
373
|
+
};
|
|
374
|
+
const workdir = resolveWorkdir(args.workdir, exec);
|
|
375
|
+
const request = {
|
|
376
|
+
command: args.command,
|
|
377
|
+
...workdir !== void 0 ? { workdir } : {},
|
|
378
|
+
...args.timeoutMs !== void 0 ? { timeoutMs: args.timeoutMs } : {},
|
|
379
|
+
hydraEnv: ctx.shellEnv.collect(exec),
|
|
380
|
+
...policy !== void 0 ? { sandboxPolicy: policy } : {}
|
|
381
|
+
};
|
|
382
|
+
if (args.run_in_background === true) {
|
|
383
|
+
if (!backgroundEnabled) throw new Error("run_in_background is disabled for this deployment (enableRunInBackground: false)");
|
|
384
|
+
const jobs = ctx.get("jobs");
|
|
385
|
+
if (jobs === void 0) throw new Error("background jobs unavailable: load @hydraharness/harness-jobs and @hydraharness/harness-tool-jobs");
|
|
386
|
+
if (exec.signal.aborted) {
|
|
387
|
+
const error = new HarnessError("tool call aborted", TOOL_ABORTED);
|
|
388
|
+
error.name = "AbortError";
|
|
389
|
+
throw error;
|
|
390
|
+
}
|
|
391
|
+
return {
|
|
392
|
+
kind: "background",
|
|
393
|
+
jobId: jobs.start({
|
|
394
|
+
kind: "pwsh",
|
|
395
|
+
label: args.command,
|
|
396
|
+
...exec.agent ? { owner: exec.agent } : {},
|
|
397
|
+
run: () => {
|
|
398
|
+
const proc = ctx.shell.start(ctx.shell.resolve(request));
|
|
399
|
+
return {
|
|
400
|
+
cancel: () => void proc.kill(),
|
|
401
|
+
done: proc.done.then(() => processOutcome(proc)),
|
|
402
|
+
readOutput: () => renderPwshProcessRead(proc.readOutput(), proc.sandbox, escalationModes)
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
})
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
const result = await ctx.shell.run(ctx.shell.resolve({
|
|
409
|
+
...request,
|
|
410
|
+
signal: exec.signal
|
|
411
|
+
}));
|
|
412
|
+
if (result.aborted) {
|
|
413
|
+
const error = new HarnessError("tool call aborted", TOOL_ABORTED);
|
|
414
|
+
error.name = "AbortError";
|
|
415
|
+
throw error;
|
|
416
|
+
}
|
|
417
|
+
return canonicalPwshResult(result);
|
|
418
|
+
},
|
|
419
|
+
presentCall: (args) => {
|
|
420
|
+
if (args.run_in_background === true) return {
|
|
421
|
+
card: "generic",
|
|
422
|
+
title: args.command,
|
|
423
|
+
kind: "execute",
|
|
424
|
+
rawInput: args.command,
|
|
425
|
+
content: [{
|
|
426
|
+
type: "text",
|
|
427
|
+
text: args.description
|
|
428
|
+
}]
|
|
429
|
+
};
|
|
430
|
+
return {
|
|
431
|
+
card: "terminal",
|
|
432
|
+
title: args.command,
|
|
433
|
+
description: args.description,
|
|
434
|
+
...args.workdir !== void 0 ? { cwd: args.workdir } : {}
|
|
435
|
+
};
|
|
436
|
+
},
|
|
437
|
+
presentResult: (args, result) => {
|
|
438
|
+
const block = result.content.length === 1 ? result.content[0] : void 0;
|
|
439
|
+
if (block === void 0 || block.type !== "text") return void 0;
|
|
440
|
+
const raw = block.text;
|
|
441
|
+
if (typeof args === "object" && args !== null && args.run_in_background === true || result.isError) return {
|
|
442
|
+
card: "generic",
|
|
443
|
+
content: [{
|
|
444
|
+
type: "text",
|
|
445
|
+
text: `\`\`\`console\n${raw.replace(/\n+$/, "")}\n\`\`\``
|
|
446
|
+
}]
|
|
447
|
+
};
|
|
448
|
+
const { body, ...exit } = parseExitStatus(raw);
|
|
449
|
+
return {
|
|
450
|
+
card: "terminal",
|
|
451
|
+
output: body,
|
|
452
|
+
...exit
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
}));
|
|
456
|
+
}
|
|
457
|
+
//#endregion
|
|
458
|
+
export { Config, apply, inject, name };
|
package/lib/invariant.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//#region lib/types/invariant.js
|
|
2
|
+
/**
|
|
3
|
+
* Package-owned invariant companion for `@hydraharness/harness-tool-pwsh`.
|
|
4
|
+
* @module @hydraharness/harness-tool-pwsh/invariant
|
|
5
|
+
*/
|
|
6
|
+
const PACKAGE_NAME = "@hydraharness/harness-tool-pwsh";
|
|
7
|
+
/** Cordis companion plugin name. */
|
|
8
|
+
const name = "tool-pwsh-invariant";
|
|
9
|
+
/** Service required before the companion can reserve package ownership. */
|
|
10
|
+
const inject = ["invariants"];
|
|
11
|
+
/**
|
|
12
|
+
* No runtime invariant: this package exposes no independent event sequence or mutable data relation
|
|
13
|
+
* beyond contracts enforced at its owning seam.
|
|
14
|
+
*/
|
|
15
|
+
const install = () => {};
|
|
16
|
+
/**
|
|
17
|
+
* Register this package's invariant companion.
|
|
18
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
19
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
20
|
+
*/
|
|
21
|
+
const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
|
|
22
|
+
//#endregion
|
|
23
|
+
export { apply, inject, name };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic-task adaptation for background pwsh process handles — the shell-agnostic
|
|
3
|
+
* twin of `@hydraharness/harness-tool-bash`'s background adaptation.
|
|
4
|
+
*
|
|
5
|
+
* @module @hydraharness/harness-tool-pwsh/background
|
|
6
|
+
*/
|
|
7
|
+
import type { ShellProcess } from '@hydraharness/harness-shell';
|
|
8
|
+
/**
|
|
9
|
+
* Map a settled background process onto the generic task-outcome vocabulary:
|
|
10
|
+
* `killed` stays `killed` (detail: the signal when one is known), everything
|
|
11
|
+
* else is `completed` with the exit code as detail. A nonzero command exit is
|
|
12
|
+
* reported, not failed, exactly like the foreground rendering.
|
|
13
|
+
* @param proc - the settled process handle.
|
|
14
|
+
* @returns the outcome for the `ctx.jobs` registration.
|
|
15
|
+
*/
|
|
16
|
+
export declare function processOutcome(proc: ShellProcess): {
|
|
17
|
+
status: 'completed' | 'killed';
|
|
18
|
+
detail: string;
|
|
19
|
+
};
|
|
20
|
+
//# sourceMappingURL=background.d.ts.map
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model-facing PowerShell Consumer of the `ctx.shell` capability seam. Intended for
|
|
3
|
+
* Windows compositions where a PowerShell executor (e.g.
|
|
4
|
+
* `@hydraharness/harness-pwsh-local`) backs `ctx.shell`; the tool contract is
|
|
5
|
+
* PowerShell-dialect: native `C:\...` paths and `$env:NAME` variables.
|
|
6
|
+
*
|
|
7
|
+
* Behavior mirrors `@hydraharness/harness-tool-bash` call-for-call: foreground and
|
|
8
|
+
* `run_in_background` execution (background handles register with the
|
|
9
|
+
* generic `ctx.jobs` runtime), the managed `HYDRA_*` environment through the
|
|
10
|
+
* shared `shell-env` registry, the per-call sandbox policy resolution (the
|
|
11
|
+
* calling session's mode and cwd travel to the confining executor), the
|
|
12
|
+
* sandbox-denial rendering with the same-turn escalation surface
|
|
13
|
+
* (`sandbox_permissions` + `justification` resolved through
|
|
14
|
+
* `ctx.approval`), and the bash marker/truncation rendering story. UI
|
|
15
|
+
* presentation mirrors the bash tool's too: a completed foreground call is
|
|
16
|
+
* a terminal card with the parsed exit-status pill, using the shared
|
|
17
|
+
* exit-status parse from `@hydraharness/harness-shell`.
|
|
18
|
+
*
|
|
19
|
+
* @module @hydraharness/harness-tool-pwsh
|
|
20
|
+
*/
|
|
21
|
+
import type { Context } from '@hydraharness/cordis';
|
|
22
|
+
import z from '@hydraharness/schemastery';
|
|
23
|
+
declare module '@hydraharness/harness-jobs' {
|
|
24
|
+
interface JobKindMap {
|
|
25
|
+
pwsh: 'pwsh';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export declare const name = "tool-pwsh";
|
|
29
|
+
export declare const inject: string[];
|
|
30
|
+
/** Configuration for the pwsh tool. */
|
|
31
|
+
export interface Config {
|
|
32
|
+
/** Expose `run_in_background` (default true); disabled calls are also rejected. */
|
|
33
|
+
enableRunInBackground?: boolean;
|
|
34
|
+
}
|
|
35
|
+
/** Runtime configuration schema for the pwsh tool plugin. */
|
|
36
|
+
export declare const Config: z<Config>;
|
|
37
|
+
export declare function apply(ctx: Context, config?: Config): void;
|
|
38
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package-owned invariant companion for `@hydraharness/harness-tool-pwsh`.
|
|
3
|
+
* @module @hydraharness/harness-tool-pwsh/invariant
|
|
4
|
+
*/
|
|
5
|
+
import type { Context } from '@hydraharness/cordis';
|
|
6
|
+
/** Cordis companion plugin name. */
|
|
7
|
+
export declare const name = "tool-pwsh-invariant";
|
|
8
|
+
/** Service required before the companion can reserve package ownership. */
|
|
9
|
+
export declare const inject: string[];
|
|
10
|
+
/**
|
|
11
|
+
* Register this package's invariant companion.
|
|
12
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
13
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
14
|
+
*/
|
|
15
|
+
export declare const apply: (ctx: Context) => Promise<() => void>;
|
|
16
|
+
//# sourceMappingURL=invariant.d.ts.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model-facing result rendering for the pwsh tool — the PowerShell twin of
|
|
3
|
+
* `@hydraharness/harness-tool-bash`'s renderer: stdout, a marked stderr section, sandbox
|
|
4
|
+
* denial/runner-failure markers (with the same-turn escalation hint), and
|
|
5
|
+
* truncation notices with spill paths, then exit-status markers. Non-zero
|
|
6
|
+
* exits are reported, not errored — the model decides how to react; only
|
|
7
|
+
* infrastructure failures (spawn errors, aborts) surface as isError
|
|
8
|
+
* results.
|
|
9
|
+
*
|
|
10
|
+
* @module @hydraharness/harness-tool-pwsh/render
|
|
11
|
+
*/
|
|
12
|
+
import type { ShellProcessRead, ShellSandboxInfo, CollectedOutput } from '@hydraharness/harness-shell';
|
|
13
|
+
import type { SandboxMode } from '@hydraharness/harness-sandbox';
|
|
14
|
+
/** The renderable foreground result shape (the schema-derived value, no `kind`). */
|
|
15
|
+
export interface RenderablePwshResult {
|
|
16
|
+
exitCode: number | null;
|
|
17
|
+
signal: string | null;
|
|
18
|
+
timedOut: boolean;
|
|
19
|
+
timeoutMs: number;
|
|
20
|
+
stdout: CollectedOutput;
|
|
21
|
+
stderr: CollectedOutput;
|
|
22
|
+
sandbox?: ShellSandboxInfo;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Shape one finished run into the text the model sees: stdout, then a marked
|
|
26
|
+
* stderr section, then exit-status markers, matching the bash tool's story —
|
|
27
|
+
* a clean exit (0, no signal) produces no marker.
|
|
28
|
+
* @param result - the completed foreground run from the executor.
|
|
29
|
+
* @param escalationModes - the escalation targets this composition advertises;
|
|
30
|
+
* non-empty adds the same-turn escalation hint after a denial marker
|
|
31
|
+
* (default `[]`: no hint).
|
|
32
|
+
* @returns the model-facing text: output body (or `(no output)`), then any timeout/signal/exit markers, each on its own line.
|
|
33
|
+
*/
|
|
34
|
+
export declare function renderPwshResult(result: RenderablePwshResult, escalationModes?: readonly SandboxMode[]): string;
|
|
35
|
+
/**
|
|
36
|
+
* Shape one background-process read into the `job_output` delta the model
|
|
37
|
+
* sees: the incremental delta, plus the lossy-read notice (with full-stream
|
|
38
|
+
* spill paths) when in-memory truncation dropped unread bytes.
|
|
39
|
+
* @param read - one incremental read from the process handle.
|
|
40
|
+
* @param sandbox - settled sandbox facts, when this was a confined process.
|
|
41
|
+
* @param escalationModes - escalation targets advertised by this composition.
|
|
42
|
+
* @returns the delta text with any loss or sandbox notice appended.
|
|
43
|
+
*/
|
|
44
|
+
export declare function renderPwshProcessRead(read: ShellProcessRead, sandbox?: ShellSandboxInfo, escalationModes?: readonly SandboxMode[]): string;
|
|
45
|
+
//# sourceMappingURL=render.d.ts.map
|
package/package.json
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hydraharness/harness-tool-pwsh",
|
|
3
|
+
"description": "Model-facing pwsh tool over the bash executor seam",
|
|
4
|
+
"hydra": {
|
|
5
|
+
"plugin": {
|
|
6
|
+
"application": "Let the agent run PowerShell commands and collect their output."
|
|
7
|
+
}
|
|
8
|
+
},
|
|
9
|
+
"version": "0.1.1-rc.6",
|
|
10
|
+
"publishConfig": {
|
|
11
|
+
"access": "public"
|
|
12
|
+
},
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/MaiHongPhong1902/Hydra-Harness.git",
|
|
16
|
+
"directory": "packages/shell/tool-pwsh"
|
|
17
|
+
},
|
|
18
|
+
"type": "module",
|
|
19
|
+
"main": "lib/index.js",
|
|
20
|
+
"types": "lib/types/index.d.ts",
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"types": "./lib/types/index.d.ts",
|
|
24
|
+
"default": "./lib/index.js"
|
|
25
|
+
},
|
|
26
|
+
"./invariant": {
|
|
27
|
+
"types": "./lib/types/invariant.d.ts",
|
|
28
|
+
"default": "./lib/invariant.js"
|
|
29
|
+
},
|
|
30
|
+
"./src/*": "./src/*",
|
|
31
|
+
"./package.json": "./package.json"
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"lib/index.js",
|
|
35
|
+
"lib/invariant.js",
|
|
36
|
+
"lib/types/**/*.d.ts"
|
|
37
|
+
],
|
|
38
|
+
"license": "MIT",
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"@hydraharness/harness-shell": "^0.1.1-rc.6",
|
|
41
|
+
"@hydraharness/harness-shell-env": "^0.1.1-rc.6",
|
|
42
|
+
"@hydraharness/harness-llm": "^0.1.1-rc.6",
|
|
43
|
+
"@hydraharness/harness-sandbox": "^0.1.1-rc.6",
|
|
44
|
+
"@hydraharness/harness-agent": "^0.1.1-rc.6",
|
|
45
|
+
"@hydraharness/harness-invariants": "^0.1.1-rc.6",
|
|
46
|
+
"@hydraharness/harness-sandbox-policy": "^0.1.1-rc.6",
|
|
47
|
+
"@hydraharness/harness-system-prompt": "^0.1.1-rc.6",
|
|
48
|
+
"@hydraharness/harness-user-approval": "^0.1.1-rc.6",
|
|
49
|
+
"@hydraharness/cordis": "^4.0.2",
|
|
50
|
+
"@hydraharness/harness-jobs": "^0.1.1-rc.6",
|
|
51
|
+
"@hydraharness/harness-tools": "^0.1.1-rc.6"
|
|
52
|
+
},
|
|
53
|
+
"dependencies": {
|
|
54
|
+
"@hydraharness/schemastery": "^3.18.2"
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"@hydraharness/harness-shell": "^0.1.1-rc.6",
|
|
58
|
+
"@hydraharness/harness-agent": "^0.1.1-rc.6",
|
|
59
|
+
"@hydraharness/harness-llm": "^0.1.1-rc.6",
|
|
60
|
+
"@hydraharness/harness-invariants": "^0.1.1-rc.6",
|
|
61
|
+
"@hydraharness/harness-shell-env": "^0.1.1-rc.6",
|
|
62
|
+
"@hydraharness/harness-loader-smoke": "^0.1.1-rc.6",
|
|
63
|
+
"@hydraharness/harness-pwsh-local": "^0.1.1-rc.6",
|
|
64
|
+
"@hydraharness/harness-sandbox-policy": "^0.1.1-rc.6",
|
|
65
|
+
"@hydraharness/harness-subprocess-local": "^0.1.1-rc.6",
|
|
66
|
+
"@hydraharness/harness-system-prompt": "^0.1.1-rc.6",
|
|
67
|
+
"@hydraharness/harness-jobs": "^0.1.1-rc.6",
|
|
68
|
+
"@hydraharness/harness-jobs-local": "^0.1.1-rc.6",
|
|
69
|
+
"@hydraharness/harness-tool-jobs": "^0.1.1-rc.6",
|
|
70
|
+
"@hydraharness/harness-tools": "^0.1.1-rc.6",
|
|
71
|
+
"@hydraharness/cordis": "^4.0.2",
|
|
72
|
+
"@hydraharness/harness-user-approval": "^0.1.1-rc.6",
|
|
73
|
+
"@hydraharness/harness-sandbox": "^0.1.1-rc.6"
|
|
74
|
+
}
|
|
75
|
+
}
|