larkin 0.2.70
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/CONTRIBUTING.md +48 -0
- package/LICENSE +201 -0
- package/README.md +122 -0
- package/SECURITY.md +14 -0
- package/artifacts/release/THIRD_PARTY_NOTICES.txt +5488 -0
- package/dist/agent/agent-cli-capabilities.cjs +43 -0
- package/dist/agent/agent-cli-capabilities.mjs +35 -0
- package/dist/agent/agent-state-store.cjs +961 -0
- package/dist/agent/agent-state-store.mjs +923 -0
- package/dist/agent/agent-transport.cjs +201 -0
- package/dist/agent/agent-transport.mjs +194 -0
- package/dist/agent/config-cli-contract.cjs +19 -0
- package/dist/agent/config-cli-contract.mjs +16 -0
- package/dist/agent/context-prompt.cjs +159 -0
- package/dist/agent/context-prompt.mjs +155 -0
- package/dist/agent/freshness-gate.mjs +24 -0
- package/dist/agent/history-projection.cjs +72 -0
- package/dist/agent/history-projection.mjs +65 -0
- package/dist/agent/host-reminder-orchestrator.cjs +289 -0
- package/dist/agent/host-reminder-orchestrator.mjs +252 -0
- package/dist/agent/inbox-projection.cjs +92 -0
- package/dist/agent/inbox-projection.mjs +85 -0
- package/dist/agent/interaction-state-machine.cjs +710 -0
- package/dist/agent/interaction-state-machine.mjs +705 -0
- package/dist/agent/reminder-routes.cjs +192 -0
- package/dist/agent/reminder-routes.mjs +156 -0
- package/dist/agent/reminder-store.cjs +220 -0
- package/dist/agent/reminder-store.mjs +205 -0
- package/dist/agent/transport-business-context.cjs +486 -0
- package/dist/agent/transport-business-context.mjs +446 -0
- package/dist/agent/transport-shell.cjs +45 -0
- package/dist/agent/transport-shell.mjs +39 -0
- package/dist/app/agent-cli.mjs +596 -0
- package/dist/app/agent-config.mjs +633 -0
- package/dist/app/agent-lark-cli-workspace.cjs +95 -0
- package/dist/app/agent-lark-cli-workspace.mjs +85 -0
- package/dist/app/agent-readiness.cjs +42 -0
- package/dist/app/agent-readiness.mjs +38 -0
- package/dist/app/binary-entry.mjs +86 -0
- package/dist/app/cli.mjs +176 -0
- package/dist/app/dashboard.mjs +205 -0
- package/dist/app/internal-command.cjs +57 -0
- package/dist/app/internal-command.mjs +48 -0
- package/dist/app/lark-channel-secret.mjs +22 -0
- package/dist/app/lark-cli.mjs +887 -0
- package/dist/app/lark.mjs +45 -0
- package/dist/app/local-control.mjs +728 -0
- package/dist/app/official-lark-cli.cjs +138 -0
- package/dist/app/official-lark-cli.mjs +128 -0
- package/dist/app/pi-auth-cli.mjs +67 -0
- package/dist/app/run.mjs +364 -0
- package/dist/app/runtime-agent-config.mjs +416 -0
- package/dist/app/runtime-model-directory.mjs +78 -0
- package/dist/app/runtime-process.mjs +113 -0
- package/dist/app/session-cli.mjs +83 -0
- package/dist/app/setup-dashboard.mjs +26 -0
- package/dist/app/setup.mjs +166 -0
- package/dist/app/telemetry.mjs +83 -0
- package/dist/dashboard/dashboard-assets.mjs +21 -0
- package/dist/dashboard/dashboard-avatar.mjs +128 -0
- package/dist/dashboard/dashboard-config-controller.mjs +525 -0
- package/dist/dashboard/dashboard-lifecycle.cjs +97 -0
- package/dist/dashboard/dashboard-lifecycle.mjs +88 -0
- package/dist/dashboard/dashboard-template.mjs +29 -0
- package/dist/dashboard/dashboard-view-model.mjs +556 -0
- package/dist/dashboard/dashboard-workspace.mjs +104 -0
- package/dist/dashboard/web/assets/dashboard.css +3 -0
- package/dist/dashboard/web/assets/dashboard.js +49 -0
- package/dist/feishu/channel-transport.cjs +151 -0
- package/dist/feishu/channel-transport.mjs +148 -0
- package/dist/feishu/document-comment.cjs +120 -0
- package/dist/feishu/document-comment.mjs +109 -0
- package/dist/feishu/host-business-state.cjs +477 -0
- package/dist/feishu/host-business-state.mjs +462 -0
- package/dist/feishu/host-channel-business.cjs +161 -0
- package/dist/feishu/host-channel-business.mjs +157 -0
- package/dist/feishu/host-processing-eye.cjs +185 -0
- package/dist/feishu/host-processing-eye.mjs +181 -0
- package/dist/feishu/host-shell.cjs +1425 -0
- package/dist/feishu/host-shell.mjs +1385 -0
- package/dist/feishu/im-freshness-adapter.mjs +89 -0
- package/dist/feishu/interaction-orchestrator.cjs +279 -0
- package/dist/feishu/interaction-orchestrator.mjs +242 -0
- package/dist/feishu/lark-passthrough.mjs +70 -0
- package/dist/feishu/message-policy.cjs +111 -0
- package/dist/feishu/message-policy.mjs +98 -0
- package/dist/feishu/outbound-transport.cjs +221 -0
- package/dist/feishu/outbound-transport.mjs +215 -0
- package/dist/platform/build-info.cjs +25 -0
- package/dist/platform/build-info.mjs +18 -0
- package/dist/platform/callback-capability.cjs +301 -0
- package/dist/platform/callback-capability.mjs +252 -0
- package/dist/platform/check-bun-version.cjs +12 -0
- package/dist/platform/config.cjs +751 -0
- package/dist/platform/config.mjs +694 -0
- package/dist/platform/process-inspect.cjs +89 -0
- package/dist/platform/process-state.cjs +237 -0
- package/dist/platform/process-state.mjs +223 -0
- package/dist/platform/release-artifacts.mjs +61 -0
- package/dist/platform/root-layout.cjs +111 -0
- package/dist/platform/root-layout.mjs +72 -0
- package/dist/platform/telemetry-config.cjs +64 -0
- package/dist/platform/telemetry-config.mjs +56 -0
- package/dist/platform/telemetry-spool.cjs +661 -0
- package/dist/platform/telemetry-spool.mjs +653 -0
- package/dist/platform/telemetry-tracing.cjs +602 -0
- package/dist/platform/telemetry-tracing.mjs +596 -0
- package/dist/platform/telemetry-uploader.cjs +150 -0
- package/dist/platform/telemetry-uploader.mjs +146 -0
- package/dist/platform/workspace-service.cjs +469 -0
- package/dist/platform/workspace-service.mjs +430 -0
- package/dist/runtime/builtin-pi-assets.mjs +33 -0
- package/dist/runtime/claude-model-catalog.cjs +102 -0
- package/dist/runtime/claude-model-catalog.mjs +99 -0
- package/dist/runtime/codex-model-catalog.cjs +108 -0
- package/dist/runtime/codex-model-catalog.mjs +105 -0
- package/dist/runtime/pi-bash-timeout-extension.mjs +60 -0
- package/dist/runtime/pi-bash-timeout-injection.mjs +60 -0
- package/dist/runtime/pi-bash-timeout.bundle.js +43 -0
- package/dist/runtime/pi-model-catalog.cjs +116 -0
- package/dist/runtime/pi-model-catalog.mjs +75 -0
- package/dist/runtime/pi-official-auth.mjs +480 -0
- package/dist/runtime/pi-provider-config.cjs +252 -0
- package/dist/runtime/pi-provider-config.mjs +239 -0
- package/dist/runtime/pi-rpc-client.cjs +255 -0
- package/dist/runtime/pi-rpc-client.mjs +251 -0
- package/dist/runtime/pi-subagent-injection.mjs +129 -0
- package/dist/runtime/pi-subagents.bundle.js +8990 -0
- package/dist/runtime/runtime-adapters.mjs +1019 -0
- package/dist/runtime/runtime-contracts.cjs +2 -0
- package/dist/runtime/runtime-contracts.mjs +1 -0
- package/dist/runtime/runtime-host.cjs +930 -0
- package/dist/runtime/runtime-host.mjs +922 -0
- package/dist/runtime/runtime-model-catalog.cjs +43 -0
- package/dist/runtime/runtime-model-catalog.mjs +40 -0
- package/dist/runtime/runtime-readiness.cjs +177 -0
- package/dist/runtime/runtime-readiness.mjs +133 -0
- package/dist/setup/bot-register.mjs +655 -0
- package/dist/setup/grant-scopes.mjs +133 -0
- package/dist/setup/run-credential-preflight.mjs +89 -0
- package/dist/setup/setup-agent-choice.mjs +172 -0
- package/dist/setup/setup-bind.mjs +337 -0
- package/dist/setup/setup-binding.mjs +52 -0
- package/package.json +112 -0
|
@@ -0,0 +1,728 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import net from "node:net";
|
|
4
|
+
import { RuntimePrerequisiteError } from "../runtime/runtime-readiness.mjs";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { readProcessState } from "../platform/process-state.mjs";
|
|
8
|
+
import { currentProcessMetadata } from "../platform/process-inspect.cjs";
|
|
9
|
+
import { processCommandToken } from "./internal-command.mjs";
|
|
10
|
+
const AGENT_ID = /^cli_[A-Za-z0-9]+$/;
|
|
11
|
+
const OPERATION_ID = /^[A-Za-z0-9_-]{8,128}$/;
|
|
12
|
+
const AUTHORIZATION = /^[A-Za-z0-9_-]{43,128}$/;
|
|
13
|
+
const UNIX_SOCKET_PATH_MAX_BYTES = process.platform === "darwin" ? 103 : 107;
|
|
14
|
+
function socketPathsFit(root) {
|
|
15
|
+
return ["supervisor.sock", "daemon.sock"].every((name) => Buffer.byteLength(path.join(root, name)) <= UNIX_SOCKET_PATH_MAX_BYTES);
|
|
16
|
+
}
|
|
17
|
+
function controlSocketRoot(larkinHome) {
|
|
18
|
+
const owner = typeof process.getuid === "function" ? process.getuid() : "user";
|
|
19
|
+
const identity = crypto.createHash("sha256").update(path.resolve(larkinHome)).digest("hex").slice(0, 16);
|
|
20
|
+
const leaf = `lk-${owner}-${identity}`;
|
|
21
|
+
const preferred = path.join(path.resolve(os.tmpdir()), leaf);
|
|
22
|
+
if (socketPathsFit(preferred))
|
|
23
|
+
return preferred;
|
|
24
|
+
const fallback = path.join("/tmp", leaf);
|
|
25
|
+
if (socketPathsFit(fallback))
|
|
26
|
+
return fallback;
|
|
27
|
+
throw new Error("无法生成满足 Unix socket 长度限制的 control root");
|
|
28
|
+
}
|
|
29
|
+
function assertSecureSocketDirectory(root) {
|
|
30
|
+
const stat = fs.lstatSync(root);
|
|
31
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()
|
|
32
|
+
|| (typeof process.getuid === "function" && stat.uid !== process.getuid())
|
|
33
|
+
|| (stat.mode & 0o077) !== 0)
|
|
34
|
+
throw new Error("control socket root 不安全");
|
|
35
|
+
return root;
|
|
36
|
+
}
|
|
37
|
+
function ensureSecureSocketRoot(larkinHome) {
|
|
38
|
+
const root = controlSocketRoot(larkinHome);
|
|
39
|
+
fs.mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
40
|
+
return assertSecureSocketDirectory(root);
|
|
41
|
+
}
|
|
42
|
+
export function controlSocketPath(larkinHome) {
|
|
43
|
+
return path.join(controlSocketRoot(larkinHome), "daemon.sock");
|
|
44
|
+
}
|
|
45
|
+
export function controlAuthorityPath(larkinHome) {
|
|
46
|
+
return path.join(path.resolve(larkinHome), "daemon-control-auth.json");
|
|
47
|
+
}
|
|
48
|
+
export function supervisorControlSocketPath(larkinHome) {
|
|
49
|
+
return path.join(controlSocketRoot(larkinHome), "supervisor.sock");
|
|
50
|
+
}
|
|
51
|
+
function assertSecureRoot(root) {
|
|
52
|
+
const stat = fs.lstatSync(root);
|
|
53
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()
|
|
54
|
+
|| (typeof process.getuid === "function" && stat.uid !== process.getuid())
|
|
55
|
+
|| (stat.mode & 0o077) !== 0)
|
|
56
|
+
throw new Error("Larkin config root 必须由当前用户拥有且不可被其他用户访问");
|
|
57
|
+
}
|
|
58
|
+
function removeLegacyResetLedger(larkinHome) {
|
|
59
|
+
const file = path.join(path.resolve(larkinHome), "daemon-control-operations.json");
|
|
60
|
+
let stat;
|
|
61
|
+
try {
|
|
62
|
+
stat = fs.lstatSync(file);
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
if (error.code === "ENOENT")
|
|
66
|
+
return;
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
if (!stat.isFile() || stat.isSymbolicLink()
|
|
70
|
+
|| (typeof process.getuid === "function" && stat.uid !== process.getuid()) || (stat.mode & 0o077) !== 0) {
|
|
71
|
+
throw new Error("legacy daemon control operation ledger 不安全");
|
|
72
|
+
}
|
|
73
|
+
fs.unlinkSync(file);
|
|
74
|
+
}
|
|
75
|
+
function parseRequest(line) {
|
|
76
|
+
const value = JSON.parse(line);
|
|
77
|
+
if (!AGENT_ID.test(String(value.agentId || "")) || !AUTHORIZATION.test(String(value.authorization || ""))) {
|
|
78
|
+
throw new Error("invalid agent control request");
|
|
79
|
+
}
|
|
80
|
+
if (value.operation === "session-reset") {
|
|
81
|
+
if (value.waitReadyMs !== undefined && (!Number.isSafeInteger(value.waitReadyMs) || value.waitReadyMs < 0 || value.waitReadyMs > 300_000)) {
|
|
82
|
+
throw new Error("invalid session reset waitReadyMs");
|
|
83
|
+
}
|
|
84
|
+
if (Object.keys(value).some((key) => !["agentId", "authorization", "operation", "waitReadyMs"].includes(key))) {
|
|
85
|
+
throw new Error("session reset control request 包含未知字段");
|
|
86
|
+
}
|
|
87
|
+
return value;
|
|
88
|
+
}
|
|
89
|
+
if (value.operation !== undefined || !OPERATION_ID.test(String(value.operationId || ""))) {
|
|
90
|
+
throw new Error("invalid agent upsert request");
|
|
91
|
+
}
|
|
92
|
+
if (value.waitReadyMs !== undefined) {
|
|
93
|
+
throw new Error("invalid session reset waitReadyMs");
|
|
94
|
+
}
|
|
95
|
+
if (Object.keys(value).some((key) => !["operationId", "agentId", "authorization"].includes(key))) {
|
|
96
|
+
throw new Error("agent control request 包含未知字段");
|
|
97
|
+
}
|
|
98
|
+
return value;
|
|
99
|
+
}
|
|
100
|
+
function atomicWritePrivateJson(file, value) {
|
|
101
|
+
const temporary = `${file}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
102
|
+
const fd = fs.openSync(temporary, "wx", 0o600);
|
|
103
|
+
try {
|
|
104
|
+
fs.writeFileSync(fd, `${JSON.stringify(value, null, 2)}\n`);
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
fs.closeSync(fd);
|
|
108
|
+
}
|
|
109
|
+
fs.renameSync(temporary, file);
|
|
110
|
+
}
|
|
111
|
+
function secureAuthority(larkinHome) {
|
|
112
|
+
assertSecureRoot(larkinHome);
|
|
113
|
+
const file = controlAuthorityPath(larkinHome);
|
|
114
|
+
const stat = fs.lstatSync(file);
|
|
115
|
+
if (!stat.isFile() || stat.isSymbolicLink()
|
|
116
|
+
|| (typeof process.getuid === "function" && stat.uid !== process.getuid())
|
|
117
|
+
|| (stat.mode & 0o077) !== 0)
|
|
118
|
+
throw new Error("daemon control authority 不安全");
|
|
119
|
+
const value = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
120
|
+
const validBinding = (binding) => !!binding && Number.isSafeInteger(binding.pid) && binding.pid > 0
|
|
121
|
+
&& typeof binding.processStartToken === "string" && binding.processStartToken.length > 0;
|
|
122
|
+
const validSocketBinding = (binding) => !!binding && /^\d+$/.test(binding.device) && /^\d+$/.test(binding.inode)
|
|
123
|
+
&& /^\d+$/.test(binding.owner) && /^\d+$/.test(binding.changeTimeNs);
|
|
124
|
+
const socketRoot = String(value.socketRoot || "");
|
|
125
|
+
const supervisorSocket = String(value.supervisorSocketPath || "");
|
|
126
|
+
const daemonSocket = String(value.daemonSocketPath || "");
|
|
127
|
+
const validSocketPaths = path.isAbsolute(socketRoot)
|
|
128
|
+
&& path.resolve(socketRoot) === socketRoot
|
|
129
|
+
&& socketPathsFit(socketRoot)
|
|
130
|
+
&& path.dirname(supervisorSocket) === socketRoot && path.basename(supervisorSocket) === "supervisor.sock"
|
|
131
|
+
&& path.dirname(daemonSocket) === socketRoot && path.basename(daemonSocket) === "daemon.sock";
|
|
132
|
+
if (value.version !== 2 || !AUTHORIZATION.test(String(value.token || "")) || !validBinding(value.supervisor)
|
|
133
|
+
|| !validSocketPaths
|
|
134
|
+
|| (value.supervisorSocket !== undefined && !validSocketBinding(value.supervisorSocket))
|
|
135
|
+
|| (value.daemon !== undefined && !validBinding(value.daemon))
|
|
136
|
+
|| (value.daemonSocket !== undefined && (!value.daemon || !validSocketBinding(value.daemonSocket)))) {
|
|
137
|
+
throw new Error("daemon control authority 无效");
|
|
138
|
+
}
|
|
139
|
+
return value;
|
|
140
|
+
}
|
|
141
|
+
function sameSecret(left, right) {
|
|
142
|
+
const a = Buffer.from(left);
|
|
143
|
+
const b = Buffer.from(right);
|
|
144
|
+
return a.length === b.length && crypto.timingSafeEqual(a, b);
|
|
145
|
+
}
|
|
146
|
+
function bindingMatches(binding, record) {
|
|
147
|
+
return binding.pid === Number(record.pid) && binding.processStartToken === record.processStartToken;
|
|
148
|
+
}
|
|
149
|
+
function socketBinding(stat) {
|
|
150
|
+
return {
|
|
151
|
+
device: String(stat.dev),
|
|
152
|
+
inode: String(stat.ino),
|
|
153
|
+
owner: String(stat.uid),
|
|
154
|
+
changeTimeNs: String(stat.ctimeNs),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
function socketObjectMatches(binding, stat) {
|
|
158
|
+
return binding.device === String(stat.dev) && binding.inode === String(stat.ino)
|
|
159
|
+
&& binding.owner === String(stat.uid);
|
|
160
|
+
}
|
|
161
|
+
function socketBindingMatches(binding, stat) {
|
|
162
|
+
return socketObjectMatches(binding, stat) && binding.changeTimeNs === String(stat.ctimeNs);
|
|
163
|
+
}
|
|
164
|
+
function inspectOwnedSocket(socket) {
|
|
165
|
+
const stat = fs.lstatSync(socket, { bigint: true });
|
|
166
|
+
if (!stat.isSocket() || stat.isSymbolicLink()
|
|
167
|
+
|| (typeof process.getuid === "function" && stat.uid !== BigInt(process.getuid()))) {
|
|
168
|
+
throw new Error("control socket identity 不安全");
|
|
169
|
+
}
|
|
170
|
+
return { stat, binding: socketBinding(stat) };
|
|
171
|
+
}
|
|
172
|
+
function assertLiveAuthority(larkinHome, expectedToken) {
|
|
173
|
+
const authority = secureAuthority(larkinHome);
|
|
174
|
+
if (expectedToken && !sameSecret(authority.token, expectedToken))
|
|
175
|
+
throw new Error("daemon control authorization 已失效");
|
|
176
|
+
const { supervisor, daemon } = readProcessState(larkinHome);
|
|
177
|
+
if (supervisor.state !== "owned" || !bindingMatches(authority.supervisor, supervisor)) {
|
|
178
|
+
throw new Error("daemon control supervisor identity 已失效");
|
|
179
|
+
}
|
|
180
|
+
if (!authority.daemon || daemon.state !== "owned" || !bindingMatches(authority.daemon, daemon)) {
|
|
181
|
+
throw new Error("daemon control daemon identity 已失效");
|
|
182
|
+
}
|
|
183
|
+
return authority;
|
|
184
|
+
}
|
|
185
|
+
function assertSupervisorAuthority(larkinHome, expectedToken) {
|
|
186
|
+
const authority = secureAuthority(larkinHome);
|
|
187
|
+
if (expectedToken && !sameSecret(authority.token, expectedToken))
|
|
188
|
+
throw new Error("supervisor control authorization 已失效");
|
|
189
|
+
const supervisor = readProcessState(larkinHome).supervisor;
|
|
190
|
+
if (supervisor.state !== "owned" || !bindingMatches(authority.supervisor, supervisor)) {
|
|
191
|
+
throw new Error("supervisor control identity 已失效");
|
|
192
|
+
}
|
|
193
|
+
return authority;
|
|
194
|
+
}
|
|
195
|
+
function prepareSocket(socket) {
|
|
196
|
+
try {
|
|
197
|
+
const stat = fs.lstatSync(socket);
|
|
198
|
+
if (!stat.isSocket() || stat.isSymbolicLink()
|
|
199
|
+
|| (typeof process.getuid === "function" && stat.uid !== process.getuid())) {
|
|
200
|
+
throw new Error("control socket 路径被不安全文件占用");
|
|
201
|
+
}
|
|
202
|
+
fs.unlinkSync(socket);
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
if (error.code !== "ENOENT")
|
|
206
|
+
throw error;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
function cleanupSocketRoot(root) {
|
|
210
|
+
try {
|
|
211
|
+
fs.rmdirSync(root);
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
if (!["ENOENT", "ENOTEMPTY"].includes(String(error.code || "")))
|
|
215
|
+
throw error;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
async function closePrivateServer(server, socket, identity) {
|
|
219
|
+
let shield = null;
|
|
220
|
+
let closeError;
|
|
221
|
+
try {
|
|
222
|
+
let current = null;
|
|
223
|
+
try {
|
|
224
|
+
current = fs.lstatSync(socket, { bigint: true });
|
|
225
|
+
}
|
|
226
|
+
catch (error) {
|
|
227
|
+
if (error.code !== "ENOENT")
|
|
228
|
+
throw error;
|
|
229
|
+
}
|
|
230
|
+
if (current && (!identity || !socketBindingMatches(identity, current))) {
|
|
231
|
+
// The runtime unlinks its original Unix pathname during server.close(). Move a
|
|
232
|
+
// replacement entry aside so closing the old server cannot delete it.
|
|
233
|
+
shield = `${socket}.replacement-${crypto.randomUUID()}`;
|
|
234
|
+
fs.renameSync(socket, shield);
|
|
235
|
+
}
|
|
236
|
+
if (server.listening) {
|
|
237
|
+
await new Promise((resolve) => server.close((error) => {
|
|
238
|
+
if (error)
|
|
239
|
+
closeError = error;
|
|
240
|
+
resolve();
|
|
241
|
+
}));
|
|
242
|
+
}
|
|
243
|
+
if (!shield && identity) {
|
|
244
|
+
try {
|
|
245
|
+
const after = fs.lstatSync(socket, { bigint: true });
|
|
246
|
+
if (after.isSocket() && !after.isSymbolicLink() && socketBindingMatches(identity, after))
|
|
247
|
+
fs.unlinkSync(socket);
|
|
248
|
+
}
|
|
249
|
+
catch (error) {
|
|
250
|
+
if (error.code !== "ENOENT")
|
|
251
|
+
throw error;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
finally {
|
|
256
|
+
if (shield) {
|
|
257
|
+
try {
|
|
258
|
+
fs.lstatSync(socket);
|
|
259
|
+
throw new Error(`replacement socket 恢复失败,原路径已被再次占用;保留于 ${shield}`);
|
|
260
|
+
}
|
|
261
|
+
catch (error) {
|
|
262
|
+
if (error.code === "ENOENT")
|
|
263
|
+
fs.renameSync(shield, socket);
|
|
264
|
+
else
|
|
265
|
+
throw error;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
cleanupSocketRoot(path.dirname(socket));
|
|
269
|
+
}
|
|
270
|
+
if (closeError)
|
|
271
|
+
throw closeError;
|
|
272
|
+
}
|
|
273
|
+
async function listenPrivate(server, socket) {
|
|
274
|
+
let identity = null;
|
|
275
|
+
try {
|
|
276
|
+
await new Promise((resolve, reject) => {
|
|
277
|
+
server.once("error", reject);
|
|
278
|
+
server.listen(socket, () => { server.off("error", reject); resolve(); });
|
|
279
|
+
});
|
|
280
|
+
identity = inspectOwnedSocket(socket).binding;
|
|
281
|
+
const deadline = Date.now() + 1_000;
|
|
282
|
+
for (;;) {
|
|
283
|
+
try {
|
|
284
|
+
fs.chmodSync(socket, 0o600);
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
catch (error) {
|
|
288
|
+
if (error.code !== "ENOENT" || Date.now() >= deadline)
|
|
289
|
+
throw error;
|
|
290
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
const secured = inspectOwnedSocket(socket);
|
|
294
|
+
if (!socketObjectMatches(identity, secured.stat)) {
|
|
295
|
+
throw new Error("control socket 在权限收紧期间被其他 server 替换");
|
|
296
|
+
}
|
|
297
|
+
identity = secured.binding;
|
|
298
|
+
return identity;
|
|
299
|
+
}
|
|
300
|
+
catch (error) {
|
|
301
|
+
await closePrivateServer(server, socket, identity);
|
|
302
|
+
throw error;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
export function initializeControlAuthority(larkinHome, supervisor) {
|
|
306
|
+
fs.mkdirSync(larkinHome, { recursive: true, mode: 0o700 });
|
|
307
|
+
assertSecureRoot(larkinHome);
|
|
308
|
+
const socketRoot = ensureSecureSocketRoot(larkinHome);
|
|
309
|
+
const token = crypto.randomBytes(32).toString("base64url");
|
|
310
|
+
atomicWritePrivateJson(controlAuthorityPath(larkinHome), {
|
|
311
|
+
version: 2,
|
|
312
|
+
token,
|
|
313
|
+
socketRoot,
|
|
314
|
+
supervisorSocketPath: path.join(socketRoot, "supervisor.sock"),
|
|
315
|
+
daemonSocketPath: path.join(socketRoot, "daemon.sock"),
|
|
316
|
+
supervisor,
|
|
317
|
+
});
|
|
318
|
+
return token;
|
|
319
|
+
}
|
|
320
|
+
export function removeControlAuthority(larkinHome, expectedToken) {
|
|
321
|
+
try {
|
|
322
|
+
const authority = secureAuthority(larkinHome);
|
|
323
|
+
if (sameSecret(authority.token, expectedToken)) {
|
|
324
|
+
fs.unlinkSync(controlAuthorityPath(larkinHome));
|
|
325
|
+
cleanupSocketRoot(authority.socketRoot);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
catch (error) {
|
|
329
|
+
if (error.code !== "ENOENT")
|
|
330
|
+
throw error;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
export function createSupervisorControlServer({ larkinHome, authorityToken, ensureDashboard, }) {
|
|
334
|
+
let socket = "";
|
|
335
|
+
let socketRoot = "";
|
|
336
|
+
let socketIdentity = null;
|
|
337
|
+
const completed = new Map();
|
|
338
|
+
let server = null;
|
|
339
|
+
return {
|
|
340
|
+
async start() {
|
|
341
|
+
const authority = assertSupervisorAuthority(larkinHome, authorityToken);
|
|
342
|
+
socketRoot = assertSecureSocketDirectory(authority.socketRoot);
|
|
343
|
+
socket = authority.supervisorSocketPath;
|
|
344
|
+
prepareSocket(socket);
|
|
345
|
+
server = net.createServer((connection) => {
|
|
346
|
+
connection.setEncoding("utf8");
|
|
347
|
+
let input = "";
|
|
348
|
+
connection.on("data", (chunk) => {
|
|
349
|
+
input += chunk;
|
|
350
|
+
if (input.length > 4096)
|
|
351
|
+
connection.destroy(new Error("control request too large"));
|
|
352
|
+
const newline = input.indexOf("\n");
|
|
353
|
+
if (newline < 0)
|
|
354
|
+
return;
|
|
355
|
+
const line = input.slice(0, newline);
|
|
356
|
+
input = "";
|
|
357
|
+
void (async () => {
|
|
358
|
+
let request;
|
|
359
|
+
try {
|
|
360
|
+
request = JSON.parse(line);
|
|
361
|
+
if (!OPERATION_ID.test(String(request.operationId || "")) || request.operation !== "ensure-dashboard"
|
|
362
|
+
|| !AUTHORIZATION.test(String(request.authorization || ""))
|
|
363
|
+
|| Object.keys(request).some((key) => !["operationId", "operation", "authorization"].includes(key))) {
|
|
364
|
+
throw new Error("invalid supervisor control request");
|
|
365
|
+
}
|
|
366
|
+
const authority = assertSupervisorAuthority(larkinHome, authorityToken);
|
|
367
|
+
if (!sameSecret(authority.token, request.authorization))
|
|
368
|
+
throw new Error("unauthorized supervisor control request");
|
|
369
|
+
}
|
|
370
|
+
catch {
|
|
371
|
+
connection.end(`${JSON.stringify({ ok: false, operationId: "invalid", error: "unauthorized supervisor control request" })}\n`);
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
const replay = completed.get(request.operationId);
|
|
375
|
+
if (replay) {
|
|
376
|
+
connection.end(`${JSON.stringify(replay)}\n`);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
let response;
|
|
380
|
+
try {
|
|
381
|
+
response = { ok: true, operationId: request.operationId, state: await ensureDashboard() };
|
|
382
|
+
}
|
|
383
|
+
catch (error) {
|
|
384
|
+
response = { ok: false, operationId: request.operationId, error: error instanceof Error ? error.message : String(error) };
|
|
385
|
+
}
|
|
386
|
+
completed.set(request.operationId, response);
|
|
387
|
+
while (completed.size > 256)
|
|
388
|
+
completed.delete(completed.keys().next().value);
|
|
389
|
+
connection.end(`${JSON.stringify(response)}\n`);
|
|
390
|
+
})();
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
try {
|
|
394
|
+
socketIdentity = await listenPrivate(server, socket);
|
|
395
|
+
const liveAuthority = secureAuthority(larkinHome);
|
|
396
|
+
if (!sameSecret(liveAuthority.token, authorityToken))
|
|
397
|
+
throw new Error("supervisor control authorization 已失效");
|
|
398
|
+
atomicWritePrivateJson(controlAuthorityPath(larkinHome), {
|
|
399
|
+
...liveAuthority,
|
|
400
|
+
supervisorSocket: socketIdentity,
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
catch (error) {
|
|
404
|
+
if (server && socketIdentity)
|
|
405
|
+
await closePrivateServer(server, socket, socketIdentity);
|
|
406
|
+
server = null;
|
|
407
|
+
socketIdentity = null;
|
|
408
|
+
throw error;
|
|
409
|
+
}
|
|
410
|
+
},
|
|
411
|
+
async close() {
|
|
412
|
+
const active = server;
|
|
413
|
+
server = null;
|
|
414
|
+
if (active && socket)
|
|
415
|
+
await closePrivateServer(active, socket, socketIdentity);
|
|
416
|
+
else if (socketRoot)
|
|
417
|
+
cleanupSocketRoot(socketRoot);
|
|
418
|
+
socketIdentity = null;
|
|
419
|
+
},
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
export function createAgentControlServer({ larkinHome, authorityToken, upsert, resetSession, maxRememberedOperations = 256, }) {
|
|
423
|
+
let socket = "";
|
|
424
|
+
let socketRoot = "";
|
|
425
|
+
let socketIdentity = null;
|
|
426
|
+
const completed = new Map();
|
|
427
|
+
const inFlight = new Map();
|
|
428
|
+
const resetInFlight = new Map();
|
|
429
|
+
const agentQueues = new Map();
|
|
430
|
+
const upsertConflict = (operationId, agentId) => ({
|
|
431
|
+
ok: false, operationId, agentId, code: "operation_conflict", error: "operationId 已绑定其他 Agent 或操作",
|
|
432
|
+
});
|
|
433
|
+
let server = null;
|
|
434
|
+
return {
|
|
435
|
+
async start() {
|
|
436
|
+
fs.mkdirSync(larkinHome, { recursive: true, mode: 0o700 });
|
|
437
|
+
assertSecureRoot(larkinHome);
|
|
438
|
+
removeLegacyResetLedger(larkinHome);
|
|
439
|
+
const authority = secureAuthority(larkinHome);
|
|
440
|
+
if (!sameSecret(authority.token, authorityToken))
|
|
441
|
+
throw new Error("daemon control authorization 不匹配");
|
|
442
|
+
const supervisor = readProcessState(larkinHome).supervisor;
|
|
443
|
+
if (supervisor.state !== "owned" || !bindingMatches(authority.supervisor, supervisor)) {
|
|
444
|
+
throw new Error("daemon control supervisor identity 无效");
|
|
445
|
+
}
|
|
446
|
+
const daemonMetadata = currentProcessMetadata(processCommandToken("daemon", "app/runtime-process.mjs"));
|
|
447
|
+
if (!daemonMetadata.processStartToken)
|
|
448
|
+
throw new Error("daemon control 无法取得 daemon start identity");
|
|
449
|
+
atomicWritePrivateJson(controlAuthorityPath(larkinHome), {
|
|
450
|
+
...authority,
|
|
451
|
+
daemon: { pid: process.pid, processStartToken: daemonMetadata.processStartToken },
|
|
452
|
+
});
|
|
453
|
+
socketRoot = assertSecureSocketDirectory(authority.socketRoot);
|
|
454
|
+
socket = authority.daemonSocketPath;
|
|
455
|
+
prepareSocket(socket);
|
|
456
|
+
server = net.createServer((connection) => {
|
|
457
|
+
connection.setEncoding("utf8");
|
|
458
|
+
let input = "";
|
|
459
|
+
connection.on("data", (chunk) => {
|
|
460
|
+
input += chunk;
|
|
461
|
+
if (input.length > 4096)
|
|
462
|
+
connection.destroy(new Error("control request too large"));
|
|
463
|
+
const newline = input.indexOf("\n");
|
|
464
|
+
if (newline < 0)
|
|
465
|
+
return;
|
|
466
|
+
const line = input.slice(0, newline);
|
|
467
|
+
input = "";
|
|
468
|
+
void (async () => {
|
|
469
|
+
let request;
|
|
470
|
+
try {
|
|
471
|
+
request = parseRequest(line);
|
|
472
|
+
}
|
|
473
|
+
catch (error) {
|
|
474
|
+
connection.end(`${JSON.stringify({ ok: false, agentId: "invalid", error: error.message })}\n`);
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
try {
|
|
478
|
+
const live = assertLiveAuthority(larkinHome, authorityToken);
|
|
479
|
+
if (!sameSecret(live.token, request.authorization))
|
|
480
|
+
throw new Error("unauthorized control request");
|
|
481
|
+
}
|
|
482
|
+
catch {
|
|
483
|
+
connection.end(`${JSON.stringify({ ok: false, ...("operation" in request ? {} : { operationId: request.operationId }),
|
|
484
|
+
agentId: request.agentId, error: "unauthorized control request" })}\n`);
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
if ("operation" in request) {
|
|
488
|
+
const resetRequest = request;
|
|
489
|
+
let operation = resetInFlight.get(resetRequest.agentId);
|
|
490
|
+
if (!operation) {
|
|
491
|
+
const executeReset = async () => {
|
|
492
|
+
try {
|
|
493
|
+
if (!resetSession)
|
|
494
|
+
throw new Error("session reset control unavailable");
|
|
495
|
+
return await resetSession({ agentId: resetRequest.agentId, waitReadyMs: resetRequest.waitReadyMs ?? 30_000 });
|
|
496
|
+
}
|
|
497
|
+
catch (error) {
|
|
498
|
+
const code = typeof error.code === "string"
|
|
499
|
+
? String(error.code)
|
|
500
|
+
: error instanceof RuntimePrerequisiteError && error.readiness.state === "unavailable"
|
|
501
|
+
? "runtime_unavailable" : "reset_refused";
|
|
502
|
+
const projection = error;
|
|
503
|
+
return { ok: false, agentId: resetRequest.agentId, code,
|
|
504
|
+
error: error instanceof Error ? error.message : String(error), resetCommitted: false,
|
|
505
|
+
generationChanged: false, sessionChanged: false,
|
|
506
|
+
turns: Math.max(0, Number(projection.turns) || 0), runtimeReady: projection.runtimeReady === true,
|
|
507
|
+
channelConnected: projection.channelConnected === true, reconnecting: projection.reconnecting === true,
|
|
508
|
+
pendingCount: Math.max(0, Number(projection.pendingCount) || 0),
|
|
509
|
+
readyForFreshScenario: false, inboundObserved: false,
|
|
510
|
+
...(error instanceof RuntimePrerequisiteError ? { readiness: error.readiness } : {}) };
|
|
511
|
+
}
|
|
512
|
+
};
|
|
513
|
+
const prior = agentQueues.get(resetRequest.agentId) ?? Promise.resolve();
|
|
514
|
+
const executing = prior.catch(() => { }).then(executeReset);
|
|
515
|
+
let queued;
|
|
516
|
+
queued = executing.finally(() => {
|
|
517
|
+
if (agentQueues.get(resetRequest.agentId) === queued)
|
|
518
|
+
agentQueues.delete(resetRequest.agentId);
|
|
519
|
+
if (resetInFlight.get(resetRequest.agentId) === queued)
|
|
520
|
+
resetInFlight.delete(resetRequest.agentId);
|
|
521
|
+
});
|
|
522
|
+
agentQueues.set(resetRequest.agentId, queued);
|
|
523
|
+
resetInFlight.set(resetRequest.agentId, queued);
|
|
524
|
+
operation = queued;
|
|
525
|
+
}
|
|
526
|
+
const response = await operation;
|
|
527
|
+
connection.end(`${JSON.stringify(response)}\n`);
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
const upsertRequest = request;
|
|
531
|
+
const replay = completed.get(upsertRequest.operationId);
|
|
532
|
+
if (replay) {
|
|
533
|
+
connection.end(`${JSON.stringify(replay.agentId === upsertRequest.agentId
|
|
534
|
+
? replay : upsertConflict(upsertRequest.operationId, upsertRequest.agentId))}\n`);
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
const existing = inFlight.get(upsertRequest.operationId);
|
|
538
|
+
if (existing && existing.agentId !== upsertRequest.agentId) {
|
|
539
|
+
connection.end(`${JSON.stringify(upsertConflict(upsertRequest.operationId, upsertRequest.agentId))}\n`);
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
const execute = async () => {
|
|
543
|
+
try {
|
|
544
|
+
await upsert({ operationId: upsertRequest.operationId, agentId: upsertRequest.agentId });
|
|
545
|
+
return { ok: true, operationId: upsertRequest.operationId, agentId: upsertRequest.agentId };
|
|
546
|
+
}
|
|
547
|
+
catch (error) {
|
|
548
|
+
return { ok: false, operationId: upsertRequest.operationId, agentId: upsertRequest.agentId,
|
|
549
|
+
error: error instanceof Error ? error.message : String(error),
|
|
550
|
+
...(error instanceof RuntimePrerequisiteError ? { readiness: error.readiness } : {}) };
|
|
551
|
+
}
|
|
552
|
+
};
|
|
553
|
+
let operation = existing?.response;
|
|
554
|
+
if (!operation) {
|
|
555
|
+
const prior = agentQueues.get(upsertRequest.agentId) ?? Promise.resolve();
|
|
556
|
+
operation = prior.catch(() => { }).then(execute);
|
|
557
|
+
const queued = operation.finally(() => {
|
|
558
|
+
if (agentQueues.get(upsertRequest.agentId) === queued)
|
|
559
|
+
agentQueues.delete(upsertRequest.agentId);
|
|
560
|
+
});
|
|
561
|
+
agentQueues.set(upsertRequest.agentId, queued);
|
|
562
|
+
inFlight.set(upsertRequest.operationId, { agentId: upsertRequest.agentId, response: operation });
|
|
563
|
+
}
|
|
564
|
+
const response = await operation;
|
|
565
|
+
inFlight.delete(upsertRequest.operationId);
|
|
566
|
+
completed.set(upsertRequest.operationId, response);
|
|
567
|
+
while (completed.size > maxRememberedOperations)
|
|
568
|
+
completed.delete(completed.keys().next().value);
|
|
569
|
+
connection.end(`${JSON.stringify(response)}\n`);
|
|
570
|
+
})();
|
|
571
|
+
});
|
|
572
|
+
});
|
|
573
|
+
try {
|
|
574
|
+
socketIdentity = await listenPrivate(server, socket);
|
|
575
|
+
const liveAuthority = secureAuthority(larkinHome);
|
|
576
|
+
if (!sameSecret(liveAuthority.token, authorityToken))
|
|
577
|
+
throw new Error("daemon control authorization 已失效");
|
|
578
|
+
atomicWritePrivateJson(controlAuthorityPath(larkinHome), {
|
|
579
|
+
...liveAuthority,
|
|
580
|
+
daemonSocket: socketIdentity,
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
catch (error) {
|
|
584
|
+
if (server && socketIdentity)
|
|
585
|
+
await closePrivateServer(server, socket, socketIdentity);
|
|
586
|
+
server = null;
|
|
587
|
+
socketIdentity = null;
|
|
588
|
+
throw error;
|
|
589
|
+
}
|
|
590
|
+
},
|
|
591
|
+
async close() {
|
|
592
|
+
const active = server;
|
|
593
|
+
server = null;
|
|
594
|
+
if (active && socket)
|
|
595
|
+
await closePrivateServer(active, socket, socketIdentity);
|
|
596
|
+
else if (socketRoot)
|
|
597
|
+
cleanupSocketRoot(socketRoot);
|
|
598
|
+
socketIdentity = null;
|
|
599
|
+
},
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
async function sendSupervisorRecovery(larkinHome, operationId, timeoutMs) {
|
|
603
|
+
const authority = assertSupervisorAuthority(larkinHome);
|
|
604
|
+
assertSecureSocketDirectory(authority.socketRoot);
|
|
605
|
+
const socket = authority.supervisorSocketPath;
|
|
606
|
+
const stat = fs.lstatSync(socket);
|
|
607
|
+
if (!stat.isSocket() || stat.isSymbolicLink()
|
|
608
|
+
|| (typeof process.getuid === "function" && stat.uid !== process.getuid())
|
|
609
|
+
|| (stat.mode & 0o077) !== 0)
|
|
610
|
+
throw new Error("supervisor control socket 不安全");
|
|
611
|
+
return await new Promise((resolve, reject) => {
|
|
612
|
+
const client = net.createConnection(socket);
|
|
613
|
+
const timer = setTimeout(() => { client.destroy(); reject(new Error("dashboard recovery control timeout")); }, timeoutMs);
|
|
614
|
+
let input = "";
|
|
615
|
+
client.setEncoding("utf8");
|
|
616
|
+
client.once("error", (error) => { clearTimeout(timer); reject(error); });
|
|
617
|
+
client.once("connect", () => client.write(`${JSON.stringify({
|
|
618
|
+
operationId, operation: "ensure-dashboard", authorization: authority.token,
|
|
619
|
+
})}\n`));
|
|
620
|
+
client.on("data", (chunk) => {
|
|
621
|
+
input += chunk;
|
|
622
|
+
const newline = input.indexOf("\n");
|
|
623
|
+
if (newline < 0)
|
|
624
|
+
return;
|
|
625
|
+
clearTimeout(timer);
|
|
626
|
+
client.end();
|
|
627
|
+
try {
|
|
628
|
+
resolve(JSON.parse(input.slice(0, newline)));
|
|
629
|
+
}
|
|
630
|
+
catch (error) {
|
|
631
|
+
reject(error);
|
|
632
|
+
}
|
|
633
|
+
});
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
export async function requestDashboardRecovery({ larkinHome, operationId = crypto.randomUUID(), timeoutMs = 30_000, }) {
|
|
637
|
+
const deadline = Date.now() + timeoutMs;
|
|
638
|
+
let lastError;
|
|
639
|
+
while (Date.now() < deadline) {
|
|
640
|
+
try {
|
|
641
|
+
return await sendSupervisorRecovery(larkinHome, operationId, Math.max(100, deadline - Date.now()));
|
|
642
|
+
}
|
|
643
|
+
catch (error) {
|
|
644
|
+
lastError = error;
|
|
645
|
+
const code = error.code;
|
|
646
|
+
if (!code || !["ENOENT", "ECONNREFUSED"].includes(code))
|
|
647
|
+
throw error;
|
|
648
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
throw lastError instanceof Error ? lastError : new Error("supervisor control unavailable");
|
|
652
|
+
}
|
|
653
|
+
async function requestAgentControl({ larkinHome, timeoutMs = 30_000, request, }) {
|
|
654
|
+
const { supervisor, daemon } = readProcessState(larkinHome);
|
|
655
|
+
if (supervisor.state !== "owned")
|
|
656
|
+
throw new Error(`supervisor control ownership=${supervisor.state}(${supervisor.reason})`);
|
|
657
|
+
if (daemon.state !== "owned")
|
|
658
|
+
throw new Error(`daemon control ownership=${daemon.state}(${daemon.reason})`);
|
|
659
|
+
const authority = assertLiveAuthority(larkinHome);
|
|
660
|
+
assertSecureSocketDirectory(authority.socketRoot);
|
|
661
|
+
const socket = authority.daemonSocketPath;
|
|
662
|
+
const stat = fs.lstatSync(socket);
|
|
663
|
+
if (!stat.isSocket() || stat.isSymbolicLink()
|
|
664
|
+
|| (typeof process.getuid === "function" && stat.uid !== process.getuid())
|
|
665
|
+
|| (stat.mode & 0o077) !== 0)
|
|
666
|
+
throw new Error("daemon control socket 不安全");
|
|
667
|
+
return await new Promise((resolve, reject) => {
|
|
668
|
+
const client = net.createConnection(socket);
|
|
669
|
+
const timer = setTimeout(() => { client.destroy(); reject(new Error("agent control timeout")); }, timeoutMs);
|
|
670
|
+
let input = "";
|
|
671
|
+
client.setEncoding("utf8");
|
|
672
|
+
client.once("error", (error) => { clearTimeout(timer); reject(error); });
|
|
673
|
+
client.once("connect", () => client.write(`${JSON.stringify({ ...request, authorization: authority.token })}\n`));
|
|
674
|
+
client.on("data", (chunk) => {
|
|
675
|
+
input += chunk;
|
|
676
|
+
const newline = input.indexOf("\n");
|
|
677
|
+
if (newline < 0)
|
|
678
|
+
return;
|
|
679
|
+
clearTimeout(timer);
|
|
680
|
+
client.end();
|
|
681
|
+
try {
|
|
682
|
+
resolve(JSON.parse(input.slice(0, newline)));
|
|
683
|
+
}
|
|
684
|
+
catch (error) {
|
|
685
|
+
reject(error);
|
|
686
|
+
}
|
|
687
|
+
});
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
export async function requestAgentUpsert(input) {
|
|
691
|
+
return requestAgentControl({ larkinHome: input.larkinHome, timeoutMs: input.timeoutMs,
|
|
692
|
+
request: { operationId: input.operationId ?? crypto.randomUUID(), agentId: input.agentId } });
|
|
693
|
+
}
|
|
694
|
+
export async function requestSessionReset({ larkinHome, agentId, waitReadyMs = 30_000, }) {
|
|
695
|
+
return requestAgentControl({
|
|
696
|
+
larkinHome, timeoutMs: Math.max(1_000, waitReadyMs + 1_000),
|
|
697
|
+
request: { operation: "session-reset", agentId, waitReadyMs },
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
export function cleanupStaleAgentControlSocket(larkinHome, expectedToken) {
|
|
701
|
+
const authority = secureAuthority(larkinHome);
|
|
702
|
+
if (!sameSecret(authority.token, expectedToken))
|
|
703
|
+
throw new Error("daemon control authorization 已失效");
|
|
704
|
+
const root = assertSecureSocketDirectory(authority.socketRoot);
|
|
705
|
+
const socket = authority.daemonSocketPath;
|
|
706
|
+
if (path.dirname(socket) !== root || path.basename(socket) !== "daemon.sock") {
|
|
707
|
+
throw new Error("daemon control socket 路径不属于认证 control root");
|
|
708
|
+
}
|
|
709
|
+
try {
|
|
710
|
+
const stat = fs.lstatSync(socket, { bigint: true });
|
|
711
|
+
if (!stat.isSocket() || stat.isSymbolicLink()
|
|
712
|
+
|| (typeof process.getuid === "function" && stat.uid !== BigInt(process.getuid()))) {
|
|
713
|
+
throw new Error("拒绝清理不安全的 daemon control socket");
|
|
714
|
+
}
|
|
715
|
+
if (!authority.daemonSocket || !socketBindingMatches(authority.daemonSocket, stat)) {
|
|
716
|
+
throw new Error("拒绝清理已被其他 server 替换的 daemon control socket");
|
|
717
|
+
}
|
|
718
|
+
fs.unlinkSync(socket);
|
|
719
|
+
cleanupSocketRoot(root);
|
|
720
|
+
return "removed";
|
|
721
|
+
}
|
|
722
|
+
catch (error) {
|
|
723
|
+
if (error.code !== "ENOENT")
|
|
724
|
+
throw error;
|
|
725
|
+
cleanupSocketRoot(root);
|
|
726
|
+
return "absent";
|
|
727
|
+
}
|
|
728
|
+
}
|