larkin 0.5.16 → 0.5.18
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/CHANGELOG.md +4 -0
- package/README.md +11 -0
- package/dist/agent/config-cli-contract.cjs +3 -1
- package/dist/agent/config-cli-contract.mjs +3 -1
- package/dist/agent/context-prompt.cjs +7 -7
- package/dist/agent/context-prompt.mjs +7 -7
- package/dist/app/agent-cli.mjs +19 -2
- package/dist/app/agent-config.mjs +33 -4
- package/dist/app/cli.mjs +16 -0
- package/dist/app/lark-cli.mjs +77 -119
- package/dist/app/lark.mjs +14 -0
- package/dist/app/runtime-agent-config.mjs +15 -4
- package/dist/app/runtime-context.mjs +17 -0
- package/dist/dashboard/dashboard-config-controller.mjs +11 -0
- package/dist/dashboard/web/assets/dashboard.js +15 -15
- package/dist/feishu/host-shell.cjs +11 -1
- package/dist/feishu/host-shell.mjs +12 -2
- package/dist/feishu/im-freshness-adapter.cjs +2 -2
- package/dist/feishu/im-freshness-adapter.mjs +2 -2
- package/dist/platform/config.cjs +85 -2
- package/dist/platform/config.mjs +83 -2
- package/dist/runtime/runtime-host.cjs +112 -9
- package/dist/runtime/runtime-host.mjs +112 -9
- package/package.json +1 -1
package/dist/app/lark-cli.mjs
CHANGED
|
@@ -190,7 +190,7 @@ export function classifyLarkCliCommand(argv) {
|
|
|
190
190
|
return { kind: "passthrough" };
|
|
191
191
|
const protectedPaths = protectedOperations(argv);
|
|
192
192
|
if (command === "larkin-draft")
|
|
193
|
-
return { kind: "denied", reason: "larkin-draft
|
|
193
|
+
return { kind: "denied", reason: "larkin-draft 已移除;请使用受 Runtime 身份与目标绑定保护的普通写命令" };
|
|
194
194
|
if (exactPath(parsed.commandArgv, ["im", "+messages-send"]))
|
|
195
195
|
return uniqueProtectedOperation(protectedPaths, "send")
|
|
196
196
|
? (parsed.flags.has("--thread-id")
|
|
@@ -223,21 +223,21 @@ export function classifyLarkCliCommand(argv) {
|
|
|
223
223
|
if (exactPath(parsed.commandArgv, ["im", "messages", "create"]) || exactPath(parsed.commandArgv, ["im", "messages", "reply"])) {
|
|
224
224
|
const expected = parsed.commandArgv[2] === "create" ? "raw-create" : "raw-reply";
|
|
225
225
|
return uniqueProtectedOperation(protectedPaths, expected)
|
|
226
|
-
? { kind: "denied", reason: "该原始 IM 写入口会旁路 target
|
|
226
|
+
? { kind: "denied", reason: "该原始 IM 写入口会旁路 Runtime target binding;请使用 +messages-send/+messages-reply" }
|
|
227
227
|
: noncanonicalProtectedDecision();
|
|
228
228
|
}
|
|
229
229
|
if (["forward", "merge_forward", "urgent_phone", "urgent_sms"]
|
|
230
230
|
.some((operation) => exactPath(parsed.commandArgv, ["im", "messages", operation]))) {
|
|
231
231
|
const expected = `raw-${parsed.commandArgv[2]}`;
|
|
232
232
|
return uniqueProtectedOperation(protectedPaths, expected)
|
|
233
|
-
? { kind: "denied", reason: "该 IM 写入口无法建立 target
|
|
233
|
+
? { kind: "denied", reason: "该 IM 写入口无法建立 Runtime target binding;请先用 larkin inbox poll 读取目标,再使用受保护的 +messages-send/+messages-reply" }
|
|
234
234
|
: noncanonicalProtectedDecision();
|
|
235
235
|
}
|
|
236
236
|
if (["forward", "merge_forward"]
|
|
237
237
|
.some((operation) => exactPath(parsed.commandArgv, ["im", "threads", operation]))) {
|
|
238
238
|
const expected = `thread-${parsed.commandArgv[2]}`;
|
|
239
239
|
return uniqueProtectedOperation(protectedPaths, expected)
|
|
240
|
-
? { kind: "denied", reason: "该 IM forwarding 入口无法建立 target
|
|
240
|
+
? { kind: "denied", reason: "该 IM forwarding 入口无法建立 Runtime target binding;请先用 larkin inbox poll 读取目标,再使用受保护的 +messages-send/+messages-reply" }
|
|
241
241
|
: noncanonicalProtectedDecision();
|
|
242
242
|
}
|
|
243
243
|
if (command === "api")
|
|
@@ -347,8 +347,7 @@ function runCommentReply(argv, privateEnv, io, dependencies, store, agentId) {
|
|
|
347
347
|
const target = targetKey ? parseDocumentCommentTarget(targetKey) : null;
|
|
348
348
|
if (!target)
|
|
349
349
|
throw new Error("comment reply 无法从当前 Agent Inbox 绑定文档评论 locator;先 poll 该消息且不得跨 Agent/评论回复");
|
|
350
|
-
|
|
351
|
-
throw new Error("comment reply 需要先 poll 当前 document-comment target 的最新 Inbox 消息");
|
|
350
|
+
const staleCommentTarget = !store.inboxTargetIsFresh(targetKey);
|
|
352
351
|
const digest = createHash("sha256").update(input.text).digest("hex");
|
|
353
352
|
const ledgerKey = `${input.messageId}::${digest}`;
|
|
354
353
|
const claim = store.mutateJson("freshnessState", { version: 1, cursors: {} }, (state) => {
|
|
@@ -421,6 +420,13 @@ function runCommentReply(argv, privateEnv, io, dependencies, store, agentId) {
|
|
|
421
420
|
});
|
|
422
421
|
}
|
|
423
422
|
const committedWrite = !result.error && result.status === 0;
|
|
423
|
+
if (committedWrite && staleCommentTarget) {
|
|
424
|
+
io.stderr(`${JSON.stringify({
|
|
425
|
+
larkin_notice: "freshness",
|
|
426
|
+
target: targetKey,
|
|
427
|
+
hint: "Newer Inbox messages exist. Re-read the document comment context with larkin before sending a follow-up. Do not resend the same content.",
|
|
428
|
+
})}\n`);
|
|
429
|
+
}
|
|
424
430
|
const currentReminder = committedWrite
|
|
425
431
|
? store.resolveCurrentReminders().filter((reminder) => reminder.deliveryTarget === targetKey).at(-1) ?? null
|
|
426
432
|
: null;
|
|
@@ -464,9 +470,9 @@ function definitiveProviderRejection(result) {
|
|
|
464
470
|
}
|
|
465
471
|
return false;
|
|
466
472
|
}
|
|
467
|
-
function callNative(argv, env, io, dependencies) {
|
|
473
|
+
function callNative(argv, env, io, dependencies, timeoutMs) {
|
|
468
474
|
const native = dependencies.nativeCommand;
|
|
469
|
-
const result = (dependencies.spawn ?? spawnSync)(native?.command ?? resolveOfficialLarkCli({ spawn: dependencies.spawn, env }).command, [...(native?.argsPrefix ?? []), ...argv], { encoding: "utf8", env: { ...process.env, ...env } });
|
|
475
|
+
const result = (dependencies.spawn ?? spawnSync)(native?.command ?? resolveOfficialLarkCli({ spawn: dependencies.spawn, env }).command, [...(native?.argsPrefix ?? []), ...argv], { encoding: "utf8", env: { ...process.env, ...env }, ...(timeoutMs ? { timeout: timeoutMs } : {}) });
|
|
470
476
|
return result;
|
|
471
477
|
}
|
|
472
478
|
function spawnNative(argv, env, io, dependencies) {
|
|
@@ -524,7 +530,7 @@ function guardedTarget(decision, argv, store) {
|
|
|
524
530
|
const chatId = policyFlagValue(argv, "--chat-id");
|
|
525
531
|
const userId = policyFlagValue(argv, "--user-id");
|
|
526
532
|
if (!chatId || userId) {
|
|
527
|
-
throw new Error("Runtime +messages-send
|
|
533
|
+
throw new Error("Runtime +messages-send 必须使用已确认的 --chat-id;--user-id 无法建立受限 target binding");
|
|
528
534
|
}
|
|
529
535
|
return feishuImTarget(`chat:${chatId}`);
|
|
530
536
|
}
|
|
@@ -533,7 +539,7 @@ function guardedTarget(decision, argv, store) {
|
|
|
533
539
|
throw new Error(`${decision.operation} 写入缺少 --message-id`);
|
|
534
540
|
const target = store.resolveInboxMessageTarget(messageId);
|
|
535
541
|
if (!target)
|
|
536
|
-
throw new Error(`无法从 Inbox 状态确定 ${messageId} 的 target;先 poll 对应消息,禁止旁路
|
|
542
|
+
throw new Error(`无法从 Inbox 状态确定 ${messageId} 的 target;先 poll 对应消息,禁止旁路 target binding`);
|
|
537
543
|
return feishuImTarget(target);
|
|
538
544
|
}
|
|
539
545
|
function recallLedgerEntry(store, messageId) {
|
|
@@ -889,22 +895,62 @@ function intentId(target, argv) {
|
|
|
889
895
|
.update(JSON.stringify([target, argv])).digest("hex");
|
|
890
896
|
return `larkin-${fingerprint.slice(0, 32)}`;
|
|
891
897
|
}
|
|
892
|
-
function
|
|
898
|
+
function emitFreshnessNotice(io, input) {
|
|
893
899
|
io.stderr(`${JSON.stringify({
|
|
894
|
-
|
|
895
|
-
identity: "bot",
|
|
896
|
-
error: { type: input.subtype === "freshness_conflict" ? "conflict" : "unavailable", subtype: input.subtype,
|
|
897
|
-
...(input.reason ? { message: input.reason } : {}) },
|
|
900
|
+
larkin_notice: "freshness",
|
|
898
901
|
target: input.target,
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
+
newer_messages: input.messages.length,
|
|
903
|
+
latest_message_ids: input.messages.map((message) => message.message_id),
|
|
904
|
+
hint: "Newer messages exist. Re-read this target with larkin and send a short follow-up only if they change your answer. Do not resend the same content.",
|
|
902
905
|
})}\n`);
|
|
903
906
|
}
|
|
904
907
|
function freshnessGeneration(env) {
|
|
905
908
|
return typeof env.LARKIN_RUNTIME_OBSERVATION_GENERATION === "string" && env.LARKIN_RUNTIME_OBSERVATION_GENERATION
|
|
906
909
|
? env.LARKIN_RUNTIME_OBSERVATION_GENERATION : "external";
|
|
907
910
|
}
|
|
911
|
+
// The post-write observation is advisory: cap it tightly so success is not
|
|
912
|
+
// held hostage by a slow provider history response.
|
|
913
|
+
const FRESHNESS_PROBE_TIMEOUT_MS = 500;
|
|
914
|
+
function emitSoftFreshnessNotice(target, targetKey, seen, ownMessageId, env, io, dependencies, store) {
|
|
915
|
+
let timedOut = false;
|
|
916
|
+
try {
|
|
917
|
+
const gated = evaluateFreshness({
|
|
918
|
+
seen,
|
|
919
|
+
adapter: feishuImFreshnessAdapter,
|
|
920
|
+
probe: () => {
|
|
921
|
+
const result = callNative(probeArgv(target), env, io, dependencies, FRESHNESS_PROBE_TIMEOUT_MS);
|
|
922
|
+
const error = result.error;
|
|
923
|
+
timedOut = error?.code === "ETIMEDOUT"
|
|
924
|
+
|| error?.message.includes("ETIMEDOUT") === true
|
|
925
|
+
|| (result.status === null && result.signal === "SIGTERM");
|
|
926
|
+
return parseHistory(result, target, true);
|
|
927
|
+
},
|
|
928
|
+
});
|
|
929
|
+
if (gated.status === "unavailable") {
|
|
930
|
+
if (timedOut)
|
|
931
|
+
io.stderr(`lark-cli: freshness observation timed out after ${FRESHNESS_PROBE_TIMEOUT_MS}ms; no read position was recorded\n`);
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
if (!seen && gated.status === "fresh" && gated.current) {
|
|
935
|
+
// A first authoritative observation establishes a quiet baseline from
|
|
936
|
+
// the provider's latest 20-message window, including messages that
|
|
937
|
+
// arrived while this write was in flight. This does not claim the Agent
|
|
938
|
+
// read those messages; it only enables change detection on later writes.
|
|
939
|
+
store.mergeFreshnessCursor(targetKey, gated.current, mergeFeishuImCursor, freshnessGeneration(env));
|
|
940
|
+
}
|
|
941
|
+
if (gated.status !== "conflict")
|
|
942
|
+
return;
|
|
943
|
+
store.mergeFreshnessCursor(targetKey, gated.current, mergeFeishuImCursor, freshnessGeneration(env));
|
|
944
|
+
const newer = gated.context.filter((message) => message.message_id !== ownMessageId);
|
|
945
|
+
if (newer.length)
|
|
946
|
+
emitFreshnessNotice(io, { target: targetKey, messages: newer });
|
|
947
|
+
}
|
|
948
|
+
catch {
|
|
949
|
+
// freshness 仅作提示:观察失败绝不能改变已完成写入的结果。
|
|
950
|
+
if (timedOut)
|
|
951
|
+
io.stderr(`lark-cli: freshness observation timed out after ${FRESHNESS_PROBE_TIMEOUT_MS}ms; no read position was recorded\n`);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
908
954
|
function rawFlagValue(argv, flag) {
|
|
909
955
|
const nativeArgv = nativeArgvBeforeBoundary(argv);
|
|
910
956
|
const direct = nativeArgv.indexOf(flag);
|
|
@@ -1066,23 +1112,6 @@ function writeResponseMessage(result) {
|
|
|
1066
1112
|
return null;
|
|
1067
1113
|
}
|
|
1068
1114
|
}
|
|
1069
|
-
function observeSuccessfulWrite(result, target, targetKey, store, generation) {
|
|
1070
|
-
if (result.error || result.status !== 0)
|
|
1071
|
-
return false;
|
|
1072
|
-
try {
|
|
1073
|
-
const candidate = writeResponseMessage(result);
|
|
1074
|
-
if (!candidate)
|
|
1075
|
-
return false;
|
|
1076
|
-
const snapshot = parseHistory({ ...result, stdout: JSON.stringify({ ok: true, data: { messages: [candidate] } }) }, target);
|
|
1077
|
-
const cursor = feishuImFreshnessAdapter.cursor(snapshot);
|
|
1078
|
-
if (cursor)
|
|
1079
|
-
store.mergeFreshnessCursor(targetKey, cursor, mergeFeishuImCursor, generation);
|
|
1080
|
-
return cursor !== null;
|
|
1081
|
-
}
|
|
1082
|
-
catch {
|
|
1083
|
-
return false;
|
|
1084
|
-
}
|
|
1085
|
-
}
|
|
1086
1115
|
function emitNativeResult(result, io) {
|
|
1087
1116
|
if (result.stdout)
|
|
1088
1117
|
io.stdout(result.stdout);
|
|
@@ -1125,35 +1154,6 @@ function emitDuplicatedRecall(io, messageId, target) {
|
|
|
1125
1154
|
})}\n`);
|
|
1126
1155
|
return 0;
|
|
1127
1156
|
}
|
|
1128
|
-
function emitCommittedUnverified(result, io, input) {
|
|
1129
|
-
let providerResponse;
|
|
1130
|
-
try {
|
|
1131
|
-
providerResponse = JSON.parse(result.stdout || "");
|
|
1132
|
-
}
|
|
1133
|
-
catch {
|
|
1134
|
-
providerResponse = { raw_stdout: result.stdout || "" };
|
|
1135
|
-
}
|
|
1136
|
-
const providerDocument = providerResponse && typeof providerResponse === "object" && !Array.isArray(providerResponse)
|
|
1137
|
-
? providerResponse
|
|
1138
|
-
: { provider_response: providerResponse };
|
|
1139
|
-
io.stdout(`${JSON.stringify({
|
|
1140
|
-
...providerDocument,
|
|
1141
|
-
ok: true,
|
|
1142
|
-
committed: true,
|
|
1143
|
-
verified: false,
|
|
1144
|
-
cursor_advanced: false,
|
|
1145
|
-
target: input.target,
|
|
1146
|
-
verification: {
|
|
1147
|
-
status: "unverified",
|
|
1148
|
-
subtype: "post_write_unverified",
|
|
1149
|
-
message: input.reason,
|
|
1150
|
-
...(input.current ? { current_cursor: input.current } : {}),
|
|
1151
|
-
...(input.messages ? { unseen_messages: input.messages } : {}),
|
|
1152
|
-
},
|
|
1153
|
-
...(result.stderr ? { provider_stderr_present: true } : {}),
|
|
1154
|
-
})}\n`);
|
|
1155
|
-
return 0;
|
|
1156
|
-
}
|
|
1157
1157
|
export function runLarkCli(argv, env = process.env, dependencies = {}) {
|
|
1158
1158
|
const io = dependencies.io ?? defaultIo();
|
|
1159
1159
|
let effectiveArgv = argv;
|
|
@@ -1235,20 +1235,6 @@ export function runLarkCli(argv, env = process.env, dependencies = {}) {
|
|
|
1235
1235
|
const targetKey = serializeFeishuImTarget(target);
|
|
1236
1236
|
const generation = freshnessGeneration(privateEnv);
|
|
1237
1237
|
const seen = store.readFreshnessCursor(targetKey, generation);
|
|
1238
|
-
const gated = evaluateFreshness({
|
|
1239
|
-
seen,
|
|
1240
|
-
adapter: feishuImFreshnessAdapter,
|
|
1241
|
-
probe: () => parseHistory(callNative(probeArgv(target), privateEnv, io, nativeDependencies), target, true),
|
|
1242
|
-
});
|
|
1243
|
-
if (gated.status === "unavailable") {
|
|
1244
|
-
emitFreshnessError(io, { subtype: "freshness_unavailable", target: targetKey, reason: gated.reason });
|
|
1245
|
-
return 3;
|
|
1246
|
-
}
|
|
1247
|
-
if (gated.status === "conflict") {
|
|
1248
|
-
emitFreshnessError(io, { subtype: "freshness_conflict", target: targetKey, current: gated.current, messages: gated.context });
|
|
1249
|
-
store.mergeFreshnessCursor(targetKey, gated.current, mergeFeishuImCursor, generation);
|
|
1250
|
-
return 3;
|
|
1251
|
-
}
|
|
1252
1238
|
if (decision.operation === "recall") {
|
|
1253
1239
|
const claim = claimRecall(store, recallMessageId, targetKey);
|
|
1254
1240
|
if (claim === "deleted")
|
|
@@ -1258,16 +1244,15 @@ export function runLarkCli(argv, env = process.env, dependencies = {}) {
|
|
|
1258
1244
|
}
|
|
1259
1245
|
const recallWrite = callNative(botArgv(effectiveArgv, "", decision), privateEnv, io, nativeDependencies);
|
|
1260
1246
|
if (!recallWrite.error && recallWrite.status === 0) {
|
|
1261
|
-
|
|
1262
|
-
messages: gated.snapshot.messages.filter((message) => message.message_id !== recallMessageId),
|
|
1263
|
-
};
|
|
1264
|
-
const predictedCursor = feishuImFreshnessAdapter.cursor(predictedSnapshot);
|
|
1265
|
-
finalizeSuccessfulRecall(store, recallMessageId, targetKey, predictedCursor, generation);
|
|
1247
|
+
finalizeSuccessfulRecall(store, recallMessageId, targetKey, null, generation);
|
|
1266
1248
|
}
|
|
1267
1249
|
else if (definitiveProviderRejection(recallWrite)) {
|
|
1268
1250
|
finalizeFailedRecall(store, recallMessageId, targetKey);
|
|
1269
1251
|
}
|
|
1270
|
-
|
|
1252
|
+
const code = emitNativeResult(recallWrite, io);
|
|
1253
|
+
if (code === 0)
|
|
1254
|
+
emitSoftFreshnessNotice(target, targetKey, seen, undefined, privateEnv, io, nativeDependencies, store);
|
|
1255
|
+
return code;
|
|
1271
1256
|
}
|
|
1272
1257
|
if (decision.operation === "urgent-app") {
|
|
1273
1258
|
const userIds = assertUrgentAppPreconditions(effectiveArgv, urgent.message, target, agent.feishuAppId);
|
|
@@ -1291,7 +1276,10 @@ export function runLarkCli(argv, env = process.env, dependencies = {}) {
|
|
|
1291
1276
|
})}\n`);
|
|
1292
1277
|
return 2;
|
|
1293
1278
|
}
|
|
1294
|
-
|
|
1279
|
+
const code = emitNativeResult(write, io);
|
|
1280
|
+
if (code === 0)
|
|
1281
|
+
emitSoftFreshnessNotice(target, targetKey, seen, writeResponseMessage(write)?.message_id, privateEnv, io, nativeDependencies, store);
|
|
1282
|
+
return code;
|
|
1295
1283
|
}
|
|
1296
1284
|
const writeMessage = writeResponseMessage(write);
|
|
1297
1285
|
const deliveryTarget = decision.operation === "send"
|
|
@@ -1331,40 +1319,10 @@ export function runLarkCli(argv, env = process.env, dependencies = {}) {
|
|
|
1331
1319
|
io.stderr(`lark-cli: reminder delivery audit failed: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
1332
1320
|
}
|
|
1333
1321
|
const duplicate = memo.duplicate;
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
}
|
|
1339
|
-
if (!write.error && write.status === 0 && !observeSuccessfulWrite(write, target, targetKey, store, generation)) {
|
|
1340
|
-
const responseMessage = writeResponseMessage(write);
|
|
1341
|
-
try {
|
|
1342
|
-
const postSnapshot = parseHistory(callNative(probeArgv(target), privateEnv, io, nativeDependencies), target, true);
|
|
1343
|
-
const postCursor = feishuImFreshnessAdapter.cursor(postSnapshot);
|
|
1344
|
-
const unseenAfterWrite = feishuImFreshnessAdapter.unseen(gated.current, postSnapshot);
|
|
1345
|
-
const confirmedOwnWrite = responseMessage && postCursor
|
|
1346
|
-
&& unseenAfterWrite.some((message) => message.message_id === responseMessage.message_id)
|
|
1347
|
-
&& unseenAfterWrite.every((message) => message.message_id === responseMessage.message_id);
|
|
1348
|
-
if (!confirmedOwnWrite) {
|
|
1349
|
-
return emitCommittedUnverified(write, io, {
|
|
1350
|
-
target: targetKey,
|
|
1351
|
-
...(postCursor ? { current: postCursor } : {}),
|
|
1352
|
-
messages: unseenAfterWrite,
|
|
1353
|
-
reason: responseMessage
|
|
1354
|
-
? "provider write succeeded but bounded post-write probe found an additional concurrent update; cursor was not advanced"
|
|
1355
|
-
: "provider write succeeded without a message id/revision and bounded post-write probe could not identify the write; cursor was not advanced",
|
|
1356
|
-
});
|
|
1357
|
-
}
|
|
1358
|
-
store.mergeFreshnessCursor(targetKey, postCursor, mergeFeishuImCursor, generation);
|
|
1359
|
-
}
|
|
1360
|
-
catch {
|
|
1361
|
-
return emitCommittedUnverified(write, io, {
|
|
1362
|
-
target: targetKey,
|
|
1363
|
-
reason: "provider write succeeded but bounded post-write confirmation was unavailable; cursor was not advanced",
|
|
1364
|
-
});
|
|
1365
|
-
}
|
|
1366
|
-
}
|
|
1367
|
-
return emitNativeResult(write, io);
|
|
1322
|
+
const code = duplicate ? emitDuplicatedWrite(write, io, { target: targetKey }) : emitNativeResult(write, io);
|
|
1323
|
+
if (code === 0)
|
|
1324
|
+
emitSoftFreshnessNotice(target, targetKey, seen, writeMessage?.message_id, privateEnv, io, nativeDependencies, store);
|
|
1325
|
+
return code;
|
|
1368
1326
|
}
|
|
1369
1327
|
catch (error) {
|
|
1370
1328
|
io.stderr(`lark-cli: ${error instanceof Error ? error.message : String(error)}\n`);
|
package/dist/app/lark.mjs
CHANGED
|
@@ -4,6 +4,20 @@ import { spawn } from "node:child_process";
|
|
|
4
4
|
import * as larkinConfig from "../platform/config.mjs";
|
|
5
5
|
import { assessPassthrough, PASSTHROUGH_USAGE } from "../feishu/lark-passthrough.mjs";
|
|
6
6
|
import { managedOfficialLarkCli } from "./agent-lark-cli-workspace.mjs";
|
|
7
|
+
import { isLarkinRuntimeContext } from "./runtime-context.mjs";
|
|
8
|
+
const runtimeScopedCommands = new Set(["inbox", "reminder", "interaction", "profile", "comment"]);
|
|
9
|
+
const requestedCommand = process.argv.slice(2).find((argument) => !argument.startsWith("-"));
|
|
10
|
+
const runtimeContext = isLarkinRuntimeContext(process.env);
|
|
11
|
+
if (!process.env.LARKIN_AGENT_ID && runtimeContext) {
|
|
12
|
+
console.error(`larkin: Runtime Agent authority is missing for "${requestedCommand ?? "lark-cli"}" (LARKIN_AGENT_ID is required). `
|
|
13
|
+
+ "Runtime commands cannot fall back to the active Agent.");
|
|
14
|
+
process.exit(2);
|
|
15
|
+
}
|
|
16
|
+
if (!process.env.LARKIN_AGENT_ID && requestedCommand && runtimeScopedCommands.has(requestedCommand)) {
|
|
17
|
+
console.error(`larkin: Runtime Agent authority is missing for "${requestedCommand}" (LARKIN_AGENT_ID is required). `
|
|
18
|
+
+ "Agent-scoped commands cannot fall back to the active Agent.");
|
|
19
|
+
process.exit(2);
|
|
20
|
+
}
|
|
7
21
|
const decision = assessPassthrough(process.argv.slice(2), process.env);
|
|
8
22
|
if (!decision.ok) {
|
|
9
23
|
console.error(`larkin: ${decision.reason}`);
|
|
@@ -240,18 +240,29 @@ function runOfficialLarkCliAsync(command, args, env, dependencies) {
|
|
|
240
240
|
});
|
|
241
241
|
});
|
|
242
242
|
}
|
|
243
|
-
function expectedRuntimeCommandShim() {
|
|
243
|
+
function expectedRuntimeCommandShim(agent) {
|
|
244
244
|
const standalone = process.env.LARKIN_STANDALONE === "1";
|
|
245
245
|
const binaryEntry = fileURLToPath(new URL("./binary-entry.mjs", import.meta.url));
|
|
246
246
|
const argumentsPrefix = standalone ? [] : [binaryEntry];
|
|
247
247
|
const command = [process.execPath, ...argumentsPrefix].map(shellQuote).join(" ");
|
|
248
|
-
|
|
248
|
+
const stateDir = path.resolve(agent.stateDir);
|
|
249
|
+
const configDir = path.resolve(stateDir, "../../..");
|
|
250
|
+
return [
|
|
251
|
+
"#!/bin/sh",
|
|
252
|
+
"export LARKIN_RUNTIME=1",
|
|
253
|
+
`export LARKIN_AGENT_ID=${shellQuote(agent.agentId)}`,
|
|
254
|
+
`export LARKIN_STATE_DIR=${shellQuote(stateDir)}`,
|
|
255
|
+
`export LARKIN_CONFIG_DIR=${shellQuote(configDir)}`,
|
|
256
|
+
`export LARKIN_HOME=${shellQuote(configDir)}`,
|
|
257
|
+
`exec ${command} "$@"`,
|
|
258
|
+
"",
|
|
259
|
+
].join("\n");
|
|
249
260
|
}
|
|
250
261
|
export function installRuntimeCommandShims(agent) {
|
|
251
262
|
const stateDir = path.resolve(agent.stateDir);
|
|
252
263
|
const commandDir = path.join(stateDir, "runtime-bin");
|
|
253
264
|
assertSecureRuntimeCommandDirectory(commandDir);
|
|
254
|
-
const fileContents = expectedRuntimeCommandShim();
|
|
265
|
+
const fileContents = expectedRuntimeCommandShim(agent);
|
|
255
266
|
for (const name of ["larkin"]) {
|
|
256
267
|
const file = path.join(commandDir, name);
|
|
257
268
|
const temporary = path.join(commandDir, `.${name}.${process.pid}.${crypto.randomUUID()}.tmp`);
|
|
@@ -274,7 +285,7 @@ function validateRuntimeCommandShims(agent) {
|
|
|
274
285
|
|| (typeof process.getuid === "function" && stat.uid !== process.getuid())
|
|
275
286
|
|| (!exactMode(stat, 0o700) && !exactMode(stat, 0o500)))
|
|
276
287
|
throw new Error("Runtime command shim 不安全");
|
|
277
|
-
if (fs.readFileSync(file, "utf8") !== expectedRuntimeCommandShim()) {
|
|
288
|
+
if (fs.readFileSync(file, "utf8") !== expectedRuntimeCommandShim(agent)) {
|
|
278
289
|
throw new Error("Runtime command shim 内容无效");
|
|
279
290
|
}
|
|
280
291
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
function hasRuntimeBin(pathValue) {
|
|
2
|
+
if (!pathValue)
|
|
3
|
+
return false;
|
|
4
|
+
return pathValue.split(process.platform === "win32" ? ";" : ":")
|
|
5
|
+
.some((entry) => /(?:^|[\\/])runtime-bin$/.test(entry));
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Runtime markers are deliberately independent of LARKIN_AGENT_ID. They keep a
|
|
9
|
+
* damaged Runtime environment from falling through to a terminal's activeAgent
|
|
10
|
+
* selection when its per-Agent identity has been stripped.
|
|
11
|
+
*/
|
|
12
|
+
export function isLarkinRuntimeContext(env = process.env) {
|
|
13
|
+
return env.LARKIN_RUNTIME === "1"
|
|
14
|
+
|| Boolean(env.LARKIN_RUNTIME_OBSERVATION_GENERATION)
|
|
15
|
+
|| Boolean(env.LARKIN_STATE_DIR)
|
|
16
|
+
|| hasRuntimeBin(env.PATH);
|
|
17
|
+
}
|
|
@@ -402,11 +402,17 @@ function dashboardMutation(value) {
|
|
|
402
402
|
"set-chat-mention": ["operation", "agentId", "chatId", "value"], "set-agent-runtime": ["operation", "agentId", "runtime", "model"],
|
|
403
403
|
"set-agent-model": ["operation", "agentId", "model"], "set-agent-effort": ["operation", "agentId", "effort"],
|
|
404
404
|
"set-global-inbox-audit": ["operation", "enabled", "intervalMs"], "set-agent-inbox-audit": ["operation", "agentId", "enabled", "intervalMs"],
|
|
405
|
+
"set-global-processing-eye": ["operation", "enabled"], "set-agent-processing-eye": ["operation", "agentId", "enabled"],
|
|
405
406
|
};
|
|
406
407
|
if (!allowed[operation] || Object.keys(value).some((key) => !allowed[operation].includes(key)))
|
|
407
408
|
throw new Error("unsupported operation");
|
|
408
409
|
if (operation === "set-global-mention")
|
|
409
410
|
return { kind: operation, value: String(value.value) };
|
|
411
|
+
if (operation === "set-global-processing-eye") {
|
|
412
|
+
if (typeof value.enabled !== "boolean")
|
|
413
|
+
throw new Error("invalid processing-eye enabled");
|
|
414
|
+
return { kind: operation, enabled: value.enabled };
|
|
415
|
+
}
|
|
410
416
|
if (operation === "set-global-inbox-audit") {
|
|
411
417
|
if (value.enabled !== undefined && typeof value.enabled !== "boolean")
|
|
412
418
|
throw new Error("invalid inbox-audit enabled");
|
|
@@ -422,6 +428,11 @@ function dashboardMutation(value) {
|
|
|
422
428
|
throw new Error("invalid inbox-audit interval");
|
|
423
429
|
return { kind: operation, agentId, ...(value.enabled !== undefined ? { enabled: value.enabled } : {}), ...(value.intervalMs !== undefined ? { intervalMs: value.intervalMs } : {}) };
|
|
424
430
|
}
|
|
431
|
+
if (operation === "set-agent-processing-eye") {
|
|
432
|
+
if (value.enabled !== "inherit" && typeof value.enabled !== "boolean")
|
|
433
|
+
throw new Error("invalid processing-eye enabled");
|
|
434
|
+
return { kind: operation, agentId, enabled: value.enabled };
|
|
435
|
+
}
|
|
425
436
|
if (operation === "set-agent-mention")
|
|
426
437
|
return { kind: operation, agentId, value: String(value.value) };
|
|
427
438
|
if (operation === "set-chat-mention")
|