@nowcrew/daemon 0.6.88-beta.28 → 0.6.88-beta.29
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 +7 -0
- package/dist/agent-ability/materializer.js +21 -10
- package/dist/agent-environment-contract.js +55 -0
- package/dist/agent-environment-control.js +24 -0
- package/dist/agent-environment-execution.js +98 -0
- package/dist/agent-environments.js +275 -0
- package/dist/config.js +7 -0
- package/dist/execution-runner.js +2 -0
- package/dist/list-models.js +12 -12
- package/dist/local-executor.js +20 -48
- package/dist/local-runtime-environment.js +50 -0
- package/dist/machine-environment-cache.js +147 -0
- package/dist/machine-environment-contract.js +10 -0
- package/dist/machine-environment-control.js +27 -0
- package/dist/machine-environment-policy.js +75 -0
- package/dist/machine-environment-process.js +110 -0
- package/dist/machine-info.js +20 -15
- package/dist/machine-runtime-environment.js +189 -0
- package/dist/machine-shell-environment-capture.js +135 -0
- package/dist/machine-windows-environment-capture.js +215 -0
- package/dist/machine-windows-environment-script.js +158 -0
- package/dist/main.js +0 -0
- package/dist/message-file-preview.js +239 -0
- package/dist/provider-env.js +26 -12
- package/dist/runner.js +14 -0
- package/dist/runtime-environment-merge.js +16 -0
- package/dist/runtime-health.js +3 -1
- package/dist/runtime-path.js +22 -12
- package/dist/runtime-probe.js +2 -2
- package/dist/runtimes/claude-models.js +20 -9
- package/dist/runtimes/deepseek-harness.js +17 -12
- package/dist/runtimes/hermes-models.js +2 -1
- package/dist/runtimes/kimi-acp-runner.js +7 -6
- package/dist/runtimes/opencode.js +2 -2
- package/dist/serve.js +50 -105
- package/dist/token.js +0 -1
- package/dist/workspace-control-handler.js +27 -0
- package/dist/workspace-fs.js +66 -15
- package/package.json +9 -10
package/dist/workspace-fs.js
CHANGED
|
@@ -3,21 +3,31 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 安全铁律:
|
|
5
5
|
* - 所有路径都 resolve 后必须仍落在 agent 根内 (防 `..` / 符号链接逃逸)。
|
|
6
|
-
* -
|
|
6
|
+
* - 列表隐藏点文件;内部环境快照始终不可列出或读取,包括链接别名。
|
|
7
7
|
* - 只回文本文件,二进制拒绝;单文件大小上限,超出截断。
|
|
8
8
|
* - 仅 list / read,无写删。
|
|
9
9
|
*/
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
10
|
+
import { constants } from "node:fs";
|
|
11
|
+
import { realpath, readdir, lstat, open, stat } from "node:fs/promises";
|
|
12
|
+
import { join, relative, resolve, sep } from "node:path";
|
|
12
13
|
export class FsError extends Error {
|
|
13
14
|
}
|
|
14
15
|
const MAX_FILE_BYTES = 256 * 1024; // 单文件查看上限 256KB
|
|
15
16
|
const HIDDEN = new Set([".crew", ".git", ".slock"]);
|
|
16
17
|
const isHidden = (name) => name.startsWith(".") || HIDDEN.has(name);
|
|
18
|
+
/** 该命名空间包含完整机器环境,只允许本地执行 harness 使用。 */
|
|
19
|
+
export const isInternalEnvironmentPath = (path) => path.split(/[\\/]/u)
|
|
20
|
+
.some((part) => part.toLowerCase().startsWith(".environment-"));
|
|
21
|
+
const assertViewablePath = (path) => {
|
|
22
|
+
if (isInternalEnvironmentPath(path))
|
|
23
|
+
throw new FsError("file_not_viewable");
|
|
24
|
+
};
|
|
17
25
|
/** 把相对路径安全解析到 root 内的绝对路径;越界抛错。realpath 兜底符号链接逃逸。 */
|
|
18
26
|
async function safeResolve(root, rel) {
|
|
27
|
+
assertViewablePath(rel);
|
|
19
28
|
const rootResolved = await realpath(root);
|
|
20
29
|
const target = resolve(rootResolved, rel.replace(/^\/+/, ""));
|
|
30
|
+
assertViewablePath(target);
|
|
21
31
|
const withSep = rootResolved.endsWith(sep) ? rootResolved : rootResolved + sep;
|
|
22
32
|
if (target !== rootResolved && !target.startsWith(withSep)) {
|
|
23
33
|
throw new FsError("path escapes workspace");
|
|
@@ -33,6 +43,7 @@ async function safeResolve(root, rel) {
|
|
|
33
43
|
if (real !== rootResolved && !real.startsWith(withSep)) {
|
|
34
44
|
throw new FsError("path escapes workspace (symlink)");
|
|
35
45
|
}
|
|
46
|
+
assertViewablePath(real);
|
|
36
47
|
return real;
|
|
37
48
|
}
|
|
38
49
|
/** 列出某目录 (相对 root)。隐藏点文件/内部目录;目录在前、按名排序。 */
|
|
@@ -51,13 +62,15 @@ export async function listWorkspace(root, rel) {
|
|
|
51
62
|
for (const name of names) {
|
|
52
63
|
if (isHidden(name))
|
|
53
64
|
continue;
|
|
54
|
-
const full = join(dir, name);
|
|
65
|
+
const full = await safeResolve(root, relative(rootResolved, join(dir, name))).catch(() => null);
|
|
66
|
+
if (full === null)
|
|
67
|
+
continue;
|
|
55
68
|
const s = await stat(full).catch(() => null);
|
|
56
69
|
if (!s)
|
|
57
70
|
continue;
|
|
58
71
|
if (s.isDirectory())
|
|
59
72
|
entries.push({ name, type: "dir" });
|
|
60
|
-
else if (s.isFile())
|
|
73
|
+
else if (s.isFile() && s.nlink === 1)
|
|
61
74
|
entries.push({ name, type: "file", size: s.size });
|
|
62
75
|
}
|
|
63
76
|
entries.sort((a, b) => a.type !== b.type ? (a.type === "dir" ? -1 : 1) : a.name.localeCompare(b.name));
|
|
@@ -66,15 +79,53 @@ export async function listWorkspace(root, rel) {
|
|
|
66
79
|
/** 读取某文本文件 (相对 root)。二进制/超大拒绝或截断。 */
|
|
67
80
|
export async function readWorkspaceFile(root, rel) {
|
|
68
81
|
const file = await safeResolve(root, rel);
|
|
69
|
-
const
|
|
70
|
-
if (!
|
|
82
|
+
const before = await lstat(file, { bigint: true }).catch(() => null);
|
|
83
|
+
if (!before?.isFile())
|
|
71
84
|
throw new FsError("not a file");
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
85
|
+
// 真实路径不能辨认硬链接的私有来源,因此拒绝多链接文件。
|
|
86
|
+
if (before.nlink !== 1n)
|
|
87
|
+
throw new FsError("file_not_viewable");
|
|
88
|
+
// macOS 原子拒绝父目录链接;Linux 在打开后额外核对句柄的真实路径。
|
|
89
|
+
const noFollow = process.platform === "darwin" ? 0x20000000 : (constants.O_NOFOLLOW ?? 0);
|
|
90
|
+
const handle = await open(file, constants.O_RDONLY | (constants.O_NONBLOCK ?? 0) | noFollow)
|
|
91
|
+
.catch(() => { throw new FsError("file_not_viewable"); });
|
|
92
|
+
try {
|
|
93
|
+
const opened = await handle.stat({ bigint: true });
|
|
94
|
+
if (!opened.isFile() || opened.nlink !== 1n || opened.dev !== before.dev || opened.ino !== before.ino)
|
|
95
|
+
throw new FsError("file_changed");
|
|
96
|
+
if (process.platform === "linux") {
|
|
97
|
+
const actual = await realpath(`/proc/self/fd/${handle.fd}`);
|
|
98
|
+
assertViewablePath(actual);
|
|
99
|
+
if (actual !== file)
|
|
100
|
+
throw new FsError("file_changed");
|
|
101
|
+
}
|
|
102
|
+
if (await safeResolve(root, rel) !== file)
|
|
103
|
+
throw new FsError("file_changed");
|
|
104
|
+
const buffer = Buffer.alloc(MAX_FILE_BYTES);
|
|
105
|
+
let length = 0;
|
|
106
|
+
while (length < buffer.length) {
|
|
107
|
+
const { bytesRead } = await handle.read(buffer, length, buffer.length - length, length);
|
|
108
|
+
if (bytesRead === 0)
|
|
109
|
+
break;
|
|
110
|
+
length += bytesRead;
|
|
111
|
+
}
|
|
112
|
+
const after = await handle.stat({ bigint: true });
|
|
113
|
+
const current = await lstat(file, { bigint: true });
|
|
114
|
+
if (await safeResolve(root, rel) !== file || !current.isFile() || current.nlink !== 1n
|
|
115
|
+
|| current.dev !== opened.dev || current.ino !== opened.ino || after.nlink !== 1n
|
|
116
|
+
|| after.size !== opened.size || after.mtimeNs !== opened.mtimeNs || after.ctimeNs !== opened.ctimeNs)
|
|
117
|
+
throw new FsError("file_changed");
|
|
118
|
+
const bytes = buffer.subarray(0, length);
|
|
119
|
+
if (bytes.subarray(0, Math.min(length, 8192)).includes(0))
|
|
120
|
+
throw new FsError("binary file not viewable");
|
|
121
|
+
return { path: rel.replace(/^\/+/, ""), content: bytes.toString("utf8"), truncated: opened.size > BigInt(length), size: Number(opened.size) };
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
if (error instanceof FsError)
|
|
125
|
+
throw error;
|
|
126
|
+
throw new FsError("file_changed");
|
|
127
|
+
}
|
|
128
|
+
finally {
|
|
129
|
+
await handle.close();
|
|
130
|
+
}
|
|
80
131
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nowcrew/daemon",
|
|
3
|
-
"version": "0.6.88-beta.
|
|
3
|
+
"version": "0.6.88-beta.29",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "crew daemon — 运行在用户机器:拉起/管理 agent 进程,注入 crew CLI,归一化 runtime 事件",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -16,16 +16,9 @@
|
|
|
16
16
|
"publishConfig": {
|
|
17
17
|
"access": "public"
|
|
18
18
|
},
|
|
19
|
-
"scripts": {
|
|
20
|
-
"daemon": "pnpm --filter @nowcrew/cli build && tsx src/main.ts",
|
|
21
|
-
"build": "tsc -p tsconfig.json",
|
|
22
|
-
"prepublishOnly": "pnpm build && node ../scripts/daemon-release-artifact.mjs --strict-registry",
|
|
23
|
-
"test": "vitest run",
|
|
24
|
-
"typecheck": "tsc --noEmit"
|
|
25
|
-
},
|
|
26
19
|
"dependencies": {
|
|
27
20
|
"@agentclientprotocol/sdk": "1.2.1",
|
|
28
|
-
"@nowcrew/cli": "0.4.
|
|
21
|
+
"@nowcrew/cli": "0.4.17",
|
|
29
22
|
"cross-spawn": "^7.0.6",
|
|
30
23
|
"semver": "7.8.5",
|
|
31
24
|
"ws": "^8",
|
|
@@ -43,5 +36,11 @@
|
|
|
43
36
|
"tsx": "^4.19.0",
|
|
44
37
|
"typescript": "^5.6.0",
|
|
45
38
|
"vitest": "^2.1.0"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"daemon": "pnpm --filter @nowcrew/cli build && tsx src/main.ts",
|
|
42
|
+
"build": "tsc -p tsconfig.json",
|
|
43
|
+
"test": "vitest run",
|
|
44
|
+
"typecheck": "tsc --noEmit"
|
|
46
45
|
}
|
|
47
|
-
}
|
|
46
|
+
}
|