larkin 0.2.78 → 0.2.80
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/dist/app/local-control.mjs +47 -1
- package/dist/platform/secure-metadata.cjs +67 -0
- package/dist/platform/secure-metadata.mjs +65 -0
- package/dist/runtime/pi-official-auth.mjs +12 -1
- package/dist/runtime/pi-provider-config.cjs +7 -1
- package/dist/runtime/pi-provider-config.mjs +7 -1
- package/dist/setup/bot-register.mjs +8 -0
- package/package.json +1 -1
|
@@ -7,7 +7,10 @@ import path from "node:path";
|
|
|
7
7
|
import { readProcessState } from "../platform/process-state.mjs";
|
|
8
8
|
import { currentProcessMetadata } from "../platform/process-inspect.cjs";
|
|
9
9
|
import { processCommandToken } from "./internal-command.mjs";
|
|
10
|
-
import { isWindows, notGroupOrWorldAccessible } from "../platform/secure-metadata.mjs";
|
|
10
|
+
import { isWindows, notGroupOrWorldAccessible, secureWindowsDirectoryAcl } from "../platform/secure-metadata.mjs";
|
|
11
|
+
// Windows 上 socket 无法 lstat(EACCES),用全数字占位 binding 使 authority 校验通过;
|
|
12
|
+
// 该平台的安全边界是 socket root 目录的 ACL,而非 inode 身份比对。
|
|
13
|
+
const WINDOWS_SOCKET_BINDING = { device: "0", inode: "0", owner: "0", changeTimeNs: "0" };
|
|
11
14
|
const AGENT_ID = /^cli_[A-Za-z0-9]+$/;
|
|
12
15
|
const OPERATION_ID = /^[A-Za-z0-9_-]{8,128}$/;
|
|
13
16
|
const AUTHORIZATION = /^[A-Za-z0-9_-]{43,128}$/;
|
|
@@ -32,6 +35,12 @@ function controlSocketRoot(larkinHome) {
|
|
|
32
35
|
throw new Error("无法生成满足 Unix socket 长度限制的 control root");
|
|
33
36
|
}
|
|
34
37
|
function assertSecureSocketDirectory(root) {
|
|
38
|
+
if (isWindows) {
|
|
39
|
+
// Windows:socket 本身无法 lstat(EACCES),安全边界是目录 ACL。
|
|
40
|
+
// 每次启动重新收紧为「当前用户 + SYSTEM」并回读校验。
|
|
41
|
+
secureWindowsDirectoryAcl(root, { label: "control socket root" });
|
|
42
|
+
return root;
|
|
43
|
+
}
|
|
35
44
|
const stat = fs.lstatSync(root);
|
|
36
45
|
if (!stat.isDirectory() || stat.isSymbolicLink()
|
|
37
46
|
|| (typeof process.getuid === "function" && stat.uid !== process.getuid())
|
|
@@ -223,6 +232,15 @@ function assertSupervisorAuthority(larkinHome, expectedToken) {
|
|
|
223
232
|
return authority;
|
|
224
233
|
}
|
|
225
234
|
function prepareSocket(socket) {
|
|
235
|
+
if (isWindows) {
|
|
236
|
+
// Windows:socket 路径无法 lstat(EACCES);尽力清除陈旧占用,失败即忽略,
|
|
237
|
+
// 由 listen 的独占语义兜底(见 listenPrivate 的 win32 分支)。
|
|
238
|
+
try {
|
|
239
|
+
fs.unlinkSync(socket);
|
|
240
|
+
}
|
|
241
|
+
catch { /* ignore */ }
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
226
244
|
try {
|
|
227
245
|
const stat = fs.lstatSync(socket);
|
|
228
246
|
if (!stat.isSocket() || stat.isSymbolicLink()
|
|
@@ -246,6 +264,14 @@ function cleanupSocketRoot(root) {
|
|
|
246
264
|
}
|
|
247
265
|
}
|
|
248
266
|
async function closePrivateServer(server, socket, identity) {
|
|
267
|
+
if (isWindows) {
|
|
268
|
+
// Windows:socket 无 lstat 身份可比对,直接关服并清理目录(残留由 ENOTEMPTY 忽略)。
|
|
269
|
+
if (server.listening) {
|
|
270
|
+
await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
|
|
271
|
+
}
|
|
272
|
+
cleanupSocketRoot(path.dirname(socket));
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
249
275
|
let shield = null;
|
|
250
276
|
let closeError;
|
|
251
277
|
try {
|
|
@@ -301,6 +327,26 @@ async function closePrivateServer(server, socket, identity) {
|
|
|
301
327
|
throw closeError;
|
|
302
328
|
}
|
|
303
329
|
async function listenPrivate(server, socket) {
|
|
330
|
+
if (isWindows) {
|
|
331
|
+
// Windows:bun 的 unix socket 可正常 listen/connect/chmod/close,但 lstatSync(socket)
|
|
332
|
+
// 抛 EACCES(socket 不是普通文件系统节点)。因此跳过 lstat 身份比对,安全边界
|
|
333
|
+
// 由 assertSecureSocketDirectory 收紧的目录 ACL 承担。
|
|
334
|
+
try {
|
|
335
|
+
await new Promise((resolve, reject) => {
|
|
336
|
+
server.once("error", reject);
|
|
337
|
+
server.listen(socket, () => { server.off("error", reject); resolve(); });
|
|
338
|
+
});
|
|
339
|
+
try {
|
|
340
|
+
fs.chmodSync(socket, 0o600);
|
|
341
|
+
}
|
|
342
|
+
catch { /* Windows chmod best-effort */ }
|
|
343
|
+
return WINDOWS_SOCKET_BINDING;
|
|
344
|
+
}
|
|
345
|
+
catch (error) {
|
|
346
|
+
await closePrivateServer(server, socket, WINDOWS_SOCKET_BINDING);
|
|
347
|
+
throw error;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
304
350
|
let identity = null;
|
|
305
351
|
try {
|
|
306
352
|
await new Promise((resolve, reject) => {
|
|
@@ -8,8 +8,12 @@ exports.exactMode = exactMode;
|
|
|
8
8
|
exports.notGroupOrWorldAccessible = notGroupOrWorldAccessible;
|
|
9
9
|
exports.fsyncDirectory = fsyncDirectory;
|
|
10
10
|
exports.fsyncDirectoryOf = fsyncDirectoryOf;
|
|
11
|
+
exports.runWindowsDirectoryAcl = runWindowsDirectoryAcl;
|
|
12
|
+
exports.secureWindowsDirectoryAcl = secureWindowsDirectoryAcl;
|
|
11
13
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
14
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
12
15
|
const node_path_1 = __importDefault(require("node:path"));
|
|
16
|
+
const node_child_process_1 = require("node:child_process");
|
|
13
17
|
/**
|
|
14
18
|
* Cross-platform secure-metadata helpers. POSIX platforms enforce ownership and
|
|
15
19
|
* mode bits (fail-closed). Windows has no POSIX mode bits, so mode-bit assertions
|
|
@@ -46,3 +50,66 @@ function fsyncDirectory(directory) {
|
|
|
46
50
|
function fsyncDirectoryOf(file) {
|
|
47
51
|
fsyncDirectory(node_path_1.default.dirname(file));
|
|
48
52
|
}
|
|
53
|
+
const SYSTEM_SID = "*S-1-5-18";
|
|
54
|
+
const ACE_LINE = /^\s*(.+?)\s*:(\((?:[A-Za-z]+)\))+\s*$/;
|
|
55
|
+
function allowedAclPrincipal(principal, username) {
|
|
56
|
+
const candidate = principal.trim().toLocaleLowerCase("en-US");
|
|
57
|
+
const user = username.toLocaleLowerCase("en-US");
|
|
58
|
+
if (!candidate)
|
|
59
|
+
return false;
|
|
60
|
+
if (candidate === user)
|
|
61
|
+
return true;
|
|
62
|
+
const suffix = candidate.split("\\").at(-1) ?? candidate;
|
|
63
|
+
if (suffix === user)
|
|
64
|
+
return true;
|
|
65
|
+
if (candidate === "*s-1-5-18" || candidate === "system" || candidate === "nt authority\\system")
|
|
66
|
+
return true;
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* 执行 Windows ACL 收紧序列(无平台守卫,供测试直接驱动与 win32 分支调用):
|
|
71
|
+
* 1) /inheritance:r 去除继承;2) 只授予当前用户 + SYSTEM 完全控制;
|
|
72
|
+
* 3) 显式移除 Users/Authenticated Users/Everyone;4) 回读 ACL 校验,
|
|
73
|
+
* 任何残留第三方 ACE 一律 fail-closed。icacls 为 Windows 内置工具。
|
|
74
|
+
*/
|
|
75
|
+
function runWindowsDirectoryAcl(directory, options = {}) {
|
|
76
|
+
const label = options.label ?? directory;
|
|
77
|
+
const username = String(options.username ?? node_os_1.default.userInfo().username ?? "").trim();
|
|
78
|
+
if (!username)
|
|
79
|
+
throw new Error(`Windows ACL 收紧失败(${label}):无法取得当前用户名`);
|
|
80
|
+
const run = (args) => {
|
|
81
|
+
const result = (options.spawn ?? node_child_process_1.spawnSync)("icacls", [directory, ...args], {
|
|
82
|
+
encoding: "utf8", windowsHide: true, maxBuffer: 1024 * 1024,
|
|
83
|
+
});
|
|
84
|
+
if (result.error || result.status !== 0) {
|
|
85
|
+
throw new Error(`Windows ACL 收紧失败(${label}):icacls ${args[0] ?? "查询"} 退出 ${result.status ?? "无"}${result.error ? `(${result.error.message})` : ""}`);
|
|
86
|
+
}
|
|
87
|
+
return String(result.stdout || "");
|
|
88
|
+
};
|
|
89
|
+
run(["/inheritance:r"]);
|
|
90
|
+
run(["/grant:r", `${username}:(OI)(CI)F`, "/grant:r", `${SYSTEM_SID}:(OI)(CI)F`]);
|
|
91
|
+
run(["/remove:g", "Users", "/remove:g", "Authenticated Users", "/remove:g", "Everyone"]);
|
|
92
|
+
const listing = run([]);
|
|
93
|
+
// icacls 首行是「<路径> <首条 ACE>」同行,其余 ACE 缩进另起一行;先剥掉已知目录前缀再解析。
|
|
94
|
+
const pathPrefix = directory.toLocaleLowerCase("en-US");
|
|
95
|
+
const offenders = listing.split(/\r?\n/).map((line) => {
|
|
96
|
+
let body = line.trimStart();
|
|
97
|
+
if (body.toLocaleLowerCase("en-US").startsWith(`${pathPrefix} `))
|
|
98
|
+
body = body.slice(directory.length + 1).trimStart();
|
|
99
|
+
return body;
|
|
100
|
+
}).filter((line) => ACE_LINE.test(line))
|
|
101
|
+
.map((line) => (line.match(ACE_LINE) ?? ["", ""])[1])
|
|
102
|
+
.filter((principal) => !allowedAclPrincipal(principal, username));
|
|
103
|
+
if (offenders.length > 0) {
|
|
104
|
+
throw new Error(`Windows ACL 校验失败(${label}):存在未授权访问者 ${offenders.join(", ")}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Windows 没有 POSIX 权限位(mode 恒为 0o666、getuid 为 undefined):
|
|
109
|
+
* 用 runWindowsDirectoryAcl 收紧目录 ACL 并回读校验。非 win32 平台 no-op。
|
|
110
|
+
*/
|
|
111
|
+
function secureWindowsDirectoryAcl(directory, options = {}) {
|
|
112
|
+
if (!exports.isWindows)
|
|
113
|
+
return;
|
|
114
|
+
runWindowsDirectoryAcl(directory, options);
|
|
115
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
2
3
|
import path from "node:path";
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
3
5
|
/**
|
|
4
6
|
* Cross-platform secure-metadata helpers. POSIX platforms enforce ownership and
|
|
5
7
|
* mode bits (fail-closed). Windows has no POSIX mode bits, so mode-bit assertions
|
|
@@ -36,3 +38,66 @@ export function fsyncDirectory(directory) {
|
|
|
36
38
|
export function fsyncDirectoryOf(file) {
|
|
37
39
|
fsyncDirectory(path.dirname(file));
|
|
38
40
|
}
|
|
41
|
+
const SYSTEM_SID = "*S-1-5-18";
|
|
42
|
+
const ACE_LINE = /^\s*(.+?)\s*:(\((?:[A-Za-z]+)\))+\s*$/;
|
|
43
|
+
function allowedAclPrincipal(principal, username) {
|
|
44
|
+
const candidate = principal.trim().toLocaleLowerCase("en-US");
|
|
45
|
+
const user = username.toLocaleLowerCase("en-US");
|
|
46
|
+
if (!candidate)
|
|
47
|
+
return false;
|
|
48
|
+
if (candidate === user)
|
|
49
|
+
return true;
|
|
50
|
+
const suffix = candidate.split("\\").at(-1) ?? candidate;
|
|
51
|
+
if (suffix === user)
|
|
52
|
+
return true;
|
|
53
|
+
if (candidate === "*s-1-5-18" || candidate === "system" || candidate === "nt authority\\system")
|
|
54
|
+
return true;
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* 执行 Windows ACL 收紧序列(无平台守卫,供测试直接驱动与 win32 分支调用):
|
|
59
|
+
* 1) /inheritance:r 去除继承;2) 只授予当前用户 + SYSTEM 完全控制;
|
|
60
|
+
* 3) 显式移除 Users/Authenticated Users/Everyone;4) 回读 ACL 校验,
|
|
61
|
+
* 任何残留第三方 ACE 一律 fail-closed。icacls 为 Windows 内置工具。
|
|
62
|
+
*/
|
|
63
|
+
export function runWindowsDirectoryAcl(directory, options = {}) {
|
|
64
|
+
const label = options.label ?? directory;
|
|
65
|
+
const username = String(options.username ?? os.userInfo().username ?? "").trim();
|
|
66
|
+
if (!username)
|
|
67
|
+
throw new Error(`Windows ACL 收紧失败(${label}):无法取得当前用户名`);
|
|
68
|
+
const run = (args) => {
|
|
69
|
+
const result = (options.spawn ?? spawnSync)("icacls", [directory, ...args], {
|
|
70
|
+
encoding: "utf8", windowsHide: true, maxBuffer: 1024 * 1024,
|
|
71
|
+
});
|
|
72
|
+
if (result.error || result.status !== 0) {
|
|
73
|
+
throw new Error(`Windows ACL 收紧失败(${label}):icacls ${args[0] ?? "查询"} 退出 ${result.status ?? "无"}${result.error ? `(${result.error.message})` : ""}`);
|
|
74
|
+
}
|
|
75
|
+
return String(result.stdout || "");
|
|
76
|
+
};
|
|
77
|
+
run(["/inheritance:r"]);
|
|
78
|
+
run(["/grant:r", `${username}:(OI)(CI)F`, "/grant:r", `${SYSTEM_SID}:(OI)(CI)F`]);
|
|
79
|
+
run(["/remove:g", "Users", "/remove:g", "Authenticated Users", "/remove:g", "Everyone"]);
|
|
80
|
+
const listing = run([]);
|
|
81
|
+
// icacls 首行是「<路径> <首条 ACE>」同行,其余 ACE 缩进另起一行;先剥掉已知目录前缀再解析。
|
|
82
|
+
const pathPrefix = directory.toLocaleLowerCase("en-US");
|
|
83
|
+
const offenders = listing.split(/\r?\n/).map((line) => {
|
|
84
|
+
let body = line.trimStart();
|
|
85
|
+
if (body.toLocaleLowerCase("en-US").startsWith(`${pathPrefix} `))
|
|
86
|
+
body = body.slice(directory.length + 1).trimStart();
|
|
87
|
+
return body;
|
|
88
|
+
}).filter((line) => ACE_LINE.test(line))
|
|
89
|
+
.map((line) => (line.match(ACE_LINE) ?? ["", ""])[1])
|
|
90
|
+
.filter((principal) => !allowedAclPrincipal(principal, username));
|
|
91
|
+
if (offenders.length > 0) {
|
|
92
|
+
throw new Error(`Windows ACL 校验失败(${label}):存在未授权访问者 ${offenders.join(", ")}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Windows 没有 POSIX 权限位(mode 恒为 0o666、getuid 为 undefined):
|
|
97
|
+
* 用 runWindowsDirectoryAcl 收紧目录 ACL 并回读校验。非 win32 平台 no-op。
|
|
98
|
+
*/
|
|
99
|
+
export function secureWindowsDirectoryAcl(directory, options = {}) {
|
|
100
|
+
if (!isWindows)
|
|
101
|
+
return;
|
|
102
|
+
runWindowsDirectoryAcl(directory, options);
|
|
103
|
+
}
|
|
@@ -2,6 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import crypto from "node:crypto";
|
|
4
4
|
import { pathToFileURL } from "node:url";
|
|
5
|
+
import { isWindows, secureWindowsDirectoryAcl } from "../platform/secure-metadata.mjs";
|
|
5
6
|
// proper-lockfile does not publish TypeScript declarations.
|
|
6
7
|
// @ts-expect-error bundled CommonJS dependency
|
|
7
8
|
import properLockfile from "proper-lockfile";
|
|
@@ -22,6 +23,11 @@ function ensureLockDirectory(lockPath) {
|
|
|
22
23
|
const directory = path.dirname(lockPath);
|
|
23
24
|
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
24
25
|
fs.chmodSync(directory, 0o700);
|
|
26
|
+
if (isWindows) {
|
|
27
|
+
// Windows 无 POSIX 权限位(mode 恒 0o666):改用 icacls 收紧 ACL 并回读校验。
|
|
28
|
+
secureWindowsDirectoryAcl(directory, { label: "Pi credential lock 目录" });
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
25
31
|
const stat = fs.lstatSync(directory);
|
|
26
32
|
if (!stat.isDirectory() || stat.isSymbolicLink()
|
|
27
33
|
|| (typeof process.getuid === "function" && stat.uid !== process.getuid())
|
|
@@ -85,6 +91,11 @@ function ensurePrivateDirectory(directory) {
|
|
|
85
91
|
fs.chmodSync(path.dirname(directory), 0o700);
|
|
86
92
|
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
87
93
|
fs.chmodSync(directory, 0o700);
|
|
94
|
+
if (isWindows) {
|
|
95
|
+
// Windows 无 POSIX 权限位:icacls 收紧为「当前用户 + SYSTEM」并回读校验。
|
|
96
|
+
secureWindowsDirectoryAcl(directory, { label: "Pi official credential 目录" });
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
88
99
|
const stat = fs.lstatSync(directory);
|
|
89
100
|
if (!stat.isDirectory() || stat.isSymbolicLink()
|
|
90
101
|
|| (typeof process.getuid === "function" && stat.uid !== process.getuid())
|
|
@@ -97,7 +108,7 @@ function safeRead(file) {
|
|
|
97
108
|
const stat = fs.lstatSync(file);
|
|
98
109
|
if (!stat.isFile() || stat.isSymbolicLink()
|
|
99
110
|
|| (typeof process.getuid === "function" && stat.uid !== process.getuid())
|
|
100
|
-
|| (stat.mode & 0o777) !== 0o600)
|
|
111
|
+
|| (!isWindows && (stat.mode & 0o777) !== 0o600))
|
|
101
112
|
throw new Error(`unsafe Pi auth file: ${path.basename(file)}`);
|
|
102
113
|
return fs.readFileSync(file);
|
|
103
114
|
}
|
|
@@ -14,6 +14,7 @@ exports.stageBuiltinPiProvider = stageBuiltinPiProvider;
|
|
|
14
14
|
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
15
15
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
16
16
|
const node_path_1 = __importDefault(require("node:path"));
|
|
17
|
+
const secure_metadata_js_1 = require("../platform/secure-metadata.cjs");
|
|
17
18
|
exports.BUNDLED_PI_VERSION = "0.83.0";
|
|
18
19
|
// These values mirror the catalog shipped by the pinned official Pi distribution.
|
|
19
20
|
// Keeping the user-facing defaults here makes endpoint changes reviewable in Larkin.
|
|
@@ -95,6 +96,11 @@ function piAgentDirectory(configDir, agentId) {
|
|
|
95
96
|
return node_path_1.default.join(node_path_1.default.resolve(configDir), "providers", "pi", agentId);
|
|
96
97
|
}
|
|
97
98
|
function assertPrivateDirectory(directory) {
|
|
99
|
+
if (secure_metadata_js_1.isWindows) {
|
|
100
|
+
// Windows 无 POSIX 权限位:icacls 收紧为「当前用户 + SYSTEM」并回读校验。
|
|
101
|
+
(0, secure_metadata_js_1.secureWindowsDirectoryAcl)(directory, { label: "Pi provider 凭证目录" });
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
98
104
|
const stat = node_fs_1.default.lstatSync(directory);
|
|
99
105
|
if (!stat.isDirectory() || stat.isSymbolicLink()
|
|
100
106
|
|| (typeof process.getuid === "function" && stat.uid !== process.getuid())
|
|
@@ -129,7 +135,7 @@ function readSnapshot(file) {
|
|
|
129
135
|
const stat = node_fs_1.default.lstatSync(file);
|
|
130
136
|
if (!stat.isFile() || stat.isSymbolicLink()
|
|
131
137
|
|| (typeof process.getuid === "function" && stat.uid !== process.getuid())
|
|
132
|
-
|| (stat.mode & 0o777) !== 0o600)
|
|
138
|
+
|| (!secure_metadata_js_1.isWindows && (stat.mode & 0o777) !== 0o600))
|
|
133
139
|
throw new Error(`Pi provider 文件不安全: ${node_path_1.default.basename(file)}`);
|
|
134
140
|
return node_fs_1.default.readFileSync(file);
|
|
135
141
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { isWindows, secureWindowsDirectoryAcl } from "../platform/secure-metadata.mjs";
|
|
4
5
|
export const BUNDLED_PI_VERSION = "0.83.0";
|
|
5
6
|
// These values mirror the catalog shipped by the pinned official Pi distribution.
|
|
6
7
|
// Keeping the user-facing defaults here makes endpoint changes reviewable in Larkin.
|
|
@@ -82,6 +83,11 @@ export function piAgentDirectory(configDir, agentId) {
|
|
|
82
83
|
return path.join(path.resolve(configDir), "providers", "pi", agentId);
|
|
83
84
|
}
|
|
84
85
|
function assertPrivateDirectory(directory) {
|
|
86
|
+
if (isWindows) {
|
|
87
|
+
// Windows 无 POSIX 权限位:icacls 收紧为「当前用户 + SYSTEM」并回读校验。
|
|
88
|
+
secureWindowsDirectoryAcl(directory, { label: "Pi provider 凭证目录" });
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
85
91
|
const stat = fs.lstatSync(directory);
|
|
86
92
|
if (!stat.isDirectory() || stat.isSymbolicLink()
|
|
87
93
|
|| (typeof process.getuid === "function" && stat.uid !== process.getuid())
|
|
@@ -116,7 +122,7 @@ function readSnapshot(file) {
|
|
|
116
122
|
const stat = fs.lstatSync(file);
|
|
117
123
|
if (!stat.isFile() || stat.isSymbolicLink()
|
|
118
124
|
|| (typeof process.getuid === "function" && stat.uid !== process.getuid())
|
|
119
|
-
|| (stat.mode & 0o777) !== 0o600)
|
|
125
|
+
|| (!isWindows && (stat.mode & 0o777) !== 0o600))
|
|
120
126
|
throw new Error(`Pi provider 文件不安全: ${path.basename(file)}`);
|
|
121
127
|
return fs.readFileSync(file);
|
|
122
128
|
}
|
|
@@ -5,6 +5,7 @@ import fs from "node:fs";
|
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
7
7
|
import { registerApp as channelRegisterApp } from "@larksuite/channel";
|
|
8
|
+
import { isWindows, secureWindowsDirectoryAcl } from "../platform/secure-metadata.mjs";
|
|
8
9
|
import { internalCommandSpec } from "../app/internal-command.mjs";
|
|
9
10
|
import * as larkinConfig from "../platform/config.mjs";
|
|
10
11
|
import { createAgentStateStore } from "../agent/agent-state-store.mjs";
|
|
@@ -230,6 +231,13 @@ function ensureSecureBotsDir() {
|
|
|
230
231
|
if (error?.code !== "EEXIST")
|
|
231
232
|
throw error;
|
|
232
233
|
}
|
|
234
|
+
if (isWindows) {
|
|
235
|
+
// Windows 无 POSIX 权限位(mode 恒 0o666):改用 icacls 收紧 CFG_DIR 与 bots 目录
|
|
236
|
+
// 的 ACL 为「当前用户 + SYSTEM」并回读校验,fail-closed。
|
|
237
|
+
secureWindowsDirectoryAcl(CFG_DIR, { label: "Larkin 配置目录" });
|
|
238
|
+
secureWindowsDirectoryAcl(directory, { label: "bots 凭证目录" });
|
|
239
|
+
return directory;
|
|
240
|
+
}
|
|
233
241
|
const stat = fs.lstatSync(directory);
|
|
234
242
|
if (!stat.isDirectory() || stat.isSymbolicLink()
|
|
235
243
|
|| (typeof process.getuid === "function" && stat.uid !== process.getuid())
|