larkin 0.4.12 → 0.4.13
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.
|
@@ -44,13 +44,14 @@ function managedOfficialLarkCli(agent, env, resolve = official_lark_cli_js_1.res
|
|
|
44
44
|
}
|
|
45
45
|
function readPrivateJson(file, label) {
|
|
46
46
|
const directory = node_fs_1.default.lstatSync(node_path_1.default.dirname(file));
|
|
47
|
-
if (!directory.isDirectory() || directory.isSymbolicLink()
|
|
47
|
+
if (!directory.isDirectory() || directory.isSymbolicLink()
|
|
48
|
+
|| (!(0, secure_metadata_js_1.exactMode)(directory, 0o700) && !(0, secure_metadata_js_1.exactMode)(directory, 0o500))
|
|
48
49
|
|| (typeof process.getuid === "function" && directory.uid !== process.getuid()))
|
|
49
50
|
throw new Error(`${label} 目录不安全`);
|
|
50
51
|
const fd = node_fs_1.default.openSync(file, node_fs_1.default.constants.O_RDONLY | (node_fs_1.default.constants.O_NOFOLLOW || 0));
|
|
51
52
|
try {
|
|
52
53
|
const stat = node_fs_1.default.fstatSync(fd);
|
|
53
|
-
if (!stat.isFile() || !(0, secure_metadata_js_1.exactMode)(stat, 0o600)
|
|
54
|
+
if (!stat.isFile() || (!(0, secure_metadata_js_1.exactMode)(stat, 0o600) && !(0, secure_metadata_js_1.exactMode)(stat, 0o400))
|
|
54
55
|
|| (typeof process.getuid === "function" && stat.uid !== process.getuid()))
|
|
55
56
|
throw new Error(`${label} 文件不安全`);
|
|
56
57
|
return JSON.parse(node_fs_1.default.readFileSync(fd, "utf8"));
|
|
@@ -34,13 +34,14 @@ export function managedOfficialLarkCli(agent, env, resolve = resolveOfficialLark
|
|
|
34
34
|
}
|
|
35
35
|
function readPrivateJson(file, label) {
|
|
36
36
|
const directory = fs.lstatSync(path.dirname(file));
|
|
37
|
-
if (!directory.isDirectory() || directory.isSymbolicLink()
|
|
37
|
+
if (!directory.isDirectory() || directory.isSymbolicLink()
|
|
38
|
+
|| (!exactMode(directory, 0o700) && !exactMode(directory, 0o500))
|
|
38
39
|
|| (typeof process.getuid === "function" && directory.uid !== process.getuid()))
|
|
39
40
|
throw new Error(`${label} 目录不安全`);
|
|
40
41
|
const fd = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0));
|
|
41
42
|
try {
|
|
42
43
|
const stat = fs.fstatSync(fd);
|
|
43
|
-
if (!stat.isFile() || !exactMode(stat, 0o600)
|
|
44
|
+
if (!stat.isFile() || (!exactMode(stat, 0o600) && !exactMode(stat, 0o400))
|
|
44
45
|
|| (typeof process.getuid === "function" && stat.uid !== process.getuid()))
|
|
45
46
|
throw new Error(`${label} 文件不安全`);
|
|
46
47
|
return JSON.parse(fs.readFileSync(fd, "utf8"));
|
|
@@ -462,11 +462,15 @@ export function removeControlAuthority(larkinHome, expectedToken) {
|
|
|
462
462
|
throw error;
|
|
463
463
|
}
|
|
464
464
|
}
|
|
465
|
-
export function createSupervisorControlServer({ larkinHome, authorityToken, ensureDashboard, }) {
|
|
465
|
+
export function createSupervisorControlServer({ larkinHome, authorityToken, ensureDashboard, onAgentUpserted, }) {
|
|
466
466
|
let socket = "";
|
|
467
467
|
let socketRoot = "";
|
|
468
468
|
let socketIdentity = null;
|
|
469
469
|
const completed = new Map();
|
|
470
|
+
const operationConflict = (operationId, agentId) => ({
|
|
471
|
+
ok: false, operationId, ...(agentId ? { agentId } : {}), code: "operation_conflict",
|
|
472
|
+
error: "operationId 已绑定其他 Agent 或操作",
|
|
473
|
+
});
|
|
470
474
|
let server = null;
|
|
471
475
|
return {
|
|
472
476
|
async start() {
|
|
@@ -490,9 +494,15 @@ export function createSupervisorControlServer({ larkinHome, authorityToken, ensu
|
|
|
490
494
|
let request;
|
|
491
495
|
try {
|
|
492
496
|
request = JSON.parse(line);
|
|
493
|
-
|
|
497
|
+
const isDashboardRecovery = request.operation === "ensure-dashboard";
|
|
498
|
+
const isAgentUpsert = request.operation === "agent-upserted";
|
|
499
|
+
const allowedKeys = isDashboardRecovery
|
|
500
|
+
? ["operationId", "operation", "authorization"]
|
|
501
|
+
: isAgentUpsert ? ["operationId", "operation", "authorization", "agentId"] : [];
|
|
502
|
+
if (!OPERATION_ID.test(String(request.operationId || "")) || (!isDashboardRecovery && !isAgentUpsert)
|
|
494
503
|
|| !AUTHORIZATION.test(String(request.authorization || ""))
|
|
495
|
-
||
|
|
504
|
+
|| (isAgentUpsert && !AGENT_ID.test(String(request.agentId || "")))
|
|
505
|
+
|| Object.keys(request).some((key) => !allowedKeys.includes(key))) {
|
|
496
506
|
throw new Error("invalid supervisor control request");
|
|
497
507
|
}
|
|
498
508
|
const authority = assertSupervisorAuthority(larkinHome, authorityToken);
|
|
@@ -505,17 +515,28 @@ export function createSupervisorControlServer({ larkinHome, authorityToken, ensu
|
|
|
505
515
|
}
|
|
506
516
|
const replay = completed.get(request.operationId);
|
|
507
517
|
if (replay) {
|
|
508
|
-
|
|
518
|
+
const sameRequest = replay.operation === request.operation
|
|
519
|
+
&& replay.agentId === request.agentId;
|
|
520
|
+
const response = sameRequest ? replay.response : operationConflict(request.operationId, request.agentId);
|
|
521
|
+
connection.end(`${JSON.stringify(response)}\n`);
|
|
509
522
|
return;
|
|
510
523
|
}
|
|
511
524
|
let response;
|
|
512
525
|
try {
|
|
513
|
-
|
|
526
|
+
if (request.operation === "agent-upserted") {
|
|
527
|
+
await onAgentUpserted?.(request.agentId);
|
|
528
|
+
response = { ok: true, operationId: request.operationId, state: "agent-recorded" };
|
|
529
|
+
}
|
|
530
|
+
else {
|
|
531
|
+
response = { ok: true, operationId: request.operationId, state: await ensureDashboard() };
|
|
532
|
+
}
|
|
514
533
|
}
|
|
515
534
|
catch (error) {
|
|
516
535
|
response = { ok: false, operationId: request.operationId, error: error instanceof Error ? error.message : String(error) };
|
|
517
536
|
}
|
|
518
|
-
completed.set(request.operationId,
|
|
537
|
+
completed.set(request.operationId, {
|
|
538
|
+
operation: request.operation, agentId: request.agentId, response,
|
|
539
|
+
});
|
|
519
540
|
while (completed.size > 256)
|
|
520
541
|
completed.delete(completed.keys().next().value);
|
|
521
542
|
connection.end(`${JSON.stringify(response)}\n`);
|
|
@@ -812,6 +833,43 @@ async function sendSupervisorRecovery(larkinHome, operationId, timeoutMs) {
|
|
|
812
833
|
});
|
|
813
834
|
});
|
|
814
835
|
}
|
|
836
|
+
async function sendSupervisorAgentUpsert(larkinHome, agentId, operationId, timeoutMs) {
|
|
837
|
+
const authority = assertSupervisorAuthority(larkinHome);
|
|
838
|
+
assertSecureSocketDirectory(authority.socketRoot);
|
|
839
|
+
const socket = authority.supervisorSocketPath;
|
|
840
|
+
const stat = fs.lstatSync(socket);
|
|
841
|
+
if (!stat.isSocket() || stat.isSymbolicLink()
|
|
842
|
+
|| (typeof process.getuid === "function" && stat.uid !== process.getuid())
|
|
843
|
+
|| !notGroupOrWorldAccessible(stat))
|
|
844
|
+
throw new Error("supervisor control socket 不安全");
|
|
845
|
+
return await new Promise((resolve, reject) => {
|
|
846
|
+
const client = net.createConnection(socket);
|
|
847
|
+
const timer = setTimeout(() => { client.destroy(); reject(new Error("supervisor agent upsert control timeout")); }, timeoutMs);
|
|
848
|
+
let input = "";
|
|
849
|
+
client.setEncoding("utf8");
|
|
850
|
+
client.once("error", (error) => { clearTimeout(timer); reject(error); });
|
|
851
|
+
client.once("connect", () => client.write(`${JSON.stringify({
|
|
852
|
+
operationId, operation: "agent-upserted", agentId, authorization: authority.token,
|
|
853
|
+
})}\n`));
|
|
854
|
+
client.on("data", (chunk) => {
|
|
855
|
+
input += chunk;
|
|
856
|
+
const newline = input.indexOf("\n");
|
|
857
|
+
if (newline < 0)
|
|
858
|
+
return;
|
|
859
|
+
clearTimeout(timer);
|
|
860
|
+
client.end();
|
|
861
|
+
try {
|
|
862
|
+
resolve(JSON.parse(input.slice(0, newline)));
|
|
863
|
+
}
|
|
864
|
+
catch (error) {
|
|
865
|
+
reject(error);
|
|
866
|
+
}
|
|
867
|
+
});
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
export async function requestSupervisorAgentUpsert({ larkinHome, agentId, operationId = crypto.randomUUID(), timeoutMs = 30_000, }) {
|
|
871
|
+
return sendSupervisorAgentUpsert(larkinHome, agentId, operationId, timeoutMs);
|
|
872
|
+
}
|
|
815
873
|
export async function requestDashboardRecovery({ larkinHome, operationId = crypto.randomUUID(), timeoutMs = 30_000, }) {
|
|
816
874
|
const deadline = Date.now() + timeoutMs;
|
|
817
875
|
let lastError;
|
package/dist/app/run.mjs
CHANGED
|
@@ -8,7 +8,7 @@ import { fileURLToPath } from "node:url";
|
|
|
8
8
|
import * as larkinConfig from "../platform/config.mjs";
|
|
9
9
|
import { currentProcessMetadata } from "../platform/process-inspect.cjs";
|
|
10
10
|
import { acquireProcessLock, readProcessState, terminateOwnedProcess, waitForProcessExit, } from "../platform/process-state.mjs";
|
|
11
|
-
import { hydrateRuntimeAgent, syncAgentProfile } from "./runtime-agent-config.mjs";
|
|
11
|
+
import { hydrateRuntimeAgent, syncAgentProfile, validateAgentProfile } from "./runtime-agent-config.mjs";
|
|
12
12
|
import { cleanupStaleAgentControlSocket, createSupervisorControlServer, initializeControlAuthority, removeControlAuthority, requestDashboardRecovery, } from "./local-control.mjs";
|
|
13
13
|
import { internalCommandSpec, processCommandToken } from "./internal-command.mjs";
|
|
14
14
|
import { traceProcessBoundary } from "../platform/process-boundary-trace.mjs";
|
|
@@ -80,6 +80,9 @@ export async function main() {
|
|
|
80
80
|
catch (error) {
|
|
81
81
|
return die(error.message);
|
|
82
82
|
}
|
|
83
|
+
// Capture this supervisor's startup membership before setup can persist a new
|
|
84
|
+
// Agent. Unqualified selection must not expand from later durable config.
|
|
85
|
+
const initialNames = new Set(names);
|
|
83
86
|
const before = readProcessState(configDir);
|
|
84
87
|
if (before.supervisor.state === "owned") {
|
|
85
88
|
if (before.dashboard.state !== "owned") {
|
|
@@ -124,11 +127,16 @@ export async function main() {
|
|
|
124
127
|
die("无法取得 supervisor start identity");
|
|
125
128
|
}
|
|
126
129
|
const controlToken = initializeControlAuthority(configDir, { pid: process.pid, processStartToken: supervisorStartToken });
|
|
130
|
+
// This supervisor's successful hot-attaches are the only membership additions
|
|
131
|
+
// that survive a daemon crash. Durable config apply state describes freshness,
|
|
132
|
+
// not which Agents this explicitly selected supervisor owns.
|
|
133
|
+
const hotAttachedNames = new Set();
|
|
127
134
|
let ensureDashboardHandler = () => "starting";
|
|
128
135
|
const supervisorControl = createSupervisorControlServer({
|
|
129
136
|
larkinHome: configDir,
|
|
130
137
|
authorityToken: controlToken,
|
|
131
138
|
ensureDashboard: () => ensureDashboardHandler(),
|
|
139
|
+
onAgentUpserted: (agentId) => { hotAttachedNames.add(agentId); },
|
|
132
140
|
});
|
|
133
141
|
try {
|
|
134
142
|
await supervisorControl.start();
|
|
@@ -141,16 +149,53 @@ export async function main() {
|
|
|
141
149
|
catch { /* best effort */ }
|
|
142
150
|
die(`supervisor control 启动失败:${error instanceof Error ? error.message : String(error)}`);
|
|
143
151
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
152
|
+
let agents = [];
|
|
153
|
+
const serverId = config.serverId;
|
|
154
|
+
if (!serverId)
|
|
155
|
+
die("配置缺少 serverId");
|
|
156
|
+
const runtimeEnv = {
|
|
157
|
+
...process.env,
|
|
158
|
+
LARKIN_HOME: configDir,
|
|
159
|
+
LARKIN_CONFIG_DIR: configDir,
|
|
160
|
+
LARKIN_AGENT_TRANSPORT_MODULE: path.join(HERE, "..", "agent", "agent-transport.cjs"),
|
|
161
|
+
LARKIN_SERVER_ID: String(serverId),
|
|
162
|
+
LARKIN_AGENTS_CONFIG: "[]",
|
|
163
|
+
LARKIN_CONTROL_AUTHORIZATION: controlToken,
|
|
164
|
+
};
|
|
165
|
+
if (argv.includes("--dry-run"))
|
|
166
|
+
runtimeEnv.LARKIN_FEISHU_DRYRUN = "1";
|
|
167
|
+
// Re-read durable config for every daemon launch instead of reusing the startup
|
|
168
|
+
// snapshot: a hot attach updates the child and durable config, but cannot mutate
|
|
169
|
+
// this supervisor's old closure. Membership remains initialNames plus successful
|
|
170
|
+
// hot attaches recorded during this supervisor lifetime.
|
|
171
|
+
const refreshDaemonAgents = (reuseExistingProfiles) => {
|
|
172
|
+
const latest = larkinConfig.loadConfig(process.env);
|
|
173
|
+
if (latest.configDir !== configDir)
|
|
174
|
+
throw new Error("配置目录在 supervisor 运行期间发生变化");
|
|
175
|
+
if (!latest.config.serverId || !Object.keys(latest.config.agents).length)
|
|
176
|
+
throw new Error("配置缺少 serverId/agents");
|
|
177
|
+
const latestNames = [...new Set([...initialNames, ...hotAttachedNames])];
|
|
178
|
+
const refreshed = [];
|
|
179
|
+
for (const name of latestNames) {
|
|
180
|
+
const stored = latest.config.agents[name];
|
|
148
181
|
if (!stored)
|
|
149
182
|
throw new Error(`Agent ${name} 不存在`);
|
|
150
183
|
const agent = hydrateRuntimeAgent(configDir, stored);
|
|
151
|
-
|
|
152
|
-
|
|
184
|
+
if (reuseExistingProfiles)
|
|
185
|
+
validateAgentProfile(agent);
|
|
186
|
+
else
|
|
187
|
+
syncAgentProfile(agent, { ...process.env, LARKIN_CONFIG_DIR: configDir });
|
|
188
|
+
refreshed.push(agent);
|
|
153
189
|
}
|
|
190
|
+
agents = refreshed;
|
|
191
|
+
names = latestNames;
|
|
192
|
+
runtimeEnv.LARKIN_SERVER_ID = String(latest.config.serverId);
|
|
193
|
+
runtimeEnv.LARKIN_AGENTS_CONFIG = JSON.stringify(agents);
|
|
194
|
+
for (const agent of agents)
|
|
195
|
+
traceProcessBoundary(runtimeEnv, "supervisor:daemon-env-prepared", { configDir, agentId: agent.agentId, targetDir: path.join(configDir, "providers", "pi", agent.agentId) });
|
|
196
|
+
};
|
|
197
|
+
try {
|
|
198
|
+
refreshDaemonAgents(false);
|
|
154
199
|
}
|
|
155
200
|
catch (error) {
|
|
156
201
|
await supervisorControl.close().catch(() => { });
|
|
@@ -183,22 +228,6 @@ export async function main() {
|
|
|
183
228
|
launchLock.release();
|
|
184
229
|
die(`dashboard PID ${before.dashboard.pid} 身份无法确认;拒绝替换`);
|
|
185
230
|
}
|
|
186
|
-
const serverId = config.serverId;
|
|
187
|
-
if (!serverId)
|
|
188
|
-
die("配置缺少 serverId");
|
|
189
|
-
const runtimeEnv = {
|
|
190
|
-
...process.env,
|
|
191
|
-
LARKIN_HOME: configDir,
|
|
192
|
-
LARKIN_CONFIG_DIR: configDir,
|
|
193
|
-
LARKIN_AGENT_TRANSPORT_MODULE: path.join(HERE, "..", "agent", "agent-transport.cjs"),
|
|
194
|
-
LARKIN_SERVER_ID: String(serverId),
|
|
195
|
-
LARKIN_AGENTS_CONFIG: JSON.stringify(agents),
|
|
196
|
-
LARKIN_CONTROL_AUTHORIZATION: controlToken,
|
|
197
|
-
};
|
|
198
|
-
if (argv.includes("--dry-run"))
|
|
199
|
-
runtimeEnv.LARKIN_FEISHU_DRYRUN = "1";
|
|
200
|
-
for (const agent of agents)
|
|
201
|
-
traceProcessBoundary(runtimeEnv, "supervisor:daemon-env-prepared", { configDir, agentId: agent.agentId, targetDir: path.join(configDir, "providers", "pi", agent.agentId) });
|
|
202
231
|
let stopping = false;
|
|
203
232
|
let dashboard = null;
|
|
204
233
|
// Set when the daemon-restart path stops the dashboard on purpose (port
|
|
@@ -222,7 +251,19 @@ export async function main() {
|
|
|
222
251
|
// exiting on transient failures (e.g. network/proxy ECONNRESET during Feishu
|
|
223
252
|
// auth). Only when the budget is exhausted does the supervisor exit so the
|
|
224
253
|
// external process manager (launchd) takes over as the final respawn layer.
|
|
254
|
+
let initialRefreshPending = true;
|
|
225
255
|
const launchDaemon = () => {
|
|
256
|
+
if (initialRefreshPending)
|
|
257
|
+
initialRefreshPending = false;
|
|
258
|
+
else
|
|
259
|
+
try {
|
|
260
|
+
refreshDaemonAgents(true);
|
|
261
|
+
}
|
|
262
|
+
catch (error) {
|
|
263
|
+
console.error(`✗ daemon 启动前重新加载配置失败:${error instanceof Error ? error.message : String(error)}`);
|
|
264
|
+
resolveDaemonFinal?.({ code: null, signal: null });
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
226
267
|
daemon = spawn(daemonSpec.command, daemonSpec.args, { env: runtimeEnv, stdio: "inherit" });
|
|
227
268
|
for (const agent of agents)
|
|
228
269
|
traceProcessBoundary(runtimeEnv, "supervisor:daemon-spawned", { configDir, agentId: agent.agentId, targetDir: path.join(configDir, "providers", "pi", agent.agentId), childPid: daemon.pid ?? null });
|
|
@@ -257,7 +298,6 @@ export async function main() {
|
|
|
257
298
|
const oldDashboard = dashboard;
|
|
258
299
|
if (oldDashboard && oldDashboard.exitCode === null && oldDashboard.signalCode === null) {
|
|
259
300
|
intentionalDashboardExit = oldDashboard;
|
|
260
|
-
dashboard = null;
|
|
261
301
|
oldDashboard.kill("SIGTERM");
|
|
262
302
|
// If the old dashboard ignores SIGTERM (observed in production holding
|
|
263
303
|
// the dashboard port for minutes), escalate so the port is released
|
|
@@ -17,6 +17,13 @@ function assertSecureProfileDirectory(directory) {
|
|
|
17
17
|
|| !exactMode(stat, 0o700))
|
|
18
18
|
throw new Error("lark-cli profile 目录不安全");
|
|
19
19
|
}
|
|
20
|
+
function assertValidProfileDirectory(directory) {
|
|
21
|
+
const stat = fs.lstatSync(directory);
|
|
22
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()
|
|
23
|
+
|| (typeof process.getuid === "function" && stat.uid !== process.getuid())
|
|
24
|
+
|| (!exactMode(stat, 0o700) && !exactMode(stat, 0o500)))
|
|
25
|
+
throw new Error("lark-cli profile 目录不安全");
|
|
26
|
+
}
|
|
20
27
|
function captureProfileSnapshot(file) {
|
|
21
28
|
let fd = null;
|
|
22
29
|
try {
|
|
@@ -233,22 +240,44 @@ function runOfficialLarkCliAsync(command, args, env, dependencies) {
|
|
|
233
240
|
});
|
|
234
241
|
});
|
|
235
242
|
}
|
|
243
|
+
function expectedRuntimeCommandShim() {
|
|
244
|
+
const standalone = process.env.LARKIN_STANDALONE === "1";
|
|
245
|
+
const binaryEntry = fileURLToPath(new URL("./binary-entry.mjs", import.meta.url));
|
|
246
|
+
const argumentsPrefix = standalone ? [] : [binaryEntry];
|
|
247
|
+
const command = [process.execPath, ...argumentsPrefix].map(shellQuote).join(" ");
|
|
248
|
+
return `#!/bin/sh\nexec ${command} "$@"\n`;
|
|
249
|
+
}
|
|
236
250
|
export function installRuntimeCommandShims(agent) {
|
|
237
251
|
const stateDir = path.resolve(agent.stateDir);
|
|
238
252
|
const commandDir = path.join(stateDir, "runtime-bin");
|
|
239
253
|
assertSecureRuntimeCommandDirectory(commandDir);
|
|
240
|
-
const
|
|
241
|
-
const
|
|
242
|
-
for (const [name, argumentsPrefix] of [["larkin", standalone ? [] : [binaryEntry]]]) {
|
|
254
|
+
const fileContents = expectedRuntimeCommandShim();
|
|
255
|
+
for (const name of ["larkin"]) {
|
|
243
256
|
const file = path.join(commandDir, name);
|
|
244
257
|
const temporary = path.join(commandDir, `.${name}.${process.pid}.${crypto.randomUUID()}.tmp`);
|
|
245
|
-
|
|
246
|
-
fs.writeFileSync(temporary, `#!/bin/sh\nexec ${command} "$@"\n`, { mode: 0o700, flag: "wx" });
|
|
258
|
+
fs.writeFileSync(temporary, fileContents, { mode: 0o700, flag: "wx" });
|
|
247
259
|
fs.renameSync(temporary, file);
|
|
248
260
|
fs.chmodSync(file, 0o700);
|
|
249
261
|
}
|
|
250
262
|
return commandDir;
|
|
251
263
|
}
|
|
264
|
+
function validateRuntimeCommandShims(agent) {
|
|
265
|
+
const commandDir = path.join(path.resolve(agent.stateDir), "runtime-bin");
|
|
266
|
+
const directory = fs.lstatSync(commandDir);
|
|
267
|
+
if (!directory.isDirectory() || directory.isSymbolicLink()
|
|
268
|
+
|| (typeof process.getuid === "function" && directory.uid !== process.getuid())
|
|
269
|
+
|| (!exactMode(directory, 0o700) && !exactMode(directory, 0o500)))
|
|
270
|
+
throw new Error("Runtime command shim 目录不安全");
|
|
271
|
+
const file = path.join(commandDir, "larkin");
|
|
272
|
+
const stat = fs.lstatSync(file);
|
|
273
|
+
if (!stat.isFile() || stat.isSymbolicLink()
|
|
274
|
+
|| (typeof process.getuid === "function" && stat.uid !== process.getuid())
|
|
275
|
+
|| (!exactMode(stat, 0o700) && !exactMode(stat, 0o500)))
|
|
276
|
+
throw new Error("Runtime command shim 不安全");
|
|
277
|
+
if (fs.readFileSync(file, "utf8") !== expectedRuntimeCommandShim()) {
|
|
278
|
+
throw new Error("Runtime command shim 内容无效");
|
|
279
|
+
}
|
|
280
|
+
}
|
|
252
281
|
export function sourceProjection(agent, env) {
|
|
253
282
|
const configDir = path.resolve(env.LARKIN_CONFIG_DIR || "");
|
|
254
283
|
if (!configDir)
|
|
@@ -299,7 +328,31 @@ function validateSourceProjection(file, agent) {
|
|
|
299
328
|
throw new Error(`Agent ${agent.agentId} lark-channel source projection tenant mismatch`);
|
|
300
329
|
}
|
|
301
330
|
}
|
|
331
|
+
export function validateAgentProfile(agent) {
|
|
332
|
+
// Validate the profile root before resolving any leaf paths. The supervisor's
|
|
333
|
+
// restart fast path must not follow a replaced symlink or unsafe root.
|
|
334
|
+
assertValidProfileDirectory(agent.larkConfigDir);
|
|
335
|
+
const sourceFile = larkChannelSourceConfigPath(agent);
|
|
336
|
+
const workspaceFile = larkChannelWorkspaceConfigPath(agent);
|
|
337
|
+
const source = captureProfileSnapshot(sourceFile);
|
|
338
|
+
if (!source)
|
|
339
|
+
throw new Error(`Agent ${agent.agentId} lark-channel source projection missing`);
|
|
340
|
+
validateSourceProjection(sourceFile, agent);
|
|
341
|
+
const workspace = captureProfileSnapshot(workspaceFile);
|
|
342
|
+
if (!workspace)
|
|
343
|
+
throw new Error(`Agent ${agent.agentId} lark-channel workspace config missing`);
|
|
344
|
+
validateExclusiveBotProfile(workspace, agent);
|
|
345
|
+
validateRuntimeCommandShims(agent);
|
|
346
|
+
assertAgentWorkspaceBound(agent);
|
|
347
|
+
}
|
|
302
348
|
export function syncAgentProfile(agent, env, dependencies = {}) {
|
|
349
|
+
if (!dependencies.forceRebind) {
|
|
350
|
+
try {
|
|
351
|
+
validateAgentProfile(agent);
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
catch { /* absent or stale state requires exactly one bind */ }
|
|
355
|
+
}
|
|
303
356
|
const expected = path.join(path.resolve(env.LARKIN_CONFIG_DIR || ""), "state", "agents", agent.agentId, "lark-cli-config");
|
|
304
357
|
if (path.resolve(agent.larkConfigDir) !== expected)
|
|
305
358
|
throw new Error("lark-cli profile 路径不是 canonical contained 路径");
|
|
@@ -8,7 +8,7 @@ import { createRuntimeHost } from "../runtime/runtime-host.mjs";
|
|
|
8
8
|
import { createAgentStateStore } from "../agent/agent-state-store.mjs";
|
|
9
9
|
import { loadConfig, markConfigApplied, runtimeConfigSignature } from "../platform/config.mjs";
|
|
10
10
|
import { traceProcessBoundary } from "../platform/process-boundary-trace.mjs";
|
|
11
|
-
import { createAgentControlServer } from "./local-control.mjs";
|
|
11
|
+
import { createAgentControlServer, requestSupervisorAgentUpsert } from "./local-control.mjs";
|
|
12
12
|
import { hydrateRuntimeAgent, syncAgentProfile } from "./runtime-agent-config.mjs";
|
|
13
13
|
import { loadTelemetryConfig } from "../platform/telemetry-config.mjs";
|
|
14
14
|
import { telemetrySingleton } from "../platform/telemetry-tracing.mjs";
|
|
@@ -104,6 +104,11 @@ export async function main(env = process.env, overrides = {}) {
|
|
|
104
104
|
// Only the selected profile is synchronized; active profiles and their directory
|
|
105
105
|
// are never quarantined or rebuilt during hot attach.
|
|
106
106
|
await hostShell.upsertAgent(agent);
|
|
107
|
+
const tracked = await requestSupervisorAgentUpsert({
|
|
108
|
+
larkinHome: env.LARKIN_HOME, agentId: agent.agentId, operationId: request.operationId,
|
|
109
|
+
});
|
|
110
|
+
if (!tracked.ok)
|
|
111
|
+
throw new Error(tracked.error || "supervisor 未记录 Agent 热挂载");
|
|
107
112
|
},
|
|
108
113
|
async resetSession(request) {
|
|
109
114
|
const result = await hostShell.resetSession(request.agentId, request.waitReadyMs);
|