@oyasmi/pipiclaw 0.5.7 → 0.5.8
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/README.md +13 -0
- package/dist/sandbox.js +63 -5
- package/dist/shared/shell-escape.d.ts +7 -0
- package/dist/shared/shell-escape.js +11 -0
- package/dist/tools/edit.js +2 -2
- package/dist/tools/read.js +6 -6
- package/dist/tools/write-content.js +5 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -129,6 +129,13 @@ Pipiclaw 当前已经内置一轮工具层安全增强:
|
|
|
129
129
|
- 直接使用 Anthropic 默认模型
|
|
130
130
|
- 或在 `models.json` 中配置自定义模型提供方(provider)
|
|
131
131
|
|
|
132
|
+
Windows 补充说明:
|
|
133
|
+
|
|
134
|
+
- Pipiclaw 的工具执行层默认按 POSIX shell 语义工作
|
|
135
|
+
- 在 Windows host 模式下,建议安装 Git Bash,并确保 `bash` 可在 PATH 中找到
|
|
136
|
+
- 如果 `bash` 不在 PATH 中,可以设置 `PIPICLAW_SHELL` 指向具体可执行文件,例如 `C:\Program Files\Git\bin\bash.exe`
|
|
137
|
+
- 如果你不想依赖本机 shell 环境,推荐直接使用 Docker sandbox
|
|
138
|
+
|
|
132
139
|
#### 2. 安装(Install)
|
|
133
140
|
|
|
134
141
|
```bash
|
|
@@ -169,6 +176,12 @@ export PIPICLAW_HOME=/your/custom/pipiclaw-home
|
|
|
169
176
|
|
|
170
177
|
设置后,`channel.json`、`auth.json`、`models.json`、`settings.json` 和整个 `workspace/` 都会改为从这个目录读取和写入。
|
|
171
178
|
|
|
179
|
+
如果你在 Windows host 模式下运行,并且 `bash` 不在 PATH 中,也可以一并设置:
|
|
180
|
+
|
|
181
|
+
```powershell
|
|
182
|
+
$env:PIPICLAW_SHELL = "C:\Program Files\Git\bin\bash.exe"
|
|
183
|
+
```
|
|
184
|
+
|
|
172
185
|
如果 `channel.json` 仍然是初始化模板,程序会提示你补全配置后再启动。这是正常行为。
|
|
173
186
|
|
|
174
187
|
#### 4. 创建钉钉应用(Create a DingTalk App)
|
package/dist/sandbox.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { spawn } from "child_process";
|
|
1
|
+
import { spawn, spawnSync } from "child_process";
|
|
2
2
|
import { shellEscape } from "./shared/shell-escape.js";
|
|
3
3
|
export function parseSandboxArg(value) {
|
|
4
4
|
if (value === "host") {
|
|
@@ -17,6 +17,15 @@ export function parseSandboxArg(value) {
|
|
|
17
17
|
}
|
|
18
18
|
export async function validateSandbox(config) {
|
|
19
19
|
if (config.type === "host") {
|
|
20
|
+
if (process.platform === "win32") {
|
|
21
|
+
try {
|
|
22
|
+
resolveWindowsHostShell();
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
20
29
|
return;
|
|
21
30
|
}
|
|
22
31
|
// Check if Docker is available
|
|
@@ -45,7 +54,10 @@ export async function validateSandbox(config) {
|
|
|
45
54
|
}
|
|
46
55
|
function execSimple(cmd, args) {
|
|
47
56
|
return new Promise((resolve, reject) => {
|
|
48
|
-
const child = spawn(cmd, args, {
|
|
57
|
+
const child = spawn(cmd, args, {
|
|
58
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
59
|
+
windowsHide: true,
|
|
60
|
+
});
|
|
49
61
|
let stdout = "";
|
|
50
62
|
let stderr = "";
|
|
51
63
|
child.stdout?.on("data", (d) => {
|
|
@@ -62,6 +74,51 @@ function execSimple(cmd, args) {
|
|
|
62
74
|
});
|
|
63
75
|
});
|
|
64
76
|
}
|
|
77
|
+
const WINDOWS_POSIX_SHELL_CANDIDATES = [
|
|
78
|
+
"bash",
|
|
79
|
+
"sh",
|
|
80
|
+
"C:\\Program Files\\Git\\bin\\bash.exe",
|
|
81
|
+
"C:\\Program Files\\Git\\usr\\bin\\bash.exe",
|
|
82
|
+
"C:\\Program Files (x86)\\Git\\bin\\bash.exe",
|
|
83
|
+
"C:\\Program Files (x86)\\Git\\usr\\bin\\bash.exe",
|
|
84
|
+
];
|
|
85
|
+
let cachedWindowsHostShell;
|
|
86
|
+
function looksLikeUnixShellPath(shell) {
|
|
87
|
+
return shell.endsWith("/bash") || shell.endsWith("/sh");
|
|
88
|
+
}
|
|
89
|
+
function isUsablePosixShell(command) {
|
|
90
|
+
const result = spawnSync(command, ["-lc", "printf pipiclaw"], {
|
|
91
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
92
|
+
encoding: "utf-8",
|
|
93
|
+
windowsHide: true,
|
|
94
|
+
});
|
|
95
|
+
return !result.error && result.status === 0 && result.stdout === "pipiclaw";
|
|
96
|
+
}
|
|
97
|
+
function resolveWindowsHostShell() {
|
|
98
|
+
if (cachedWindowsHostShell) {
|
|
99
|
+
return cachedWindowsHostShell;
|
|
100
|
+
}
|
|
101
|
+
const configuredShell = process.env.PIPICLAW_SHELL?.trim();
|
|
102
|
+
const inheritedShell = process.env.SHELL?.trim();
|
|
103
|
+
const shellCandidates = [
|
|
104
|
+
configuredShell,
|
|
105
|
+
inheritedShell && looksLikeUnixShellPath(inheritedShell) ? inheritedShell.split("/").pop() : undefined,
|
|
106
|
+
...WINDOWS_POSIX_SHELL_CANDIDATES,
|
|
107
|
+
].filter((value) => Boolean(value));
|
|
108
|
+
for (const command of shellCandidates) {
|
|
109
|
+
if (isUsablePosixShell(command)) {
|
|
110
|
+
cachedWindowsHostShell = { command, args: ["-lc"] };
|
|
111
|
+
return cachedWindowsHostShell;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
throw new Error("Windows host sandbox requires a POSIX shell. Install Git Bash and ensure `bash` is on PATH, set `PIPICLAW_SHELL`, or use the Docker sandbox.");
|
|
115
|
+
}
|
|
116
|
+
function resolveHostShell() {
|
|
117
|
+
if (process.platform === "win32") {
|
|
118
|
+
return resolveWindowsHostShell();
|
|
119
|
+
}
|
|
120
|
+
return { command: "sh", args: ["-c"] };
|
|
121
|
+
}
|
|
65
122
|
/**
|
|
66
123
|
* Create an executor that runs commands either on host or in Docker container
|
|
67
124
|
*/
|
|
@@ -74,13 +131,13 @@ export function createExecutor(config) {
|
|
|
74
131
|
class HostExecutor {
|
|
75
132
|
async exec(command, options) {
|
|
76
133
|
return new Promise((resolve, reject) => {
|
|
77
|
-
const shell =
|
|
78
|
-
const shellArgs = process.platform === "win32" ? ["/c"] : ["-c"];
|
|
134
|
+
const shell = resolveHostShell();
|
|
79
135
|
const child = (() => {
|
|
80
136
|
try {
|
|
81
|
-
return spawn(shell, [...
|
|
137
|
+
return spawn(shell.command, [...shell.args, command], {
|
|
82
138
|
detached: true,
|
|
83
139
|
stdio: ["pipe", "pipe", "pipe"],
|
|
140
|
+
windowsHide: true,
|
|
84
141
|
});
|
|
85
142
|
}
|
|
86
143
|
catch (err) {
|
|
@@ -199,6 +256,7 @@ function killProcessTree(pid) {
|
|
|
199
256
|
spawn("taskkill", ["/F", "/T", "/PID", String(pid)], {
|
|
200
257
|
stdio: "ignore",
|
|
201
258
|
detached: true,
|
|
259
|
+
windowsHide: true,
|
|
202
260
|
});
|
|
203
261
|
}
|
|
204
262
|
catch {
|
|
@@ -3,3 +3,10 @@
|
|
|
3
3
|
* Wraps in single quotes and escapes internal single quotes.
|
|
4
4
|
*/
|
|
5
5
|
export declare function shellEscape(s: string): string;
|
|
6
|
+
/**
|
|
7
|
+
* Normalize filesystem paths for POSIX-style shells.
|
|
8
|
+
* On Windows we convert backslashes to forward slashes so Git Bash/MSYS tools
|
|
9
|
+
* can consume the path consistently.
|
|
10
|
+
*/
|
|
11
|
+
export declare function toShellPath(path: string): string;
|
|
12
|
+
export declare function shellEscapePath(path: string): string;
|
|
@@ -5,3 +5,14 @@
|
|
|
5
5
|
export function shellEscape(s) {
|
|
6
6
|
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
7
7
|
}
|
|
8
|
+
/**
|
|
9
|
+
* Normalize filesystem paths for POSIX-style shells.
|
|
10
|
+
* On Windows we convert backslashes to forward slashes so Git Bash/MSYS tools
|
|
11
|
+
* can consume the path consistently.
|
|
12
|
+
*/
|
|
13
|
+
export function toShellPath(path) {
|
|
14
|
+
return process.platform === "win32" ? path.replace(/\\/g, "/") : path;
|
|
15
|
+
}
|
|
16
|
+
export function shellEscapePath(path) {
|
|
17
|
+
return shellEscape(toShellPath(path));
|
|
18
|
+
}
|
package/dist/tools/edit.js
CHANGED
|
@@ -3,7 +3,7 @@ import * as Diff from "diff";
|
|
|
3
3
|
import { DEFAULT_SECURITY_CONFIG } from "../security/config.js";
|
|
4
4
|
import { logSecurityEvent } from "../security/logger.js";
|
|
5
5
|
import { guardPath } from "../security/path-guard.js";
|
|
6
|
-
import {
|
|
6
|
+
import { shellEscapePath } from "../shared/shell-escape.js";
|
|
7
7
|
import { writeContent } from "./write-content.js";
|
|
8
8
|
/**
|
|
9
9
|
* Generate a unified diff string with line numbers and context
|
|
@@ -123,7 +123,7 @@ export function createEditTool(executor, options = {}) {
|
|
|
123
123
|
}
|
|
124
124
|
}
|
|
125
125
|
// Read the file
|
|
126
|
-
const readResult = await executor.exec(`cat ${
|
|
126
|
+
const readResult = await executor.exec(`cat ${shellEscapePath(path)}`, { signal });
|
|
127
127
|
if (readResult.code !== 0) {
|
|
128
128
|
throw new Error(readResult.stderr || `File not found: ${path}`);
|
|
129
129
|
}
|
package/dist/tools/read.js
CHANGED
|
@@ -3,7 +3,7 @@ import { extname } from "path";
|
|
|
3
3
|
import { DEFAULT_SECURITY_CONFIG } from "../security/config.js";
|
|
4
4
|
import { logSecurityEvent } from "../security/logger.js";
|
|
5
5
|
import { guardPath } from "../security/path-guard.js";
|
|
6
|
-
import {
|
|
6
|
+
import { shellEscapePath, toShellPath } from "../shared/shell-escape.js";
|
|
7
7
|
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, truncateHead } from "./truncate.js";
|
|
8
8
|
/**
|
|
9
9
|
* Map of file extensions to MIME types for common image formats
|
|
@@ -70,7 +70,7 @@ export function createReadTool(executor, options = {}) {
|
|
|
70
70
|
const mimeType = isImageFile(path);
|
|
71
71
|
if (mimeType) {
|
|
72
72
|
// Read as image (binary) - use base64
|
|
73
|
-
const result = await executor.exec(`base64 < ${
|
|
73
|
+
const result = await executor.exec(`base64 < ${shellEscapePath(path)}`, { signal });
|
|
74
74
|
if (result.code !== 0) {
|
|
75
75
|
throw new Error(result.stderr || `Failed to read file: ${path}`);
|
|
76
76
|
}
|
|
@@ -84,7 +84,7 @@ export function createReadTool(executor, options = {}) {
|
|
|
84
84
|
};
|
|
85
85
|
}
|
|
86
86
|
// Get total line count first
|
|
87
|
-
const countResult = await executor.exec(`wc -l < ${
|
|
87
|
+
const countResult = await executor.exec(`wc -l < ${shellEscapePath(path)}`, { signal });
|
|
88
88
|
if (countResult.code !== 0) {
|
|
89
89
|
throw new Error(countResult.stderr || `Failed to read file: ${path}`);
|
|
90
90
|
}
|
|
@@ -99,10 +99,10 @@ export function createReadTool(executor, options = {}) {
|
|
|
99
99
|
// Read content with offset
|
|
100
100
|
let cmd;
|
|
101
101
|
if (startLine === 1) {
|
|
102
|
-
cmd = `cat ${
|
|
102
|
+
cmd = `cat ${shellEscapePath(path)}`;
|
|
103
103
|
}
|
|
104
104
|
else {
|
|
105
|
-
cmd = `tail -n +${startLine} ${
|
|
105
|
+
cmd = `tail -n +${startLine} ${shellEscapePath(path)}`;
|
|
106
106
|
}
|
|
107
107
|
const result = await executor.exec(cmd, { signal });
|
|
108
108
|
if (result.code !== 0) {
|
|
@@ -124,7 +124,7 @@ export function createReadTool(executor, options = {}) {
|
|
|
124
124
|
if (truncation.firstLineExceedsLimit) {
|
|
125
125
|
// First line at offset exceeds 50KB - tell model to use bash
|
|
126
126
|
const firstLineSize = formatSize(Buffer.byteLength(selectedContent.split("\n")[0], "utf-8"));
|
|
127
|
-
outputText = `[Line ${startLineDisplay} is ${firstLineSize}, exceeds ${formatSize(DEFAULT_MAX_BYTES)} limit. Use bash: sed -n '${startLineDisplay}p' ${path} | head -c ${DEFAULT_MAX_BYTES}]`;
|
|
127
|
+
outputText = `[Line ${startLineDisplay} is ${firstLineSize}, exceeds ${formatSize(DEFAULT_MAX_BYTES)} limit. Use bash: sed -n '${startLineDisplay}p' ${toShellPath(path)} | head -c ${DEFAULT_MAX_BYTES}]`;
|
|
128
128
|
details = { truncation };
|
|
129
129
|
}
|
|
130
130
|
else if (truncation.truncated) {
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import { dirname } from "node:path";
|
|
1
2
|
import { DEFAULT_SECURITY_CONFIG } from "../security/config.js";
|
|
2
3
|
import { logSecurityEvent } from "../security/logger.js";
|
|
3
4
|
import { guardPath } from "../security/path-guard.js";
|
|
4
|
-
import {
|
|
5
|
+
import { shellEscapePath } from "../shared/shell-escape.js";
|
|
5
6
|
function getDir(path) {
|
|
6
|
-
return
|
|
7
|
+
return dirname(path);
|
|
7
8
|
}
|
|
8
9
|
function ensureSuccess(result, path) {
|
|
9
10
|
if (result.code !== 0) {
|
|
@@ -41,8 +42,8 @@ export async function writeContent(executor, path, content, signal, options) {
|
|
|
41
42
|
throw new Error(lines.join("\n"));
|
|
42
43
|
}
|
|
43
44
|
}
|
|
44
|
-
const dirPrefix = createParentDir ? `mkdir -p ${
|
|
45
|
-
const result = await executor.exec(`${dirPrefix}cat > ${
|
|
45
|
+
const dirPrefix = createParentDir ? `mkdir -p ${shellEscapePath(getDir(path))} && ` : "";
|
|
46
|
+
const result = await executor.exec(`${dirPrefix}cat > ${shellEscapePath(path)}`, {
|
|
46
47
|
signal,
|
|
47
48
|
stdin: content,
|
|
48
49
|
});
|
package/package.json
CHANGED