@qilin-agent/tool-pwsh 0.0.0-stage → 3.1.3
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.i18n.yaml +121 -0
- package/README.md +211 -2
- package/README.zh.md +212 -0
- package/lib/index.js +540 -0
- package/lib/types/background.d.ts +45 -0
- package/lib/types/index.d.ts +38 -0
- package/lib/types/render.d.ts +45 -0
- package/package.json +66 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 kkutysllb
|
|
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.i18n.yaml
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# Bilingual-pair consistency record for README.md (docs/i18n/README.md): per heading
|
|
2
|
+
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
|
|
3
|
+
# After editing either side, bring the other along and re-record with:
|
|
4
|
+
# pnpm run verify-translation-pairing --write packages/shell/tool-pwsh/README.md
|
|
5
|
+
/:
|
|
6
|
+
en: 83e748b077469aac
|
|
7
|
+
zh: f858d23ea8c1c6ba
|
|
8
|
+
/qilin-tool-pwsh:
|
|
9
|
+
en: 7c96246d95d566d4
|
|
10
|
+
zh: 3753177f123b2ab4
|
|
11
|
+
/qilin-tool-pwsh/summary:
|
|
12
|
+
en: c41ec1fbfa215c94
|
|
13
|
+
zh: 314e5d3265be3d6d
|
|
14
|
+
/qilin-tool-pwsh/table-of-contents:
|
|
15
|
+
en: d152484eb41ac6b4
|
|
16
|
+
zh: 09388d293f9be9cb
|
|
17
|
+
/qilin-tool-pwsh/use-this-package:
|
|
18
|
+
en: 1b4216447cd42de2
|
|
19
|
+
zh: 76280937fc3e7b26
|
|
20
|
+
/qilin-tool-pwsh/use-this-package/when-to-choose-it:
|
|
21
|
+
en: c9cfb8651fc9558f
|
|
22
|
+
zh: 21ad622a9d50bd4e
|
|
23
|
+
/qilin-tool-pwsh/use-this-package/minimal-configuration:
|
|
24
|
+
en: bd6629bd8397b329
|
|
25
|
+
zh: f1dc3bb067d951e4
|
|
26
|
+
/qilin-tool-pwsh/use-this-package/running-a-command:
|
|
27
|
+
en: 02b18286e7ae41a8
|
|
28
|
+
zh: 377624e32d370cce
|
|
29
|
+
/qilin-tool-pwsh/use-this-package/windows-specific-sandbox-behavior:
|
|
30
|
+
en: a2d3592afd0c5678
|
|
31
|
+
zh: dc9ef914149c3604
|
|
32
|
+
/qilin-tool-pwsh/use-this-package/what-can-go-wrong:
|
|
33
|
+
en: 0db3e3f9fc823853
|
|
34
|
+
zh: 7196d080d76babee
|
|
35
|
+
/qilin-tool-pwsh/understand-the-implementation:
|
|
36
|
+
en: 8d94a7381bca3fa9
|
|
37
|
+
zh: f4bd2866fff6fa8d
|
|
38
|
+
/qilin-tool-pwsh/understand-the-implementation/design-philosophy:
|
|
39
|
+
en: d03cc8af1c9c1315
|
|
40
|
+
zh: 202bea78d0bcdea6
|
|
41
|
+
/qilin-tool-pwsh/understand-the-implementation/source-map:
|
|
42
|
+
en: 02d3f9c1a49f9c31
|
|
43
|
+
zh: 5a59a787a37bd9cb
|
|
44
|
+
/qilin-tool-pwsh/understand-the-implementation/rendering-and-exit-markers:
|
|
45
|
+
en: 35818fa611875c4a
|
|
46
|
+
zh: 7a0fc35fa0a72b78
|
|
47
|
+
/qilin-tool-pwsh/further-exploration:
|
|
48
|
+
en: 24d79bef5d6e6648
|
|
49
|
+
zh: 518eb32cea322bc4
|
|
50
|
+
/qilin-tool-pwsh/model-experience:
|
|
51
|
+
en: 215e7ba838619b7b
|
|
52
|
+
zh: a311da3843709f90
|
|
53
|
+
/qilin-tool-pwsh/model-experience/system-prompt:
|
|
54
|
+
en: cf78b9dd5596495f
|
|
55
|
+
zh: bd44560f8c2435e8
|
|
56
|
+
/qilin-tool-pwsh/model-experience/system-prompt/what-the-model-sees:
|
|
57
|
+
en: ed8a75117d71cc5d
|
|
58
|
+
zh: 7b9119a5246d096e
|
|
59
|
+
/qilin-tool-pwsh/model-experience/system-prompt/what-the-model-sees/pwsh-guidance:
|
|
60
|
+
en: de907562b460fc1b
|
|
61
|
+
zh: 2a398555cc929dd3
|
|
62
|
+
/qilin-tool-pwsh/model-experience/system-prompt/token-effect:
|
|
63
|
+
en: 4fea242a5deec6d2
|
|
64
|
+
zh: 8dcbe863eb960a4d
|
|
65
|
+
/qilin-tool-pwsh/model-experience/system-prompt/kv-cache-effect:
|
|
66
|
+
en: 3959f5f66ff2f4be
|
|
67
|
+
zh: 66636c0d90becdb1
|
|
68
|
+
/qilin-tool-pwsh/model-experience/tool-schemas:
|
|
69
|
+
en: 3039bc5e7a796b21
|
|
70
|
+
zh: 6b7a9eaf69c9ea53
|
|
71
|
+
/qilin-tool-pwsh/model-experience/tool-schemas/what-the-model-sees:
|
|
72
|
+
en: 0f994a8c1382cc63
|
|
73
|
+
zh: d1d18fcee5735073
|
|
74
|
+
/qilin-tool-pwsh/model-experience/tool-schemas/token-effect:
|
|
75
|
+
en: 97cce067584eadff
|
|
76
|
+
zh: 54b1f5cd5899e7f3
|
|
77
|
+
/qilin-tool-pwsh/model-experience/tool-schemas/kv-cache-effect:
|
|
78
|
+
en: a6768b4fdc27de21
|
|
79
|
+
zh: 6d1d9b8357441af8
|
|
80
|
+
/qilin-tool-pwsh/model-experience/foreground-result:
|
|
81
|
+
en: d83f1f25a3719c54
|
|
82
|
+
zh: 445ca808e28dc936
|
|
83
|
+
/qilin-tool-pwsh/model-experience/foreground-result/what-the-model-sees:
|
|
84
|
+
en: 790fd2ecc7e1fd38
|
|
85
|
+
zh: 76b3da098b1b5cf8
|
|
86
|
+
/qilin-tool-pwsh/model-experience/foreground-result/token-effect:
|
|
87
|
+
en: b2018adbda15b8a7
|
|
88
|
+
zh: efa06f083800a36b
|
|
89
|
+
/qilin-tool-pwsh/model-experience/foreground-result/kv-cache-effect:
|
|
90
|
+
en: f5d5f97a153f7c70
|
|
91
|
+
zh: 31911b4f3506d6c8
|
|
92
|
+
/qilin-tool-pwsh/model-experience/background-result:
|
|
93
|
+
en: 3f23f9211197b0b9
|
|
94
|
+
zh: 8cff3c78249b66ff
|
|
95
|
+
/qilin-tool-pwsh/model-experience/background-result/what-the-model-sees:
|
|
96
|
+
en: a471e423bb4fd271
|
|
97
|
+
zh: 8949b37acf642e0c
|
|
98
|
+
/qilin-tool-pwsh/model-experience/background-result/token-effect:
|
|
99
|
+
en: 1480a7a9ec4e3a96
|
|
100
|
+
zh: c7ec0cee960c8c99
|
|
101
|
+
/qilin-tool-pwsh/model-experience/background-result/kv-cache-effect:
|
|
102
|
+
en: f5d5f97a153f7c70
|
|
103
|
+
zh: 31911b4f3506d6c8
|
|
104
|
+
/qilin-tool-pwsh/model-experience/tool-errors:
|
|
105
|
+
en: 2d4f2c8605eccab3
|
|
106
|
+
zh: 67f1f0218f73c6bf
|
|
107
|
+
/qilin-tool-pwsh/model-experience/tool-errors/what-the-model-sees:
|
|
108
|
+
en: efc765cc65699edb
|
|
109
|
+
zh: 072fe15a1979148e
|
|
110
|
+
/qilin-tool-pwsh/model-experience/tool-errors/token-effect:
|
|
111
|
+
en: 243bad012dd99c7f
|
|
112
|
+
zh: 5ee74905020bd49a
|
|
113
|
+
/qilin-tool-pwsh/model-experience/tool-errors/kv-cache-effect:
|
|
114
|
+
en: f5d5f97a153f7c70
|
|
115
|
+
zh: 31911b4f3506d6c8
|
|
116
|
+
/qilin-tool-pwsh/known-limitations-and-deferred-work:
|
|
117
|
+
en: 8414f06d98e2be52
|
|
118
|
+
zh: 64a484b891053be9
|
|
119
|
+
/qilin-tool-pwsh/known-limitations-and-deferred-work/dev-note:
|
|
120
|
+
en: e67f633dc6fe7773
|
|
121
|
+
zh: 3bb0c20c93f16f98
|
package/README.md
CHANGED
|
@@ -1,3 +1,212 @@
|
|
|
1
|
-
|
|
1
|
+
---
|
|
2
|
+
description: "The model-facing pwsh tool for users and maintainers choosing, configuring, or debugging one-shot PowerShell execution, background jobs, and sandbox escalation on Windows."
|
|
3
|
+
kind: "package-reference"
|
|
4
|
+
---
|
|
2
5
|
|
|
3
|
-
|
|
6
|
+
# @qilin-agent/tool-pwsh
|
|
7
|
+
|
|
8
|
+
English | [中文](README.zh.md)
|
|
9
|
+
|
|
10
|
+
## Summary
|
|
11
|
+
|
|
12
|
+
`qilin-tool-pwsh` gives the agent a `pwsh` tool that runs PowerShell commands through the mounted shell executor — the Windows counterpart of `qilin-tool-bash`, mirroring it call-for-call. Each call runs in a fresh pwsh process, so no state survives; `run_in_background` turns long-running commands into background jobs. Commands are PowerShell-dialect: native `C:\...` paths and `$env:NAME` variables, with no dialect translation. Every call runs with the managed `QILIN_*` environment, and under a sandboxing executor the tool teaches and enforces the Windows-specific language-mode and named-pipe contracts. Mount it with a PowerShell executor such as `qilin-pwsh-local` and the `qilin-shell-env` plugin.
|
|
13
|
+
|
|
14
|
+
## Table of Contents
|
|
15
|
+
|
|
16
|
+
- [Use this package](#use-this-package)
|
|
17
|
+
- [Understand the implementation](#understand-the-implementation)
|
|
18
|
+
- [Further Exploration](#further-exploration)
|
|
19
|
+
- [Model Experience](#model-experience)
|
|
20
|
+
- [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
|
|
21
|
+
- [Dev Note](#dev-note)
|
|
22
|
+
|
|
23
|
+
-----
|
|
24
|
+
|
|
25
|
+
<a id="use-this-package"></a>
|
|
26
|
+
## Use this package
|
|
27
|
+
|
|
28
|
+
Load this plugin in any composition where the agent should run PowerShell commands — typically a Windows composition whose `ctx.shell` is backed by a PowerShell executor. It registers the `pwsh` tool once the executor provider and the `qilin-shell-env` registry are mounted.
|
|
29
|
+
|
|
30
|
+
### When to choose it
|
|
31
|
+
|
|
32
|
+
Choose the pwsh tool when commands must be written in PowerShell — native paths and `$env:` variables — or when the deployment is Windows-native. Choose `qilin-tool-bash` when the command set is bash-dialect; there is no translation between the two. When work needs cross-call state (cwd, variables), the persistent counterpart [`qilin-tool-pwsh-persistent`](../tool-pwsh-persistent/README.md) keeps one owner-scoped shell alive.
|
|
33
|
+
|
|
34
|
+
### Minimal configuration
|
|
35
|
+
|
|
36
|
+
The common path is a PowerShell executor provider, the environment registry, and this tool.
|
|
37
|
+
|
|
38
|
+
```yaml
|
|
39
|
+
- name: '@qilin-agent/pwsh-local'
|
|
40
|
+
- name: '@qilin-agent/shell-env'
|
|
41
|
+
- name: '@qilin-agent/tool-pwsh'
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The single config field toggles background support.
|
|
45
|
+
|
|
46
|
+
| Field | Default | Meaning |
|
|
47
|
+
|---|---|---|
|
|
48
|
+
| `enableRunInBackground` | `true` | Expose `run_in_background`; when `false`, forced background calls are rejected |
|
|
49
|
+
|
|
50
|
+
The generated [configuration catalog](../../../docs/config-catalog.md#qilin-agenttool-pwsh) is the exhaustive source for every accepted field and its JSDoc; the generated [tool catalog](../../../docs/tool-catalog.md#qilin-agenttool-pwsh) carries the full argument schema.
|
|
51
|
+
|
|
52
|
+
### Running a command
|
|
53
|
+
|
|
54
|
+
The tool executes `pwsh -Command <command>` and returns the combined output. Commands run in a fresh pwsh process every call, so state never persists — pass `workdir` instead of `cd`. Paths use native Windows form and environment variables are read with `$env:NAME`. A non-zero exit is reported as `[exit code: N]`; on Windows a force-killed command settles as `[exit code: 1]` without a signal marker, so the agent treats a bare exit 1 after an interruption as a termination, not a command failure. The description tells the agent to verify the resolved absolute target path before any delete or move and not to assign to automatic variables such as `$HOME`. Background runs, output truncation, and the `description`/`timeoutMs`/`workdir` arguments behave exactly as in [`qilin-tool-bash`](../tool-bash/README.md#running-long-commands-in-the-background), including job-owned cancellation during asynchronous shell preparation.
|
|
55
|
+
|
|
56
|
+
### Windows-specific sandbox behavior
|
|
57
|
+
|
|
58
|
+
Under a sandboxing executor, denied commands report `[sandbox: file access denied under <mode> mode]`, and the same one-shot escalation path applies: retry the exact command once with `sandbox_permissions` plus a `justification` through user approval. The tool also teaches two Windows-restricted-token contracts in its description: read-only pwsh runs in ConstrainedLanguage (`.NET` static calls, `Add-Type`, COM, and reflection fail with "only core types" errors), and in both confined modes programs cannot open named pipes, so a command that captures another program's output through piped stdio fails with EPERM — escalate the exact command once or restructure it to avoid capturing output.
|
|
59
|
+
|
|
60
|
+
### What can go wrong
|
|
61
|
+
|
|
62
|
+
A composition with no PowerShell executor never activates the tool, and the injected services (`tools`, `shell`, `systemPrompt`, `shellEnv`) must all exist. Background calls without the job runtime fail with `background jobs unavailable: load @qilin-agent/jobs and @qilin-agent/tool-jobs`, and `sandbox_permissions` without a sandboxing executor fails with `sandbox_permissions is not available in this composition (no sandboxing executor to escalate)`.
|
|
63
|
+
|
|
64
|
+
-----
|
|
65
|
+
|
|
66
|
+
<a id="understand-the-implementation"></a>
|
|
67
|
+
## Understand the implementation
|
|
68
|
+
|
|
69
|
+
<details>
|
|
70
|
+
<summary>Implementation internals — click to expand</summary>
|
|
71
|
+
|
|
72
|
+
This section explains the design decisions behind the tool and points at the code that realizes them; the observable behavior is fully covered in [Use this package](#use-this-package).
|
|
73
|
+
|
|
74
|
+
### Design philosophy
|
|
75
|
+
|
|
76
|
+
- **A deliberate twin of `qilin-tool-bash`.** Foreground and background execution, the managed environment, the sandbox escalation surface, and the marker/truncation rendering mirror the bash tool call-for-call, so consumers of one accept the other's wire shape ([pwsh tool bash parity Agent Note](../../../.agents/notes/implemented/feature/2026-08-02-pwsh-tool-bash-parity.md)).
|
|
77
|
+
- **PowerShell-dialect contract.** The tool contract is PowerShell: native paths and `$env:` variables, executed via `pwsh -Command` with no intermediate shell.
|
|
78
|
+
- **Windows sandbox facts taught in the description.** The ConstrainedLanguage and named-pipe contracts are Windows-restricted-token behavior; the gate for teaching them is "any confining executor is mounted", which is safe because every shipped pairing is win32-only.
|
|
79
|
+
- **Non-zero exits are reported, not errored.** Only infrastructure failures (spawn errors, aborts) surface as tool errors, matching the bash story.
|
|
80
|
+
|
|
81
|
+
### Source map
|
|
82
|
+
|
|
83
|
+
| File | Role |
|
|
84
|
+
|---|---|
|
|
85
|
+
| [`src/index.ts`](src/index.ts) | Plugin entry: tool registration, prompt section, arg validation, escalation, request assembly |
|
|
86
|
+
| [`src/background.ts`](src/background.ts) | Map a settled background process onto generic job outcome vocabulary |
|
|
87
|
+
| [`src/render.ts`](src/render.ts) | Model-facing result text: streams, markers, truncation notices (bash twin) |
|
|
88
|
+
|
|
89
|
+
### Rendering and exit markers
|
|
90
|
+
|
|
91
|
+
The renderer shares the bash tool's structure and the `parseExitStatus` marker contract from `qilin-shell`: a clean exit (0, no signal) produces no marker; the UI card consumes the exit marker as its exit-status pill. Windows forced termination settles as exit 1 without a signal, so `[killed by signal: …]` is POSIX-only there. The `tool:pwsh` prompt section (first-party order 1010) teaches the exit-marker convention and the Windows exit-1-after-interruption reading.
|
|
92
|
+
|
|
93
|
+
</details>
|
|
94
|
+
|
|
95
|
+
-----
|
|
96
|
+
|
|
97
|
+
<a id="further-exploration"></a>
|
|
98
|
+
## Further Exploration
|
|
99
|
+
|
|
100
|
+
Read these pages when the package-level contract is not enough. They move from the shell family to the executor seam and the design notes behind the Windows behavior.
|
|
101
|
+
|
|
102
|
+
- [shell package map](../README.md) — the bash capability family and its roles.
|
|
103
|
+
- [Bash executor subsystem](../../../docs/subsystems/shell.md) — request/spec vocabulary, results, and background processes.
|
|
104
|
+
- [shell-env](../shell-env/README.md) — the managed `QILIN_*` environment every call receives.
|
|
105
|
+
- [tool-jobs](../../jobs/tool-jobs/README.md) — `job_output`, `job_list`, and `job_kill` controls for background runs.
|
|
106
|
+
- [pwsh tool bash parity Agent Note](../../../.agents/notes/implemented/feature/2026-08-02-pwsh-tool-bash-parity.md) — why the tool mirrors the bash tool.
|
|
107
|
+
- [Windows ACL restricted-token sandbox Agent Note](../../../.agents/notes/implemented/feature/2026-08-08-windows-acl-restricted-token-sandbox.md) — the language-mode and named-pipe contracts.
|
|
108
|
+
- [Generated tool catalog](../../../docs/tool-catalog.md#qilin-agenttool-pwsh) — the exact `pwsh` argument schema.
|
|
109
|
+
- [Generated configuration catalog](../../../docs/config-catalog.md#qilin-agenttool-pwsh) — every accepted config field and its source declaration.
|
|
110
|
+
|
|
111
|
+
-----
|
|
112
|
+
|
|
113
|
+
<a id="model-experience"></a>
|
|
114
|
+
## Model Experience
|
|
115
|
+
|
|
116
|
+
### System prompt
|
|
117
|
+
|
|
118
|
+
#### What the model sees
|
|
119
|
+
|
|
120
|
+
Every request in this plugin's registration scope contains the pwsh guidance below at first-party order 1010. Scoped tool restrictions can hide the schema without removing this independently registered section.
|
|
121
|
+
|
|
122
|
+
##### Pwsh guidance
|
|
123
|
+
|
|
124
|
+
```markdown
|
|
125
|
+
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.
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
#### Token effect
|
|
129
|
+
|
|
130
|
+
Small fixed input cost per request while the plugin is active.
|
|
131
|
+
|
|
132
|
+
#### KV Cache effect
|
|
133
|
+
|
|
134
|
+
Prefix-stable while the registration scope and prompt text are unchanged. Plugin activation or disposal may invalidate reuse from this prompt section.
|
|
135
|
+
|
|
136
|
+
### Tool schemas
|
|
137
|
+
|
|
138
|
+
#### What the model sees
|
|
139
|
+
|
|
140
|
+
The model sees the generated [`pwsh` schema](../../../docs/tool-catalog.md#qilin-agenttool-pwsh). Agent-scoped tool restrictions can remove the definition for that agent.
|
|
141
|
+
|
|
142
|
+
#### Token effect
|
|
143
|
+
|
|
144
|
+
Fixed schema cost on every request where the tool is visible.
|
|
145
|
+
|
|
146
|
+
#### KV Cache effect
|
|
147
|
+
|
|
148
|
+
Prefix-stable while visibility and the tool definition are unchanged. A restriction or config change may invalidate reuse from the first changed token.
|
|
149
|
+
|
|
150
|
+
### Foreground result
|
|
151
|
+
|
|
152
|
+
#### What the model sees
|
|
153
|
+
|
|
154
|
+
The renderer emits the data-dependent stdout tail, then optional `[stderr]` and the stderr tail. Conditional lines are exactly `[output truncated; full output: <path-or-(unavailable)>]`, `[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)`.
|
|
155
|
+
|
|
156
|
+
#### Token effect
|
|
157
|
+
|
|
158
|
+
Zero result tokens before a call. Output is bounded per stream, while each emitted line remains in history until compaction.
|
|
159
|
+
|
|
160
|
+
#### KV Cache effect
|
|
161
|
+
|
|
162
|
+
Append-only; newly visible content follows the reusable request prefix and does not invalidate existing KV-cache entries.
|
|
163
|
+
|
|
164
|
+
### Background result
|
|
165
|
+
|
|
166
|
+
#### What the model sees
|
|
167
|
+
|
|
168
|
+
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.
|
|
169
|
+
|
|
170
|
+
#### Token effect
|
|
171
|
+
|
|
172
|
+
The ack is a fixed short line; job output is bounded per read.
|
|
173
|
+
|
|
174
|
+
#### KV Cache effect
|
|
175
|
+
|
|
176
|
+
Append-only; newly visible content follows the reusable request prefix and does not invalidate existing KV-cache entries.
|
|
177
|
+
|
|
178
|
+
### Tool errors
|
|
179
|
+
|
|
180
|
+
#### What the model sees
|
|
181
|
+
|
|
182
|
+
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>`, the escalation pairing failures, `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 @qilin-agent/jobs and @qilin-agent/tool-jobs`, and `tool call aborted`.
|
|
183
|
+
|
|
184
|
+
#### Token effect
|
|
185
|
+
|
|
186
|
+
Only the failing call adds these retained tokens; an aborted call adds no command output.
|
|
187
|
+
|
|
188
|
+
#### KV Cache effect
|
|
189
|
+
|
|
190
|
+
Append-only; newly visible content follows the reusable request prefix and does not invalidate existing KV-cache entries.
|
|
191
|
+
|
|
192
|
+
## Known Limitations and Deferred Work
|
|
193
|
+
|
|
194
|
+
<a id="known-limitations-and-deferred-work"></a>
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
These limits define when the tool is a poor fit or needs special care. They are current package constraints, not a task backlog.
|
|
198
|
+
|
|
199
|
+
- **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.
|
|
200
|
+
- **No persistent shell** — every call starts a fresh `pwsh -Command`; the persistent-shell counterpart is [`@qilin-agent/tool-pwsh-persistent`](../tool-pwsh-persistent/README.md), which keeps one owner-scoped pwsh alive across calls.
|
|
201
|
+
- **PowerShell-dialect contract** — the model must write PowerShell (native paths, `$env:` variables), not bash; there is no dialect translation.
|
|
202
|
+
- **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.
|
|
203
|
+
|
|
204
|
+
<a id="dev-note"></a>
|
|
205
|
+
### Dev Note
|
|
206
|
+
|
|
207
|
+
<details>
|
|
208
|
+
<summary>Working context for maintainers — click to expand</summary>
|
|
209
|
+
|
|
210
|
+
None.
|
|
211
|
+
|
|
212
|
+
</details>
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "面向模型的 pwsh 工具,供选择、配置或排查 Windows 上一次性 PowerShell 执行、后台任务与沙箱升权的使用者与维护者阅读。"
|
|
3
|
+
kind: "package-reference"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# @qilin-agent/tool-pwsh
|
|
7
|
+
|
|
8
|
+
[English](README.md) | 中文
|
|
9
|
+
|
|
10
|
+
## 概述
|
|
11
|
+
|
|
12
|
+
`qilin-tool-pwsh` 为 agent(智能体)提供 `pwsh` 工具,通过已挂载的 shell 执行器运行 PowerShell 命令——它是 `qilin-tool-bash` 的 Windows 对应物,逐调用镜像。每次调用都运行在全新 pwsh 进程中,因此状态不会保留;`run_in_background` 把长时间运行的命令变成后台任务。命令是 PowerShell 方言:原生 `C:\...` 路径与 `$env:NAME` 变量,不做方言翻译。每次调用都运行在受管 `QILIN_*` 环境中;在沙箱执行器下,工具会向模型说明并强制执行 Windows 特有的语言模式与命名管道约定。请与 `qilin-pwsh-local` 等 PowerShell 执行器以及 `qilin-shell-env` 插件一起挂载。
|
|
13
|
+
|
|
14
|
+
## 目录
|
|
15
|
+
|
|
16
|
+
- [使用本包](#use-this-package)
|
|
17
|
+
- [理解实现](#understand-the-implementation)
|
|
18
|
+
- [进一步探索](#further-exploration)
|
|
19
|
+
- [模型体验](#model-experience)
|
|
20
|
+
- [已知限制与延期工作](#known-limitations-and-deferred-work)
|
|
21
|
+
- [开发备注](#dev-note)
|
|
22
|
+
|
|
23
|
+
-----
|
|
24
|
+
|
|
25
|
+
<a id="use-this-package"></a>
|
|
26
|
+
## 使用本包
|
|
27
|
+
|
|
28
|
+
在 agent 需要运行 PowerShell 命令的任何组合中加载本插件——通常是 `ctx.shell` 由 PowerShell 执行器支撑的 Windows 组合。一旦挂载执行器提供方与 `qilin-shell-env` 注册表,它就注册 `pwsh` 工具。
|
|
29
|
+
|
|
30
|
+
### 何时选择
|
|
31
|
+
|
|
32
|
+
当命令必须用 PowerShell 编写——原生路径与 `$env:` 变量——或部署是 Windows 原生时,选择 pwsh 工具。当命令集是 bash 方言时选择 `qilin-tool-bash`;两者之间没有翻译。当工作依赖跨调用状态(cwd、变量)时,持久对应物 [`qilin-tool-pwsh-persistent`](../tool-pwsh-persistent/README.zh.md) 会保持一个按所有者隔离的 shell 存活。
|
|
33
|
+
|
|
34
|
+
### 最小配置
|
|
35
|
+
|
|
36
|
+
常用路径是 PowerShell 执行器提供方、环境注册表与本工具。
|
|
37
|
+
|
|
38
|
+
```yaml
|
|
39
|
+
- name: '@qilin-agent/pwsh-local'
|
|
40
|
+
- name: '@qilin-agent/shell-env'
|
|
41
|
+
- name: '@qilin-agent/tool-pwsh'
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
唯一的配置字段用于开关后台支持。
|
|
45
|
+
|
|
46
|
+
| 字段 | 默认值 | 含义 |
|
|
47
|
+
|---|---|---|
|
|
48
|
+
| `enableRunInBackground` | `true` | 暴露 `run_in_background`;为 `false` 时拒绝强制后台调用 |
|
|
49
|
+
|
|
50
|
+
生成的[配置目录](../../../docs/config-catalog.zh.md#qilin-agenttool-pwsh)是每个受支持字段及其 JSDoc 的穷尽式真源;生成的[工具目录](../../../docs/tool-catalog.zh.md#qilin-agenttool-pwsh)携带完整参数 schema。
|
|
51
|
+
|
|
52
|
+
### 运行命令
|
|
53
|
+
|
|
54
|
+
工具执行 `pwsh -Command <command>` 并返回合并后的输出。命令每次调用都运行在全新 pwsh 进程中,因此状态从不保留——请传 `workdir` 而不是 `cd`。路径使用原生 Windows 形式,环境变量用 `$env:NAME` 读取。非零退出以 `[exit code: N]` 报告;在 Windows 上,强制终止的命令以 `[exit code: 1]` 结算且没有信号标记,因此 agent 把中断后的裸 exit 1 当作终止而非命令失败。工具描述要求 agent 在任何删除或移动前校验解析后的绝对目标路径,并且不要给 `$HOME` 等自动变量赋值。后台运行、输出截断以及 `description`/`timeoutMs`/`workdir` 参数的行为与 [`qilin-tool-bash`](../tool-bash/README.zh.md#running-long-commands-in-the-background) 完全一致,包括异步 shell 准备过程中由任务负责的取消。
|
|
55
|
+
|
|
56
|
+
### Windows 特有的沙箱行为
|
|
57
|
+
|
|
58
|
+
在沙箱执行器下,被拒绝的命令会报告 `[sandbox: file access denied under <mode> mode]`,并适用相同的单次升权路径:用 `sandbox_permissions` 加一句 `justification`,经用户审批后重试完全相同的命令一次。工具还会在其描述中教授两条 Windows 受限令牌约定:只读 pwsh 运行在 ConstrainedLanguage 中(`.NET` 静态调用、`Add-Type`、COM 与反射会以 "only core types" 错误失败);两种受限模式下程序都无法打开命名管道,因此通过管道 stdio 捕获另一程序输出的命令会以 EPERM 失败——请升权该确切命令一次,或重构命令以避免捕获输出。
|
|
59
|
+
|
|
60
|
+
### 可能出什么问题
|
|
61
|
+
|
|
62
|
+
没有 PowerShell 执行器的组合永远不会激活该工具,且注入的服务(`tools`、`shell`、`systemPrompt`、`shellEnv`)必须全部存在。没有任务运行时的后台调用会以 `background jobs unavailable: load @qilin-agent/jobs and @qilin-agent/tool-jobs` 失败;没有沙箱执行器时的 `sandbox_permissions` 会以 `sandbox_permissions is not available in this composition (no sandboxing executor to escalate)` 失败。
|
|
63
|
+
|
|
64
|
+
-----
|
|
65
|
+
|
|
66
|
+
<a id="understand-the-implementation"></a>
|
|
67
|
+
## 理解实现
|
|
68
|
+
|
|
69
|
+
<details>
|
|
70
|
+
<summary>实现细节——点击展开</summary>
|
|
71
|
+
|
|
72
|
+
本节解释工具背后的设计决策,并指出实现它们的代码位置;可观察行为已在[使用本包](#use-this-package)中完整说明。
|
|
73
|
+
|
|
74
|
+
### 设计理念
|
|
75
|
+
|
|
76
|
+
- **`qilin-tool-bash` 的刻意孪生。** 前台与后台执行、受管环境、沙箱升权面以及标记/截断渲染都逐调用镜像 bash 工具,因此其中之一的消费方也能接受另一个的协议形状([pwsh 工具与 bash 对齐 Agent Note](../../../.agents/notes/implemented/feature/2026-08-02-pwsh-tool-bash-parity.zh.md))。
|
|
77
|
+
- **PowerShell 方言约定。** 工具约定是 PowerShell:原生路径与 `$env:` 变量,经由 `pwsh -Command` 执行,没有中间 shell。
|
|
78
|
+
- **Windows 沙箱事实写进描述。** ConstrainedLanguage 与命名管道约定是 Windows 受限令牌行为;教授它们的条件是「已挂载任意约束执行器」,之所以安全,是因为每个已发布的配对都是 win32-only。
|
|
79
|
+
- **非零退出只报告、不失败。** 只有基础设施故障(spawn 错误、中止)才会作为工具错误暴露,与 bash 的故事一致。
|
|
80
|
+
|
|
81
|
+
### 源码地图
|
|
82
|
+
|
|
83
|
+
| 文件 | 职责 |
|
|
84
|
+
|---|---|
|
|
85
|
+
| [`src/index.ts`](src/index.ts) | 插件入口:工具注册、提示词区段、参数校验、升权、请求组装 |
|
|
86
|
+
| [`src/background.ts`](src/background.ts) | 把已结算的后台进程映射为通用任务结果词汇 |
|
|
87
|
+
| [`src/render.ts`](src/render.ts) | 模型侧结果文本:流、标记、截断通知(bash 孪生) |
|
|
88
|
+
|
|
89
|
+
### 渲染与退出标记
|
|
90
|
+
|
|
91
|
+
渲染器共享 bash 工具的结构与来自 `qilin-shell` 的 `parseExitStatus` 标记约定:干净退出(0、无信号)不产生标记;UI 卡片把退出标记消费为退出状态 pill。Windows 强制终止以 exit 1 结算且没有信号,因此 `[killed by signal: …]` 仅适用于 POSIX。`tool:pwsh` 提示词区段(first-party 顺序 1010)教授退出标记约定与「中断后 exit 1」的 Windows 解读。
|
|
92
|
+
|
|
93
|
+
</details>
|
|
94
|
+
|
|
95
|
+
-----
|
|
96
|
+
|
|
97
|
+
<a id="further-exploration"></a>
|
|
98
|
+
## 进一步探索
|
|
99
|
+
|
|
100
|
+
当包级约定不够用时阅读以下页面。它们从 shell 家族逐步进入执行器 seam,以及 Windows 行为背后的设计笔记。
|
|
101
|
+
|
|
102
|
+
- [shell 包映射](../README.zh.md)——bash 能力家族及其角色。
|
|
103
|
+
- [Bash 执行器子系统](../../../docs/subsystems/shell.zh.md)——请求/spec 词汇、结果与后台进程。
|
|
104
|
+
- [shell-env](../shell-env/README.zh.md)——每次调用都会收到的受管 `QILIN_*` 环境。
|
|
105
|
+
- [tool-jobs](../../jobs/tool-jobs/README.zh.md)——后台运行的 `job_output`、`job_list` 与 `job_kill` 控制。
|
|
106
|
+
- [pwsh 工具与 bash 对齐 Agent Note](../../../.agents/notes/implemented/feature/2026-08-02-pwsh-tool-bash-parity.zh.md)——为什么工具镜像 bash 工具。
|
|
107
|
+
- [Windows ACL 受限令牌沙箱 Agent Note](../../../.agents/notes/implemented/feature/2026-08-08-windows-acl-restricted-token-sandbox.zh.md)——语言模式与命名管道约定。
|
|
108
|
+
- [生成的工具目录](../../../docs/tool-catalog.zh.md#qilin-agenttool-pwsh)——`pwsh` 参数 schema 的确切内容。
|
|
109
|
+
- [生成的配置目录](../../../docs/config-catalog.zh.md#qilin-agenttool-pwsh)——每个受支持配置字段及其源声明。
|
|
110
|
+
|
|
111
|
+
-----
|
|
112
|
+
|
|
113
|
+
<a id="model-experience"></a>
|
|
114
|
+
## 模型体验
|
|
115
|
+
|
|
116
|
+
### 系统提示词
|
|
117
|
+
|
|
118
|
+
#### 模型看到什么
|
|
119
|
+
|
|
120
|
+
该插件注册作用域内的每次请求都在 first-party 顺序 1010 处包含以下 pwsh 指引。按作用域实施的工具限制可以隐藏 schema,却不会移除这个独立注册的区段。
|
|
121
|
+
|
|
122
|
+
##### Pwsh 指引
|
|
123
|
+
|
|
124
|
+
```markdown
|
|
125
|
+
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.
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
#### Token 影响
|
|
129
|
+
|
|
130
|
+
插件激活期间,每次请求都会产生少量固定的输入 token 开销。
|
|
131
|
+
|
|
132
|
+
#### KV Cache 影响
|
|
133
|
+
|
|
134
|
+
只要注册作用域与提示词文本不变,前缀就保持稳定。插件激活或释放可能使从该提示词区段起的复用失效。
|
|
135
|
+
|
|
136
|
+
### 工具 schema
|
|
137
|
+
|
|
138
|
+
#### 模型看到什么
|
|
139
|
+
|
|
140
|
+
模型会看到生成的 [`pwsh` schema](../../../docs/tool-catalog.zh.md#qilin-agenttool-pwsh)。按 agent 作用域实施的工具限制可以移除该 agent 的定义。
|
|
141
|
+
|
|
142
|
+
#### Token 影响
|
|
143
|
+
|
|
144
|
+
工具可见的每个请求都会产生固定 schema 开销。
|
|
145
|
+
|
|
146
|
+
#### KV Cache 影响
|
|
147
|
+
|
|
148
|
+
只要可见性与工具定义不变,前缀就保持稳定。限制或配置变化可能从首个变化的 token 开始使复用失效。
|
|
149
|
+
|
|
150
|
+
### 前台结果
|
|
151
|
+
|
|
152
|
+
#### 模型看到什么
|
|
153
|
+
|
|
154
|
+
渲染器输出依数据而定的 stdout 尾部,再输出可选的 `[stderr]` 和 stderr 尾部。条件行精确为 `[output truncated; full output: <path-or-(unavailable)>]`、`[sandbox: file access denied under <mode> mode]` 加升权提示 `[sandbox: escalation available — …]`(仅在组合声明升权时)、`[timed out after <timeoutMs>ms]`、`[killed by signal: <signal>]` 与 `[exit code: <exitCode>]`(仅非零退出);空正文渲染为 `(no output)`。
|
|
155
|
+
|
|
156
|
+
#### Token 影响
|
|
157
|
+
|
|
158
|
+
调用前的结果 token 为零。输出按流设界,而每行已发出的内容在压缩(compaction)前保留于历史。
|
|
159
|
+
|
|
160
|
+
#### KV Cache 影响
|
|
161
|
+
|
|
162
|
+
仅追加;新可见内容位于可复用请求前缀之后,不会使现有 KV-cache 条目失效。
|
|
163
|
+
|
|
164
|
+
### 后台结果
|
|
165
|
+
|
|
166
|
+
#### 模型看到什么
|
|
167
|
+
|
|
168
|
+
后台启动精确渲染为 `started background job <id>`;随后的读取与状态经由通用 `job_output`/`job_kill` 工具流转,包括内存截断丢弃未读字节时的有损读取 spill 通知。
|
|
169
|
+
|
|
170
|
+
#### Token 影响
|
|
171
|
+
|
|
172
|
+
确认是一行固定的短文本;任务输出按每次读取设界。
|
|
173
|
+
|
|
174
|
+
#### KV Cache 影响
|
|
175
|
+
|
|
176
|
+
仅追加;新可见内容位于可复用请求前缀之后,不会使现有 KV-cache 条目失效。
|
|
177
|
+
|
|
178
|
+
### 工具错误
|
|
179
|
+
|
|
180
|
+
#### 模型看到什么
|
|
181
|
+
|
|
182
|
+
验证与基础设施失败统一为 `Error: <message>`。本包的稳定消息包括 `invalid command: expected a non-empty string`、`invalid description: expected a non-empty string`、`invalid timeoutMs: expected a positive number, got <value>`、升权配对失败、`sandbox_permissions is not available in this composition (no sandboxing executor to escalate)`、共享升权失败(未严格加宽/无审批服务/无 agent 可路由/无审批通道/用户拒绝/已取消)、`run_in_background is disabled for this deployment (enableRunInBackground: false)`、`background jobs unavailable: load @qilin-agent/jobs and @qilin-agent/tool-jobs`,以及 `tool call aborted`。
|
|
183
|
+
|
|
184
|
+
#### Token 影响
|
|
185
|
+
|
|
186
|
+
只有失败调用会增加这些保留 token;被中止的调用不会添加命令输出。
|
|
187
|
+
|
|
188
|
+
#### KV Cache 影响
|
|
189
|
+
|
|
190
|
+
仅追加;新可见内容位于可复用请求前缀之后,不会使现有 KV-cache 条目失效。
|
|
191
|
+
|
|
192
|
+
## 已知限制与延期工作
|
|
193
|
+
|
|
194
|
+
<a id="known-limitations-and-deferred-work"></a>
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
这些限制说明工具何时不合适或需要特别小心。它们是当前包约束,不是任务积压。
|
|
198
|
+
|
|
199
|
+
- **Windows 沙箱下的语言模式与命名管道捕获**——在 [Windows ACL 沙箱](../../sandbox/sandbox-windows-acl/README.zh.md)下,只读 pwsh 以 ConstrainedLanguage 启动,因为其临时目录写入被拒绝,导致 PowerShell 的 AppLocker 探测失败并按拒绝处理:`Add-Type`、非核心 .NET 静态调用(`[System.IO.*]::`、`[math]::`)、COM 对象与反射会以 "only core types" 错误失败,且该模式无法从内部解除。workspace-write 的私有临时目录让探测完成,因此除非宿主策略另有规定,它保持 FullLanguage。两种受限模式都拒绝命名管道打开,因此受限命令内部的管道 stdio spawn 会以 EPERM 失败。工具描述把两条约定都教给模型;完整限制以后端 README 为准。
|
|
200
|
+
- **没有持久 shell**——每次调用都启动全新的 `pwsh -Command`;持久 shell 对应物是 [`@qilin-agent/tool-pwsh-persistent`](../tool-pwsh-persistent/README.zh.md),它跨调用保持一个按所有者隔离的 pwsh 存活。
|
|
201
|
+
- **PowerShell 方言约定**——模型必须编写 PowerShell(原生路径、`$env:` 变量),而不是 bash;没有方言翻译。
|
|
202
|
+
- **会话 cwd 身份未规范化**——workdir 基准就是会话头部 cwd 原样,不像 bash 工具那样以沙箱根规范化身份为准。在约束执行器下,策略的 workspace root 确实被规范化(由共享策略服务完成),因此当原始会话 cwd 与其规范形式不同时,workdir 与约束根可能分叉——这是推迟到共享 shell 工具基座抽取的对齐差距。
|
|
203
|
+
|
|
204
|
+
<a id="dev-note"></a>
|
|
205
|
+
### 开发备注
|
|
206
|
+
|
|
207
|
+
<details>
|
|
208
|
+
<summary>维护者的工作上下文——点击展开</summary>
|
|
209
|
+
|
|
210
|
+
无。
|
|
211
|
+
|
|
212
|
+
</details>
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
import { isAbsolute, resolve } from "node:path";
|
|
2
|
+
import z from "@qilin-agent/schemastery";
|
|
3
|
+
import { TOOL_ABORTED, defineTool } from "@qilin-agent/tools";
|
|
4
|
+
import { HarnessError } from "@qilin-agent/llm";
|
|
5
|
+
import { ESCALATION_TARGETS, approveEscalation, escalationHintMarker, sandboxDenialMarker, validateEscalationArgs } from "@qilin-agent/sandbox";
|
|
6
|
+
import { parseExitStatus } from "@qilin-agent/shell";
|
|
7
|
+
//#region lib/types/render.js
|
|
8
|
+
/**
|
|
9
|
+
* Model-facing result rendering for the pwsh tool — the PowerShell twin of
|
|
10
|
+
* `qilin-tool-bash`'s renderer: stdout, a marked stderr section, sandbox
|
|
11
|
+
* denial/runner-failure markers (with the same-turn escalation hint), and
|
|
12
|
+
* truncation notices with spill paths, then exit-status markers. Non-zero
|
|
13
|
+
* exits are reported, not errored — the model decides how to react; only
|
|
14
|
+
* infrastructure failures (spawn errors, aborts) surface as isError
|
|
15
|
+
* results.
|
|
16
|
+
*
|
|
17
|
+
* @module @qilin-agent/tool-pwsh/render
|
|
18
|
+
*/
|
|
19
|
+
/** Append the truncation notice (with the full-output spill path) to a stream's text. */
|
|
20
|
+
function streamText(output) {
|
|
21
|
+
if (!output.truncated) return output.text;
|
|
22
|
+
return `${output.text}\n[output truncated; full output: ${output.spillPath ?? "(unavailable)"}]`;
|
|
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
|
+
function renderPwshResult(result, escalationModes = []) {
|
|
35
|
+
const out = streamText(result.stdout);
|
|
36
|
+
const err = streamText(result.stderr);
|
|
37
|
+
let body = out;
|
|
38
|
+
if (err.length > 0) {
|
|
39
|
+
if (body.length > 0 && !body.endsWith("\n")) body += "\n";
|
|
40
|
+
body += `[stderr]\n${err}`;
|
|
41
|
+
}
|
|
42
|
+
if (body.length === 0) body = "(no output)";
|
|
43
|
+
const markers = [];
|
|
44
|
+
if (result.sandbox?.denied) {
|
|
45
|
+
markers.push(sandboxDenialMarker(result.sandbox.mode));
|
|
46
|
+
if (escalationModes.length > 0) markers.push(escalationHintMarker("command"));
|
|
47
|
+
}
|
|
48
|
+
if (result.timedOut) markers.push(`[timed out after ${result.timeoutMs}ms]`);
|
|
49
|
+
if (result.signal !== null) markers.push(`[killed by signal: ${result.signal}]`);
|
|
50
|
+
else if (result.exitCode !== 0) markers.push(`[exit code: ${result.exitCode}]`);
|
|
51
|
+
if (markers.length === 0) return body;
|
|
52
|
+
if (!body.endsWith("\n")) body += "\n";
|
|
53
|
+
return body + markers.join("\n");
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Shape one background-process read into the `job_output` delta the model
|
|
57
|
+
* sees: the incremental delta, plus the lossy-read notice (with full-stream
|
|
58
|
+
* spill paths) when in-memory truncation dropped unread bytes.
|
|
59
|
+
* @param read - one incremental read from the process handle.
|
|
60
|
+
* @param sandbox - settled sandbox facts, when this was a confined process.
|
|
61
|
+
* @param escalationModes - escalation targets advertised by this composition.
|
|
62
|
+
* @returns the delta text with any loss or sandbox notice appended.
|
|
63
|
+
*/
|
|
64
|
+
function renderPwshProcessRead(read, sandbox, escalationModes = []) {
|
|
65
|
+
const notices = [];
|
|
66
|
+
if (read.lossy) {
|
|
67
|
+
const paths = [read.stdoutSpillPath, read.stderrSpillPath].filter((path) => path !== void 0);
|
|
68
|
+
notices.push(`[some output was dropped from memory; full output: ${paths.length > 0 ? paths.join(", ") : "(unavailable)"}]`);
|
|
69
|
+
}
|
|
70
|
+
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]`);
|
|
71
|
+
else if (sandbox?.denied) {
|
|
72
|
+
notices.push(sandboxDenialMarker(sandbox.mode));
|
|
73
|
+
if (escalationModes.length > 0) notices.push(escalationHintMarker("command"));
|
|
74
|
+
}
|
|
75
|
+
if (notices.length === 0) return read.delta;
|
|
76
|
+
return `${read.delta}${read.delta.length > 0 && !read.delta.endsWith("\n") ? "\n" : ""}${notices.join("\n")}`;
|
|
77
|
+
}
|
|
78
|
+
//#endregion
|
|
79
|
+
//#region lib/types/background.js
|
|
80
|
+
/**
|
|
81
|
+
* Generic-job adaptation for pwsh process handles: the terminal outcome the
|
|
82
|
+
* registry records and the pull source it pumps into the job's output ring.
|
|
83
|
+
*
|
|
84
|
+
* @module @qilin-agent/tool-pwsh/background
|
|
85
|
+
*/
|
|
86
|
+
/**
|
|
87
|
+
* Sandbox facts worth the terminal detail: a runner that never ran the
|
|
88
|
+
* command, or a denial (with the escalation hint this composition offers).
|
|
89
|
+
* @param sandbox - settled sandbox facts, when this was a confined process.
|
|
90
|
+
* @param escalationModes - escalation targets advertised by this composition.
|
|
91
|
+
* @returns the markers to append, oldest first.
|
|
92
|
+
*/
|
|
93
|
+
function sandboxNotes(sandbox, escalationModes) {
|
|
94
|
+
if (sandbox?.runnerFailed) return [`[sandbox: the sandbox runner itself failed under ${sandbox.mode} mode — the command did not run; this is a sandbox problem, not a command failure]`];
|
|
95
|
+
if (sandbox?.denied) {
|
|
96
|
+
const notes = [sandboxDenialMarker(sandbox.mode)];
|
|
97
|
+
if (escalationModes.length > 0) notes.push(escalationHintMarker("command"));
|
|
98
|
+
return notes;
|
|
99
|
+
}
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Map a settled background process onto the generic job-outcome vocabulary:
|
|
104
|
+
* `killed` stays `killed` (detail: the signal when one is known), everything
|
|
105
|
+
* else is `completed` with the exit code as detail. A nonzero command exit is
|
|
106
|
+
* reported, not failed, exactly like the foreground rendering. Sandbox facts
|
|
107
|
+
* join the detail, since a job's terminal reason is the one line every
|
|
108
|
+
* reader — the model's status line, the roster row — shows.
|
|
109
|
+
* @param proc - the settled process handle.
|
|
110
|
+
* @param escalationModes - escalation targets advertised by this composition.
|
|
111
|
+
* @returns the outcome for the `ctx.jobs` registration.
|
|
112
|
+
*/
|
|
113
|
+
function processOutcome(proc, escalationModes = []) {
|
|
114
|
+
const base = proc.status === "killed" ? {
|
|
115
|
+
status: "killed",
|
|
116
|
+
detail: proc.signal !== null ? `signal: ${proc.signal}` : "killed before exit"
|
|
117
|
+
} : {
|
|
118
|
+
status: "completed",
|
|
119
|
+
detail: `exit code: ${proc.exitCode ?? 0}`
|
|
120
|
+
};
|
|
121
|
+
const notes = sandboxNotes(proc.sandbox, escalationModes);
|
|
122
|
+
return notes.length === 0 ? base : {
|
|
123
|
+
...base,
|
|
124
|
+
detail: `${base.detail}; ${notes.join(" ")}`
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* The process's captured output as a registry pull source. It binds lazily
|
|
129
|
+
* because the process is spawned inside the starter, after the registry
|
|
130
|
+
* admitted the job; a read before the spawn yields nothing.
|
|
131
|
+
*
|
|
132
|
+
* The shell handle's own reader is consuming — each call hands over what
|
|
133
|
+
* arrived since the previous one — so this source counts the bytes it
|
|
134
|
+
* delivered to keep the registry's cursor monotonic, exactly as the terminal
|
|
135
|
+
* tool adapts its own consuming send reader. Truncation is reported in-band by
|
|
136
|
+
* the read rendering, which names the spill files, so the source has no
|
|
137
|
+
* separate spill path to advertise.
|
|
138
|
+
* @param proc - the started process, once the starter has spawned it.
|
|
139
|
+
* @param escalationModes - escalation targets advertised by this composition.
|
|
140
|
+
* @returns the single source over the process's captured streams.
|
|
141
|
+
*/
|
|
142
|
+
function processSources(proc, escalationModes = []) {
|
|
143
|
+
return [{ read: (fromByte) => {
|
|
144
|
+
const live = proc();
|
|
145
|
+
if (live === void 0) return {
|
|
146
|
+
text: "",
|
|
147
|
+
nextOffset: fromByte,
|
|
148
|
+
lossy: false
|
|
149
|
+
};
|
|
150
|
+
const text = renderPwshProcessRead(live.readOutput(), live.sandbox, escalationModes);
|
|
151
|
+
return {
|
|
152
|
+
text,
|
|
153
|
+
nextOffset: fromByte + Buffer.byteLength(text, "utf8"),
|
|
154
|
+
lossy: false
|
|
155
|
+
};
|
|
156
|
+
} }];
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Adapt asynchronous shell preparation after job admission without exposing a partial process.
|
|
160
|
+
* @param start - starts the process with job-owned cancellation.
|
|
161
|
+
* @param outcome - projects the settled process into the job outcome.
|
|
162
|
+
* @returns synchronous job hooks whose completion includes preparation and process settlement.
|
|
163
|
+
*/
|
|
164
|
+
function processJob(start, outcome) {
|
|
165
|
+
const controller = new AbortController();
|
|
166
|
+
let process;
|
|
167
|
+
return {
|
|
168
|
+
cancel: (reason) => {
|
|
169
|
+
if (controller.signal.aborted) return;
|
|
170
|
+
controller.abort(reason);
|
|
171
|
+
process?.kill();
|
|
172
|
+
},
|
|
173
|
+
done: (async () => {
|
|
174
|
+
try {
|
|
175
|
+
process = await start(controller.signal);
|
|
176
|
+
try {
|
|
177
|
+
if (controller.signal.aborted) process.kill();
|
|
178
|
+
} finally {
|
|
179
|
+
await process.done;
|
|
180
|
+
}
|
|
181
|
+
return outcome(process);
|
|
182
|
+
} catch (error) {
|
|
183
|
+
return {
|
|
184
|
+
status: controller.signal.aborted && process === void 0 ? "killed" : "failed",
|
|
185
|
+
detail: error instanceof Error ? error.message : String(error)
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
})()
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
//#endregion
|
|
192
|
+
//#region lib/types/index.js
|
|
193
|
+
/**
|
|
194
|
+
* Model-facing PowerShell Consumer of the `ctx.shell` capability seam. Intended for
|
|
195
|
+
* Windows compositions where a PowerShell executor (e.g.
|
|
196
|
+
* `@qilin-agent/pwsh-local`) backs `ctx.shell`; the tool contract is
|
|
197
|
+
* PowerShell-dialect: native `C:\...` paths and `$env:NAME` variables.
|
|
198
|
+
*
|
|
199
|
+
* Behavior mirrors `qilin-tool-bash` call-for-call: foreground and
|
|
200
|
+
* `run_in_background` execution (background handles register with the
|
|
201
|
+
* generic `ctx.jobs` runtime), the managed `QILIN_*` environment through the
|
|
202
|
+
* shared `shell-env` registry, the per-call sandbox policy resolution (the
|
|
203
|
+
* calling session's mode and cwd travel to the confining executor), the
|
|
204
|
+
* sandbox-denial rendering with the same-turn escalation surface
|
|
205
|
+
* (`sandbox_permissions` + `justification` resolved through
|
|
206
|
+
* `ctx.approval`), and the bash marker/truncation rendering story. UI
|
|
207
|
+
* presentation mirrors the bash tool's too: a completed foreground call is
|
|
208
|
+
* a terminal card with the parsed exit-status pill, using the shared
|
|
209
|
+
* exit-status parse from `@qilin-agent/shell`.
|
|
210
|
+
*
|
|
211
|
+
* @module @qilin-agent/tool-pwsh
|
|
212
|
+
*/
|
|
213
|
+
const name = "tool-pwsh";
|
|
214
|
+
const inject = [
|
|
215
|
+
"tools",
|
|
216
|
+
"shell",
|
|
217
|
+
"systemPrompt",
|
|
218
|
+
"shellEnv"
|
|
219
|
+
];
|
|
220
|
+
/** Runtime configuration schema for the pwsh tool plugin. */
|
|
221
|
+
const Config = z.object({ enableRunInBackground: z.boolean().default(true) });
|
|
222
|
+
function validatePwshArgs(args) {
|
|
223
|
+
if (args.command.trim().length === 0) throw new Error("invalid command: expected a non-empty string");
|
|
224
|
+
if (args.description.trim().length === 0) throw new Error("invalid description: expected a non-empty string");
|
|
225
|
+
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)}`);
|
|
226
|
+
validateEscalationArgs(args.sandbox_permissions, args.justification);
|
|
227
|
+
}
|
|
228
|
+
function pwshDescription(backgroundEnabled, escalationModes) {
|
|
229
|
+
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:QILIN_*` 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. Before any delete or move, verify that the resolved absolute target path is the intended one; never run it against a computed path you have not checked. Do not assign to automatic variables such as `$HOME`; variable names are case-insensitive, so `$home` is the same read-only variable. 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.");
|
|
230
|
+
if (escalationModes.length === 0) return base;
|
|
231
|
+
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.";
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Resolve an explicit workdir first, making a relative one session-workspace-relative;
|
|
235
|
+
* otherwise use the session header cwd and leave executor defaulting as the fallback.
|
|
236
|
+
*/
|
|
237
|
+
function resolveWorkdir(modelWorkdir, exec) {
|
|
238
|
+
const headerCwd = exec.agent?.session.header.cwd;
|
|
239
|
+
if (modelWorkdir === void 0) return headerCwd;
|
|
240
|
+
if (headerCwd !== void 0 && !isAbsolute(modelWorkdir)) return resolve(headerCwd, modelWorkdir);
|
|
241
|
+
return modelWorkdir;
|
|
242
|
+
}
|
|
243
|
+
/** Detach the executor DTO from readonly Service Definition types into plain JSON data. */
|
|
244
|
+
function canonicalPwshResult(result) {
|
|
245
|
+
const output = (stream) => ({
|
|
246
|
+
text: stream.text,
|
|
247
|
+
truncated: stream.truncated,
|
|
248
|
+
...stream.spillPath !== void 0 ? { spillPath: stream.spillPath } : {}
|
|
249
|
+
});
|
|
250
|
+
return {
|
|
251
|
+
kind: "foreground",
|
|
252
|
+
exitCode: result.exitCode,
|
|
253
|
+
signal: result.signal,
|
|
254
|
+
timedOut: result.timedOut,
|
|
255
|
+
aborted: result.aborted,
|
|
256
|
+
timeoutMs: result.timeoutMs,
|
|
257
|
+
stdout: output(result.stdout),
|
|
258
|
+
stderr: output(result.stderr),
|
|
259
|
+
...result.sandbox !== void 0 ? { sandbox: {
|
|
260
|
+
mode: result.sandbox.mode,
|
|
261
|
+
denied: result.sandbox.denied,
|
|
262
|
+
...result.sandbox.enforcement !== void 0 ? { enforcement: result.sandbox.enforcement } : {},
|
|
263
|
+
...result.sandbox.runnerFailed !== void 0 ? { runnerFailed: result.sandbox.runnerFailed } : {}
|
|
264
|
+
} } : {}
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
/** Canonical background-handle properties shared by the pwsh output union. */
|
|
268
|
+
const BACKGROUND_OUTPUT_PROPERTIES = {
|
|
269
|
+
kind: {
|
|
270
|
+
type: "string",
|
|
271
|
+
required: true,
|
|
272
|
+
const: "background"
|
|
273
|
+
},
|
|
274
|
+
jobId: {
|
|
275
|
+
type: "string",
|
|
276
|
+
required: true
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
function apply(ctx, config = {}) {
|
|
280
|
+
const backgroundEnabled = config.enableRunInBackground ?? true;
|
|
281
|
+
const defaultMode = ctx.shell.sandboxMode;
|
|
282
|
+
const escalationModes = defaultMode === void 0 ? [] : ESCALATION_TARGETS;
|
|
283
|
+
const sandboxPolicy = defaultMode === void 0 ? void 0 : ctx.get("sandboxPolicy");
|
|
284
|
+
if (defaultMode !== void 0 && sandboxPolicy === void 0) throw new Error("tool-pwsh: the mounted bash executor confines but ctx.sandboxPolicy is missing");
|
|
285
|
+
/** Resolve the complete standing policy for this call when a confining executor is mounted. */
|
|
286
|
+
const resolveSandboxPolicy = (exec) => sandboxPolicy?.resolve(exec.agent === void 0 ? {} : { session: exec.agent.session });
|
|
287
|
+
/**
|
|
288
|
+
* Resolve a sandbox-escalation request through `ctx.approval` BEFORE
|
|
289
|
+
* anything executes, delegating the shared fail-closed sequence (strict
|
|
290
|
+
* widening, channel resolution, outcome mapping) to
|
|
291
|
+
* {@link approveEscalation}. This tool contributes only the composition
|
|
292
|
+
* guard (the fields are unadvertised without a sandboxing executor, yet
|
|
293
|
+
* schema validation checks advertised keys only, so an unadvertised
|
|
294
|
+
* `sandbox_permissions` still reaches execute) and the approval
|
|
295
|
+
* ingredients. The shared policy resolver is required whenever the
|
|
296
|
+
* executor advertises confinement, so a split composition fails at
|
|
297
|
+
* tool-plugin load.
|
|
298
|
+
*/
|
|
299
|
+
const approvePwshEscalation = (mode, justification, exec, standingPolicy) => {
|
|
300
|
+
if (escalationModes.length === 0) throw new Error("sandbox_permissions is not available in this composition (no sandboxing executor to escalate)");
|
|
301
|
+
const effectiveMode = standingPolicy.mode;
|
|
302
|
+
return approveEscalation({
|
|
303
|
+
requestedMode: mode,
|
|
304
|
+
justification,
|
|
305
|
+
effectiveMode,
|
|
306
|
+
subject: "command"
|
|
307
|
+
}, {
|
|
308
|
+
approver: ctx.get("approval"),
|
|
309
|
+
agent: exec.agent,
|
|
310
|
+
callId: exec.callId,
|
|
311
|
+
toolName: "pwsh",
|
|
312
|
+
signal: exec.signal
|
|
313
|
+
});
|
|
314
|
+
};
|
|
315
|
+
ctx.systemPrompt.section({
|
|
316
|
+
name: "tool:pwsh",
|
|
317
|
+
order: ctx.systemPrompt.getSectionOrder("TOOL_PWSH"),
|
|
318
|
+
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."
|
|
319
|
+
});
|
|
320
|
+
ctx.tools.register(defineTool({
|
|
321
|
+
name: "pwsh",
|
|
322
|
+
description: pwshDescription(backgroundEnabled, escalationModes),
|
|
323
|
+
parameters: {
|
|
324
|
+
command: {
|
|
325
|
+
type: "string",
|
|
326
|
+
required: true,
|
|
327
|
+
description: "The PowerShell command to execute."
|
|
328
|
+
},
|
|
329
|
+
description: {
|
|
330
|
+
type: "string",
|
|
331
|
+
required: true,
|
|
332
|
+
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\"."
|
|
333
|
+
},
|
|
334
|
+
timeoutMs: {
|
|
335
|
+
type: "number",
|
|
336
|
+
description: "Timeout in milliseconds. The executor applies its configured default and cap, and kills the command on expiry."
|
|
337
|
+
},
|
|
338
|
+
workdir: {
|
|
339
|
+
type: "string",
|
|
340
|
+
description: "Working directory for this command. Defaults to the session workspace; a relative path is resolved against it."
|
|
341
|
+
},
|
|
342
|
+
...backgroundEnabled ? { run_in_background: {
|
|
343
|
+
type: "boolean",
|
|
344
|
+
description: "Run in the background and return a job id immediately (collect with job_output, stop with job_kill). No timeout applies."
|
|
345
|
+
} } : {},
|
|
346
|
+
...escalationModes.length > 0 ? {
|
|
347
|
+
sandbox_permissions: {
|
|
348
|
+
type: "string",
|
|
349
|
+
enum: [...escalationModes],
|
|
350
|
+
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."
|
|
351
|
+
},
|
|
352
|
+
justification: {
|
|
353
|
+
type: "string",
|
|
354
|
+
description: "Required with sandbox_permissions: one sentence for the user explaining why this exact command needs the wider access. Use the language of the user’s current request."
|
|
355
|
+
}
|
|
356
|
+
} : {}
|
|
357
|
+
},
|
|
358
|
+
output: {
|
|
359
|
+
schema: { oneOf: [{
|
|
360
|
+
type: "object",
|
|
361
|
+
additionalProperties: false,
|
|
362
|
+
properties: BACKGROUND_OUTPUT_PROPERTIES
|
|
363
|
+
}, {
|
|
364
|
+
type: "object",
|
|
365
|
+
additionalProperties: false,
|
|
366
|
+
properties: {
|
|
367
|
+
kind: {
|
|
368
|
+
type: "string",
|
|
369
|
+
required: true,
|
|
370
|
+
const: "foreground"
|
|
371
|
+
},
|
|
372
|
+
exitCode: {
|
|
373
|
+
required: true,
|
|
374
|
+
oneOf: [{ type: "integer" }, { type: "null" }]
|
|
375
|
+
},
|
|
376
|
+
signal: {
|
|
377
|
+
required: true,
|
|
378
|
+
oneOf: [{ type: "string" }, { type: "null" }]
|
|
379
|
+
},
|
|
380
|
+
timedOut: {
|
|
381
|
+
type: "boolean",
|
|
382
|
+
required: true
|
|
383
|
+
},
|
|
384
|
+
aborted: {
|
|
385
|
+
type: "boolean",
|
|
386
|
+
required: true
|
|
387
|
+
},
|
|
388
|
+
timeoutMs: {
|
|
389
|
+
type: "number",
|
|
390
|
+
required: true
|
|
391
|
+
},
|
|
392
|
+
stdout: {
|
|
393
|
+
type: "object",
|
|
394
|
+
additionalProperties: false,
|
|
395
|
+
required: true,
|
|
396
|
+
properties: {
|
|
397
|
+
text: {
|
|
398
|
+
type: "string",
|
|
399
|
+
required: true
|
|
400
|
+
},
|
|
401
|
+
truncated: {
|
|
402
|
+
type: "boolean",
|
|
403
|
+
required: true
|
|
404
|
+
},
|
|
405
|
+
spillPath: { type: "string" }
|
|
406
|
+
}
|
|
407
|
+
},
|
|
408
|
+
stderr: {
|
|
409
|
+
type: "object",
|
|
410
|
+
additionalProperties: false,
|
|
411
|
+
required: true,
|
|
412
|
+
properties: {
|
|
413
|
+
text: {
|
|
414
|
+
type: "string",
|
|
415
|
+
required: true
|
|
416
|
+
},
|
|
417
|
+
truncated: {
|
|
418
|
+
type: "boolean",
|
|
419
|
+
required: true
|
|
420
|
+
},
|
|
421
|
+
spillPath: { type: "string" }
|
|
422
|
+
}
|
|
423
|
+
},
|
|
424
|
+
sandbox: {
|
|
425
|
+
type: "object",
|
|
426
|
+
additionalProperties: false,
|
|
427
|
+
properties: {
|
|
428
|
+
mode: {
|
|
429
|
+
type: "string",
|
|
430
|
+
required: true
|
|
431
|
+
},
|
|
432
|
+
denied: {
|
|
433
|
+
type: "boolean",
|
|
434
|
+
required: true
|
|
435
|
+
},
|
|
436
|
+
enforcement: { type: "string" },
|
|
437
|
+
runnerFailed: { type: "boolean" }
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}] },
|
|
442
|
+
render: (_args, value) => [{
|
|
443
|
+
type: "text",
|
|
444
|
+
text: value.kind === "background" ? `started background job ${value.jobId}` : renderPwshResult(value, escalationModes)
|
|
445
|
+
}]
|
|
446
|
+
},
|
|
447
|
+
async execute(args, exec) {
|
|
448
|
+
validatePwshArgs(args);
|
|
449
|
+
const standingPolicy = resolveSandboxPolicy(exec);
|
|
450
|
+
const approvedMode = args.sandbox_permissions !== void 0 && args.justification !== void 0 ? await approvePwshEscalation(args.sandbox_permissions, args.justification, exec, standingPolicy) : void 0;
|
|
451
|
+
const policy = approvedMode === void 0 ? standingPolicy : {
|
|
452
|
+
...standingPolicy,
|
|
453
|
+
mode: approvedMode
|
|
454
|
+
};
|
|
455
|
+
const workdir = resolveWorkdir(args.workdir, exec);
|
|
456
|
+
const request = {
|
|
457
|
+
command: args.command,
|
|
458
|
+
...workdir !== void 0 ? { workdir } : {},
|
|
459
|
+
...args.timeoutMs !== void 0 ? { timeoutMs: args.timeoutMs } : {},
|
|
460
|
+
qilinEnv: ctx.shellEnv.collect(exec),
|
|
461
|
+
...policy !== void 0 ? { sandboxPolicy: policy } : {}
|
|
462
|
+
};
|
|
463
|
+
if (args.run_in_background === true) {
|
|
464
|
+
if (!backgroundEnabled) throw new Error("run_in_background is disabled for this deployment (enableRunInBackground: false)");
|
|
465
|
+
const jobs = ctx.get("jobs");
|
|
466
|
+
if (jobs === void 0) throw new Error("background jobs unavailable: load @qilin-agent/jobs and @qilin-agent/tool-jobs");
|
|
467
|
+
if (exec.signal.aborted) {
|
|
468
|
+
const error = new HarnessError("tool call aborted", TOOL_ABORTED);
|
|
469
|
+
error.name = "AbortError";
|
|
470
|
+
throw error;
|
|
471
|
+
}
|
|
472
|
+
let proc;
|
|
473
|
+
return {
|
|
474
|
+
kind: "background",
|
|
475
|
+
jobId: jobs.start({
|
|
476
|
+
kind: "pwsh",
|
|
477
|
+
label: args.command,
|
|
478
|
+
...exec.agent ? { owner: exec.agent.id } : {},
|
|
479
|
+
output: processSources(() => proc, escalationModes),
|
|
480
|
+
run: () => processJob(async (signal) => {
|
|
481
|
+
proc = await ctx.shell.start(ctx.shell.resolve({
|
|
482
|
+
...request,
|
|
483
|
+
signal
|
|
484
|
+
}));
|
|
485
|
+
return proc;
|
|
486
|
+
}, (started) => processOutcome(started, escalationModes))
|
|
487
|
+
})
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
const result = await ctx.shell.run(ctx.shell.resolve({
|
|
491
|
+
...request,
|
|
492
|
+
signal: exec.signal
|
|
493
|
+
}));
|
|
494
|
+
if (result.aborted) {
|
|
495
|
+
const error = new HarnessError("tool call aborted", TOOL_ABORTED);
|
|
496
|
+
error.name = "AbortError";
|
|
497
|
+
throw error;
|
|
498
|
+
}
|
|
499
|
+
return canonicalPwshResult(result);
|
|
500
|
+
},
|
|
501
|
+
presentCall: (args) => {
|
|
502
|
+
if (args.run_in_background === true) return {
|
|
503
|
+
card: "generic",
|
|
504
|
+
title: args.command,
|
|
505
|
+
kind: "execute",
|
|
506
|
+
rawInput: args.command,
|
|
507
|
+
content: [{
|
|
508
|
+
type: "text",
|
|
509
|
+
text: args.description
|
|
510
|
+
}]
|
|
511
|
+
};
|
|
512
|
+
return {
|
|
513
|
+
card: "terminal",
|
|
514
|
+
title: args.command,
|
|
515
|
+
description: args.description,
|
|
516
|
+
...args.workdir !== void 0 ? { cwd: args.workdir } : {}
|
|
517
|
+
};
|
|
518
|
+
},
|
|
519
|
+
presentResult: (args, result) => {
|
|
520
|
+
const block = result.content.length === 1 ? result.content[0] : void 0;
|
|
521
|
+
if (block === void 0 || block.type !== "text") return void 0;
|
|
522
|
+
const raw = block.text;
|
|
523
|
+
if (typeof args === "object" && args !== null && args.run_in_background === true || result.isError) return {
|
|
524
|
+
card: "generic",
|
|
525
|
+
content: [{
|
|
526
|
+
type: "text",
|
|
527
|
+
text: `\`\`\`console\n${raw.replace(/\n+$/, "")}\n\`\`\``
|
|
528
|
+
}]
|
|
529
|
+
};
|
|
530
|
+
const { body, ...exit } = parseExitStatus(raw);
|
|
531
|
+
return {
|
|
532
|
+
card: "terminal",
|
|
533
|
+
output: body,
|
|
534
|
+
...exit
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
}));
|
|
538
|
+
}
|
|
539
|
+
//#endregion
|
|
540
|
+
export { Config, apply, inject, name };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic-job adaptation for pwsh process handles: the terminal outcome the
|
|
3
|
+
* registry records and the pull source it pumps into the job's output ring.
|
|
4
|
+
*
|
|
5
|
+
* @module @qilin-agent/tool-pwsh/background
|
|
6
|
+
*/
|
|
7
|
+
import type { SandboxMode } from '@qilin-agent/sandbox';
|
|
8
|
+
import type { ShellProcess } from '@qilin-agent/shell';
|
|
9
|
+
import type { JobHooks, JobOutcome, JobOutputSource } from '@qilin-agent/jobs';
|
|
10
|
+
/**
|
|
11
|
+
* Map a settled background process onto the generic job-outcome vocabulary:
|
|
12
|
+
* `killed` stays `killed` (detail: the signal when one is known), everything
|
|
13
|
+
* else is `completed` with the exit code as detail. A nonzero command exit is
|
|
14
|
+
* reported, not failed, exactly like the foreground rendering. Sandbox facts
|
|
15
|
+
* join the detail, since a job's terminal reason is the one line every
|
|
16
|
+
* reader — the model's status line, the roster row — shows.
|
|
17
|
+
* @param proc - the settled process handle.
|
|
18
|
+
* @param escalationModes - escalation targets advertised by this composition.
|
|
19
|
+
* @returns the outcome for the `ctx.jobs` registration.
|
|
20
|
+
*/
|
|
21
|
+
export declare function processOutcome(proc: ShellProcess, escalationModes?: readonly SandboxMode[]): JobOutcome;
|
|
22
|
+
/**
|
|
23
|
+
* The process's captured output as a registry pull source. It binds lazily
|
|
24
|
+
* because the process is spawned inside the starter, after the registry
|
|
25
|
+
* admitted the job; a read before the spawn yields nothing.
|
|
26
|
+
*
|
|
27
|
+
* The shell handle's own reader is consuming — each call hands over what
|
|
28
|
+
* arrived since the previous one — so this source counts the bytes it
|
|
29
|
+
* delivered to keep the registry's cursor monotonic, exactly as the terminal
|
|
30
|
+
* tool adapts its own consuming send reader. Truncation is reported in-band by
|
|
31
|
+
* the read rendering, which names the spill files, so the source has no
|
|
32
|
+
* separate spill path to advertise.
|
|
33
|
+
* @param proc - the started process, once the starter has spawned it.
|
|
34
|
+
* @param escalationModes - escalation targets advertised by this composition.
|
|
35
|
+
* @returns the single source over the process's captured streams.
|
|
36
|
+
*/
|
|
37
|
+
export declare function processSources(proc: () => ShellProcess | undefined, escalationModes?: readonly SandboxMode[]): JobOutputSource[];
|
|
38
|
+
/**
|
|
39
|
+
* Adapt asynchronous shell preparation after job admission without exposing a partial process.
|
|
40
|
+
* @param start - starts the process with job-owned cancellation.
|
|
41
|
+
* @param outcome - projects the settled process into the job outcome.
|
|
42
|
+
* @returns synchronous job hooks whose completion includes preparation and process settlement.
|
|
43
|
+
*/
|
|
44
|
+
export declare function processJob(start: (signal: AbortSignal) => Promise<ShellProcess>, outcome: (process: ShellProcess) => JobOutcome): JobHooks;
|
|
45
|
+
//# 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
|
+
* `@qilin-agent/pwsh-local`) backs `ctx.shell`; the tool contract is
|
|
5
|
+
* PowerShell-dialect: native `C:\...` paths and `$env:NAME` variables.
|
|
6
|
+
*
|
|
7
|
+
* Behavior mirrors `qilin-tool-bash` call-for-call: foreground and
|
|
8
|
+
* `run_in_background` execution (background handles register with the
|
|
9
|
+
* generic `ctx.jobs` runtime), the managed `QILIN_*` 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 `@qilin-agent/shell`.
|
|
18
|
+
*
|
|
19
|
+
* @module @qilin-agent/tool-pwsh
|
|
20
|
+
*/
|
|
21
|
+
import type { Context } from '@qilin-agent/kylin';
|
|
22
|
+
import z from '@qilin-agent/schemastery';
|
|
23
|
+
declare module '@qilin-agent/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,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model-facing result rendering for the pwsh tool — the PowerShell twin of
|
|
3
|
+
* `qilin-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 @qilin-agent/tool-pwsh/render
|
|
11
|
+
*/
|
|
12
|
+
import type { ShellProcessRead, ShellSandboxInfo, CollectedOutput } from '@qilin-agent/shell';
|
|
13
|
+
import type { SandboxMode } from '@qilin-agent/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
CHANGED
|
@@ -1,6 +1,69 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qilin-agent/tool-pwsh",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"description": "Model-facing pwsh tool over the bash executor seam",
|
|
4
|
+
"version": "3.1.3",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/kkutysllb/QiLin.git",
|
|
11
|
+
"directory": "packages/shell/tool-pwsh"
|
|
12
|
+
},
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": "^22.19.0 || >=24.0.0"
|
|
15
|
+
},
|
|
16
|
+
"type": "module",
|
|
17
|
+
"main": "lib/index.js",
|
|
18
|
+
"types": "lib/types/index.d.ts",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./lib/types/index.d.ts",
|
|
22
|
+
"default": "./lib/index.js"
|
|
23
|
+
},
|
|
24
|
+
"./src/*": "./src/*",
|
|
25
|
+
"./package.json": "./package.json"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"lib/index.js",
|
|
29
|
+
"lib/types/**/*.d.ts"
|
|
30
|
+
],
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"@qilin-agent/kylin": "~4.0.3",
|
|
34
|
+
"@qilin-agent/agent": "3.1.3",
|
|
35
|
+
"@qilin-agent/jobs": "3.1.3",
|
|
36
|
+
"@qilin-agent/llm": "3.1.3",
|
|
37
|
+
"@qilin-agent/sandbox": "3.1.3",
|
|
38
|
+
"@qilin-agent/sandbox-policy": "3.1.3",
|
|
39
|
+
"@qilin-agent/shell": "3.1.3",
|
|
40
|
+
"@qilin-agent/shell-env": "3.1.3",
|
|
41
|
+
"@qilin-agent/system-prompt": "3.1.3",
|
|
42
|
+
"@qilin-agent/tools": "3.1.3",
|
|
43
|
+
"@qilin-agent/user-approval": "3.1.3"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@qilin-agent/schemastery": "~3.18.3"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@qilin-agent/kylin": "~4.0.3",
|
|
50
|
+
"@qilin-agent/agent-loop": "3.1.3",
|
|
51
|
+
"@qilin-agent/agent": "3.1.3",
|
|
52
|
+
"@qilin-agent/jobs": "3.1.3",
|
|
53
|
+
"@qilin-agent/app-boot": "3.1.3",
|
|
54
|
+
"@qilin-agent/jobs-local": "3.1.3",
|
|
55
|
+
"@qilin-agent/llm": "3.1.3",
|
|
56
|
+
"@qilin-agent/loader-smoke": "3.1.3",
|
|
57
|
+
"@qilin-agent/sandbox": "3.1.3",
|
|
58
|
+
"@qilin-agent/pwsh-local": "3.1.3",
|
|
59
|
+
"@qilin-agent/sandbox-policy": "3.1.3",
|
|
60
|
+
"@qilin-agent/shell-env": "3.1.3",
|
|
61
|
+
"@qilin-agent/system-prompt": "3.1.3",
|
|
62
|
+
"@qilin-agent/subprocess-local": "3.1.3",
|
|
63
|
+
"@qilin-agent/tool-jobs": "3.1.3",
|
|
64
|
+
"@qilin-agent/shell": "3.1.3",
|
|
65
|
+
"@qilin-agent/session-projection": "3.1.3",
|
|
66
|
+
"@qilin-agent/tools": "3.1.3",
|
|
67
|
+
"@qilin-agent/user-approval": "3.1.3"
|
|
68
|
+
}
|
|
6
69
|
}
|