larkin 0.4.28 → 0.5.0

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.
Files changed (55) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +5 -9
  3. package/artifacts/release/THIRD_PARTY_NOTICES.txt +751 -4425
  4. package/dist/app/agent-cli.mjs +14 -2
  5. package/dist/app/agent-config.mjs +26 -84
  6. package/dist/app/binary-entry.mjs +0 -20
  7. package/dist/app/cli.mjs +8 -14
  8. package/dist/app/internal-command.cjs +0 -2
  9. package/dist/app/internal-command.mjs +0 -2
  10. package/dist/app/local-control.mjs +16 -3
  11. package/dist/app/runtime-model-directory.mjs +2 -16
  12. package/dist/app/runtime-process.mjs +42 -6
  13. package/dist/app/setup.mjs +52 -57
  14. package/dist/dashboard/dashboard-config-controller.mjs +83 -24
  15. package/dist/dashboard/dashboard-view-model.mjs +4 -7
  16. package/dist/dashboard/web/assets/dashboard.js +1 -1
  17. package/dist/feishu/host-shell.cjs +68 -17
  18. package/dist/feishu/host-shell.mjs +69 -18
  19. package/dist/platform/config.cjs +201 -138
  20. package/dist/platform/config.mjs +199 -138
  21. package/dist/runtime/pi-artifact-provenance.mjs +3 -3
  22. package/dist/runtime/pi-bash-timeout-extension.mjs +4 -5
  23. package/dist/runtime/pi-compaction-recovery.cjs +9 -11
  24. package/dist/runtime/pi-compaction-recovery.mjs +8 -10
  25. package/dist/runtime/pi-extension-api.mjs +1 -0
  26. package/dist/runtime/pi-model-catalog.cjs +7 -8
  27. package/dist/runtime/pi-model-catalog.mjs +7 -8
  28. package/dist/runtime/pi-subagent-injection.mjs +17 -4
  29. package/dist/runtime/runtime-adapters.mjs +41 -38
  30. package/dist/runtime/runtime-host.cjs +197 -20
  31. package/dist/runtime/runtime-host.mjs +198 -21
  32. package/dist/runtime/runtime-model-catalog.cjs +1 -1
  33. package/dist/runtime/runtime-model-catalog.mjs +1 -1
  34. package/dist/runtime/runtime-readiness.cjs +112 -49
  35. package/dist/runtime/runtime-readiness.mjs +103 -49
  36. package/dist/runtime/user-runtime.cjs +7 -24
  37. package/dist/runtime/user-runtime.mjs +7 -23
  38. package/dist/setup/bot-register.mjs +15 -123
  39. package/dist/setup/setup-agent-choice.mjs +48 -74
  40. package/dist/setup/setup-bind.mjs +51 -151
  41. package/dist/setup/setup-binding.mjs +6 -9
  42. package/package.json +3 -2
  43. package/dist/app/pi-auth-cli.mjs +0 -67
  44. package/dist/platform/process-boundary-trace.cjs +0 -108
  45. package/dist/runtime/builtin-pi-assets.cjs +0 -117
  46. package/dist/runtime/builtin-pi-assets.mjs +0 -105
  47. package/dist/runtime/pi-artifact-provenance.cjs +0 -147
  48. package/dist/runtime/pi-distribution-label.cjs +0 -6
  49. package/dist/runtime/pi-distribution-label.mjs +0 -3
  50. package/dist/runtime/pi-inline-extensions.mjs +0 -78
  51. package/dist/runtime/pi-official-auth.mjs +0 -491
  52. package/dist/runtime/pi-profile-migration.cjs +0 -764
  53. package/dist/runtime/pi-profile-migration.mjs +0 -719
  54. package/dist/runtime/pi-provider-config.cjs +0 -331
  55. package/dist/runtime/pi-provider-config.mjs +0 -314
@@ -12,6 +12,8 @@ import { createReminderRoutes } from "../agent/reminder-routes.mjs";
12
12
  import { InteractionStateMachine } from "../agent/interaction-state-machine.mjs";
13
13
  import { issueCallbackProbe, readCallbackCapability } from "../platform/callback-capability.mjs";
14
14
  import { requestAgentUpsert } from "./local-control.mjs";
15
+ import { isUserRuntime } from "../runtime/user-runtime.mjs";
16
+ import { resolveRuntimeExecutable, runtimeInstallNextAction } from "../runtime/runtime-readiness.mjs";
15
17
  export { AGENT_CLI_CAPABILITIES } from "../agent/agent-cli-capabilities.mjs";
16
18
  import { AGENT_CLI_CAPABILITIES } from "../agent/agent-cli-capabilities.mjs";
17
19
  import { CONFIG_CLI_USAGE, CONFIG_CLI_VALUES } from "../agent/config-cli-contract.mjs";
@@ -157,7 +159,7 @@ function agentConfigRequest(argv, env, config, agent, dependencies) {
157
159
  const target = config.agents[targetId];
158
160
  if (!target)
159
161
  throw new Error(`Agent 不存在:${targetId};运行 larkin config show --json 查看可用 Agent`);
160
- const currentDirectory = (runtime = larkinConfig.toUserRuntime(target.runtime, target.piDistribution)) => (dependencies.modelDirectory ?? ((input) => discoverRuntimeModelDirectorySync(input, env)))({
162
+ const currentDirectory = (runtime = larkinConfig.toUserRuntime(target.runtime)) => (dependencies.modelDirectory ?? ((input) => discoverRuntimeModelDirectorySync(input, env)))({
161
163
  agentId: target.agentId, cwd: target.workspaceDir, runtime,
162
164
  });
163
165
  if (operation === "show") {
@@ -173,6 +175,16 @@ function agentConfigRequest(argv, env, config, agent, dependencies) {
173
175
  const runtime = options.positionals[0];
174
176
  if (!runtime || options.positionals.length > 1)
175
177
  throw new Error("用法: larkin config runtime <runtime> [--model <model>] [--agent <App ID>]");
178
+ if (!isUserRuntime(runtime))
179
+ throw new Error(`未知 runtime:${runtime}`);
180
+ const command = runtime === "pi"
181
+ ? env.LARKIN_PI_COMMAND || "pi"
182
+ : runtime === "codex"
183
+ ? env.LARKIN_CODEX_COMMAND || "codex"
184
+ : env.LARKIN_CLAUDE_COMMAND || "claude";
185
+ if (!resolveRuntimeExecutable(command, env)) {
186
+ throw new Error(`${runtime} is not installed;${runtimeInstallNextAction(runtime)}`);
187
+ }
176
188
  const model = options.values.get("--model");
177
189
  if (model && !currentDirectory(runtime).some((item) => item.id === model))
178
190
  throw new Error(`model 不在 ${runtime} 当前目录中:${model}`);
@@ -594,7 +606,7 @@ export function runAgentCli(argv, env = process.env, dependencies = {}) {
594
606
  openId: identity?.open_id ?? identity?.openId ?? null,
595
607
  avatarUrl: identity?.avatar_url ?? identity?.avatarUrl ?? null,
596
608
  runtime: agent.runtime,
597
- runtimeOption: larkinConfig.toUserRuntime(agent.runtime, agent.piDistribution),
609
+ runtimeOption: larkinConfig.toUserRuntime(agent.runtime),
598
610
  model: agent.model, reasoningEffort: agent.effort ?? null,
599
611
  createdAt: agent.createdAt ?? "1970-01-01T00:00:00.000Z",
600
612
  });
@@ -11,9 +11,8 @@ import { isChannelReconnecting, projectAgentReadiness } from "./agent-readiness.
11
11
  import { requestAgentUpsert } from "./local-control.mjs";
12
12
  import * as larkinConfig from "../platform/config.mjs";
13
13
  import { managedOfficialLarkCli } from "./agent-lark-cli-workspace.mjs";
14
- import { assertBuiltinPiAgentDirectory, ownedPiCatalogAgentDir, piAgentDirectory, piCatalogCommandSpec } from "../runtime/pi-provider-config.mjs";
15
- import { RUNTIME_OPTIONS, fromUserRuntime, isUserRuntime, piCatalogDistributionForUserRuntime, toUserRuntime } from "../runtime/user-runtime.mjs";
16
- import { traceProcessBoundary } from "../platform/process-boundary-trace.mjs";
14
+ import { RUNTIME_OPTIONS, fromUserRuntime, isUserRuntime, toUserRuntime } from "../runtime/user-runtime.mjs";
15
+ import { resolveRuntimeExecutable, runtimeInstallNextAction } from "../runtime/runtime-readiness.mjs";
17
16
  function safeDocumentCommentCapabilities(configDir, profile) {
18
17
  try {
19
18
  return {
@@ -52,15 +51,15 @@ const die = (message) => {
52
51
  console.error(" 下一步: larkin config --help");
53
52
  process.exit(1);
54
53
  };
55
- if (!kind || !["agents", "model", "runtime", "effort", "pi-distribution", "chats", "config"].includes(kind)) {
56
- die("用法: larkin agents | larkin config <show|mention|apply> | larkin model [<id>] | larkin runtime [<id>] [--model <id>] | larkin pi-distribution [show|builtin|external] | larkin effort [<level>|clear] | larkin chats [free|strict <oc_id>]");
54
+ if (!kind || !["agents", "model", "runtime", "effort", "chats", "config"].includes(kind)) {
55
+ die("用法: larkin agents | larkin config <show|mention|apply> | larkin model [<id>] | larkin runtime [<id>] [--model <id>] | larkin effort [<level>|clear] | larkin chats [free|strict <oc_id>]");
57
56
  }
58
57
  const allowedValueFlags = {
59
58
  agents: new Set(), model: new Set(["--agent"]), runtime: new Set(["--agent", "--model"]),
60
- effort: new Set(["--agent"]), "pi-distribution": new Set(["--agent", "--snapshot"]), chats: new Set(["--agent"]), config: new Set(["--agent", "--chat"]),
59
+ effort: new Set(["--agent"]), chats: new Set(["--agent"]), config: new Set(["--agent", "--chat"]),
61
60
  };
62
61
  const allowedBooleanFlags = {
63
- agents: new Set(["--json"]), model: new Set(), runtime: new Set(), effort: new Set(), "pi-distribution": new Set(["--import-external-profile"]), chats: new Set(), config: new Set(["--json"]),
62
+ agents: new Set(["--json"]), model: new Set(), runtime: new Set(), effort: new Set(), chats: new Set(), config: new Set(["--json"]),
64
63
  };
65
64
  const parsedValues = new Map();
66
65
  const parsedBooleans = new Set();
@@ -94,7 +93,6 @@ const flagAgent = parsedValues.get("--agent");
94
93
  const flagModel = parsedValues.get("--model");
95
94
  const flagChat = parsedValues.get("--chat");
96
95
  const flagJson = parsedBooleans.has("--json");
97
- const importExternalProfile = parsedBooleans.has("--import-external-profile");
98
96
  const value = positionals[0];
99
97
  function assertOnlyFlags(valueFlags, booleanFlags = []) {
100
98
  const allowedValues = new Set(valueFlags);
@@ -126,11 +124,6 @@ else if (kind === "effort") {
126
124
  if (positionals.length > 1)
127
125
  die("用法: larkin effort [<level>|clear|default] [--agent <App ID>]");
128
126
  }
129
- else if (kind === "pi-distribution") {
130
- assertOnlyFlags(["--agent", "--snapshot"], ["--import-external-profile"]);
131
- if (positionals.length > 1)
132
- die("用法: larkin pi-distribution [show|builtin|external] [--agent <App ID>] [--snapshot <private-file>]");
133
- }
134
127
  else if (kind === "chats") {
135
128
  assertOnlyFlags(["--agent"]);
136
129
  if (![0, 2].includes(positionals.length))
@@ -178,21 +171,6 @@ const loaded = (() => {
178
171
  const { configDir, file, config } = loaded;
179
172
  if (!fs.existsSync(file))
180
173
  die("未找到 Larkin 配置,请运行 larkin setup");
181
- const snapshotFile = parsedValues.get("--snapshot");
182
- if (kind === "pi-distribution" && value === "rollback") {
183
- if (importExternalProfile)
184
- die("--import-external-profile 只允许与 builtin 一起使用");
185
- if (!snapshotFile || flagAgent || positionals.length !== 1)
186
- die("用法: larkin pi-distribution rollback --snapshot <private-file>");
187
- try {
188
- const result = larkinConfig.rollbackConfig(process.env, snapshotFile);
189
- say(JSON.stringify({ ok: true, rolledBackAgent: result.agentId, revision: result.revision }));
190
- }
191
- catch (error) {
192
- die(`回滚失败:${error instanceof Error ? error.message : String(error)}`);
193
- }
194
- process.exit(0);
195
- }
196
174
  if (kind === "agents") {
197
175
  const list = Object.values(config.agents || {});
198
176
  const daemon = readProcessState(config.larkinHome).daemon;
@@ -224,7 +202,7 @@ if (kind === "agents") {
224
202
  agent_id: agent.agentId,
225
203
  name: agent.name,
226
204
  runtime: agent.runtime,
227
- runtimeOption: toUserRuntime(agent.runtime, agent.piDistribution),
205
+ runtimeOption: toUserRuntime(agent.runtime),
228
206
  model: effectiveModel,
229
207
  document_comment: {
230
208
  event: "drive.notice.comment_add_v1",
@@ -295,7 +273,7 @@ if (kind === "agents") {
295
273
  say(` ${agent.name}${agent.name === config.activeAgent ? " [active]" : ""}`);
296
274
  const effectiveModel = status?.session?.runtime === agent.runtime && status.session.model ? status.session.model : agent.model;
297
275
  const effectiveEffort = status?.session?.runtime === agent.runtime && status.session.reasoningEffort ? status.session.reasoningEffort : agent.effort;
298
- say(` runtime=${toUserRuntime(agent.runtime, agent.piDistribution)} model=${effectiveModel}${effectiveModel !== agent.model ? ` stored=${agent.model}` : ""}${effectiveEffort ? ` effort=${effectiveEffort}` : ""}`);
276
+ say(` runtime=${toUserRuntime(agent.runtime)} model=${effectiveModel}${effectiveModel !== agent.model ? ` stored=${agent.model}` : ""}${effectiveEffort ? ` effort=${effectiveEffort}` : ""}`);
299
277
  if (status?.runtimeReadiness) {
300
278
  const current = projectedReadiness.readiness.runtime_ready;
301
279
  const state = status.runtimeReadiness.state === "ready" && !current ? "unavailable" : status.runtimeReadiness.state || "incompatible";
@@ -429,7 +407,7 @@ if (!key) {
429
407
  say(`共 ${keys.length} 个 agent(用 --agent <appId> 查看/修改指定一个):`);
430
408
  for (const candidate of keys) {
431
409
  const item = config.agents[candidate];
432
- say(` ${candidate}${candidate === config.activeAgent ? " [active]" : ""} runtime=${toUserRuntime(item.runtime, item.piDistribution)} model=${item.model}`);
410
+ say(` ${candidate}${candidate === config.activeAgent ? " [active]" : ""} runtime=${toUserRuntime(item.runtime)} model=${item.model}`);
433
411
  }
434
412
  process.exit(0);
435
413
  }
@@ -437,47 +415,21 @@ if (!key) {
437
415
  }
438
416
  const selectedKey = key;
439
417
  const agent = config.agents[selectedKey];
440
- if (kind === "pi-distribution") {
441
- traceProcessBoundary(process.env, "agent-config:pi-distribution-loaded", { configDir, agentId: selectedKey, targetDir: piAgentDirectory(configDir, selectedKey), requested: value || "show", importExternalProfile });
442
- const requested = value || "show";
443
- if (importExternalProfile && requested !== "builtin")
444
- die("--import-external-profile 只允许与 builtin 一起使用");
445
- if (requested === "show") {
446
- say(JSON.stringify({
447
- agentId: selectedKey,
448
- runtime: agent.runtime,
449
- runtimeOption: toUserRuntime(agent.runtime, agent.piDistribution),
450
- piDistribution: agent.piDistribution ?? "external",
451
- }));
452
- process.exit(0);
453
- }
454
- if (requested !== "builtin" && requested !== "external")
455
- die("Pi distribution 只允许 show/builtin/external");
456
- if (agent.runtime !== "pi")
457
- die(`Agent ${selectedKey} 不是 Pi runtime`);
458
- if (!snapshotFile)
459
- die("修改 Pi distribution 必须提供 --snapshot <private-file>");
460
- if (requested === "builtin" && !importExternalProfile) {
461
- try {
462
- assertBuiltinPiAgentDirectory(piAgentDirectory(configDir, selectedKey));
463
- }
464
- catch {
465
- die("内置 Pi provider 尚未为该 Agent 配置;请先运行 larkin setup 完成内置 Pi provider 配置,或使用 --import-external-profile");
466
- }
467
- }
468
- try {
469
- const result = larkinConfig.mutateConfig(process.env, { kind: "set-agent-pi-distribution", agentId: selectedKey, distribution: requested }, { kind: "user" }, {
470
- snapshotFile: snapshotFile, ...(importExternalProfile ? { importExternalProfile: true } : {}),
471
- });
472
- traceProcessBoundary(process.env, "agent-config:pi-distribution-persisted", { configDir, agentId: selectedKey, targetDir: piAgentDirectory(configDir, selectedKey), requested, applyState: result.applyState });
473
- say(JSON.stringify({ ok: true, agentId: selectedKey, piDistribution: requested, revision: result.revision, applyState: result.applyState }));
418
+ function runtimeExecutableName(runtime) {
419
+ if (runtime === "pi")
420
+ return process.env.LARKIN_PI_COMMAND || "pi";
421
+ if (runtime === "codex")
422
+ return process.env.LARKIN_CODEX_COMMAND || "codex";
423
+ return process.env.LARKIN_CLAUDE_COMMAND || "claude";
424
+ }
425
+ if (kind === "runtime" && value) {
426
+ if (!isUserRuntime(value)) {
427
+ console.error(`✗ 未知 runtime:${value}。合法值: ${RUNTIME_OPTIONS.join(", ")}`);
428
+ process.exit(1);
474
429
  }
475
- catch (error) {
476
- if (importExternalProfile)
477
- die("Pi external profile import failed; no secret or private path was disclosed");
478
- die(`Pi distribution 修改失败:${error instanceof Error ? error.message : String(error)}`);
430
+ if (!resolveRuntimeExecutable(runtimeExecutableName(value), process.env)) {
431
+ die(`${value} is not installed;${runtimeInstallNextAction(value)}`);
479
432
  }
480
- process.exit(0);
481
433
  }
482
434
  const catalog = larkinConfig.loadRuntimeModels();
483
435
  if ((["model", "effort"].includes(kind || "") && agent.runtime === "codex") || (kind === "runtime" && value === "codex")) {
@@ -499,28 +451,22 @@ if ((["model", "effort"].includes(kind || "") && agent.runtime === "claude") ||
499
451
  }
500
452
  }
501
453
  let piCatalog = null;
502
- const userRuntime = toUserRuntime(agent.runtime, agent.piDistribution);
503
- const catalogUserRuntime = kind === "runtime" && (value === "pi" || value === "builtin-pi")
504
- ? value
505
- : userRuntime;
454
+ const userRuntime = toUserRuntime(agent.runtime);
506
455
  if ((kind === "model" || kind === "effort" || kind === "runtime") && !value) {
507
456
  say(`agent=${selectedKey} runtime=${userRuntime} model=${agent.model}`);
508
457
  }
509
458
  const needsLivePiCatalog = (["model", "effort"].includes(kind) && agent.runtime === "pi")
510
- || (kind === "runtime" && (value === "pi" || value === "builtin-pi") && Boolean(flagModel));
459
+ || (kind === "runtime" && value === "pi" && Boolean(flagModel));
511
460
  if (needsLivePiCatalog) {
512
461
  try {
513
- const catalogCommand = piCatalogCommandSpec(piCatalogDistributionForUserRuntime(catalogUserRuntime), process.env);
514
462
  piCatalog = await discoverPiModelCatalog({
515
463
  cwd: String(agent.workspaceDir),
516
- agentDir: ownedPiCatalogAgentDir(configDir, selectedKey),
517
- command: catalogCommand.command,
518
- commandArgs: catalogCommand.commandArgs,
464
+ command: process.env.LARKIN_PI_COMMAND || "pi",
519
465
  env: process.env,
520
466
  });
521
467
  }
522
468
  catch (error) {
523
- die(`Pi 模型目录加载失败:${error instanceof Error ? error.message : String(error)}。请先用官方 pi 登录或检查该 Agent 的隔离 provider 目录。`);
469
+ die(`Pi 模型目录加载失败:${error instanceof Error ? error.message : String(error)}。请先运行官方 pi 登录流程。`);
524
470
  }
525
471
  if (!piCatalog)
526
472
  die("Pi 模型目录加载失败");
@@ -693,10 +639,6 @@ else {
693
639
  say("\n切换: larkin runtime <id> [--model <id>]");
694
640
  process.exit(0);
695
641
  }
696
- if (!isUserRuntime(value)) {
697
- console.error(`✗ "${value}" 不是合法 runtime。合法值: ${RUNTIME_OPTIONS.join(", ")}`);
698
- process.exit(1);
699
- }
700
642
  let model = flagModel;
701
643
  if (model) {
702
644
  const items = catalog[catalogRuntimeKey(value)];
@@ -49,26 +49,6 @@ async function dispatchInternal(mode, rest) {
49
49
  case "lark-channel-secret":
50
50
  await (await import("./lark-channel-secret.mjs")).main(process.env);
51
51
  return;
52
- case "pi-rpc": {
53
- const { prepareBuiltinPiPackageAssets } = await import("../runtime/builtin-pi-assets.mjs");
54
- prepareBuiltinPiPackageAssets();
55
- process.title = "pi-rpc";
56
- process.env.PI_CODING_AGENT = "true";
57
- process.emitWarning = (() => { });
58
- // The Pi package's normal Bun CLI registers OAuth loaders before starting.
59
- // Larkin invokes its RPC entrypoint directly, so do the same here; otherwise
60
- // an OAuth-backed compact falls back to a dynamic module import unavailable in
61
- // a compiled standalone binary.
62
- const { registerBunOAuthFlows } = await import("@earendil-works/pi-ai/bun-oauth");
63
- registerBunOAuthFlows();
64
- const { main: piMain } = await import("@earendil-works/pi-coding-agent");
65
- const { invokeBuiltinPiRpc } = await import("../runtime/pi-inline-extensions.mjs");
66
- await invokeBuiltinPiRpc(piMain, rest);
67
- return;
68
- }
69
- case "pi-auth":
70
- await (await import("./pi-auth-cli.mjs")).main(rest, process.env);
71
- return;
72
52
  case "telemetry":
73
53
  await (await import("./telemetry.mjs")).main(rest, process.env);
74
54
  return;
package/dist/app/cli.mjs CHANGED
@@ -29,11 +29,9 @@ const routes = {
29
29
  model: ["agent-config", "model"],
30
30
  runtime: ["agent-config", "runtime"],
31
31
  effort: ["agent-config", "effort"],
32
- "pi-distribution": ["agent-config", "pi-distribution"],
33
32
  chats: ["agent-config", "chats"],
34
33
  config: ["agent-config", "config"],
35
34
  session: ["session-cli"],
36
- "pi-auth": ["pi-auth"],
37
35
  telemetry: ["telemetry"],
38
36
  };
39
37
  const runtimeAgentAuthority = typeof process.env.LARKIN_AGENT_ID === "string"
@@ -49,8 +47,8 @@ if (runtimeAgentAuthority && command === "comment")
49
47
  const commandHelp = {
50
48
  start: `Usage: larkin start [--agent <App ID> | --agents <App ID,...>]
51
49
  Start one foreground supervisor for the daemon and local dashboard, or reuse it.`,
52
- setup: `Usage: larkin setup [--tenant feishu|lark] [--runtime <builtin-pi|pi|codex|claude>] [--provider <id> --api-key <key>]
53
- Run setup to create or connect a bot, configure its Agent, and attach it. Choose Feishu or Lark before the authorization QR (--tenant; default feishu). Interactive terminals keep the guided flow; Agent-driven (non-TTY) runs default to builtin-pi and need --provider/--api-key (or --runtime pi).`,
50
+ setup: `Usage: larkin setup [--tenant feishu|lark] [--runtime <pi|codex|claude>]
51
+ Run setup to create or connect a bot, configure its Agent, and attach it. Choose Feishu or Lark before the authorization QR (--tenant; default feishu). Interactive terminals list pi, Codex, and Claude Code with installed / not installed status. Non-TTY runs require --runtime and fail if that executable is missing.`,
54
52
  status: `Usage: larkin status [--json]
55
53
  Show Agent configuration, bot identity, credentials, and connection status. Use --json for readiness automation.`,
56
54
  agents: `Usage: larkin agents [--json]
@@ -58,10 +56,7 @@ List every configured Agent and its current local status. Use --json for daemon/
58
56
  model: `Usage: larkin model [<model>] [--agent <App ID>]
59
57
  Show or change an Agent model.`,
60
58
  runtime: `Usage: larkin runtime [<runtime>] [--agent <App ID>] [--model <model>]
61
- Show or change an Agent runtime. User-facing ids: pi | builtin-pi | codex | claude.`,
62
- "pi-distribution": `Usage: larkin pi-distribution [show|builtin|external] [--agent <App ID>] [--snapshot <private-file>] [--import-external-profile]
63
- larkin pi-distribution rollback --snapshot <private-file>
64
- Show or change one Pi Agent distribution. builtin requires configured provider state, or explicitly imports the external Pi 0.84.2 profile with --import-external-profile; all changes support config-lock CAS rollback.`,
59
+ Show or change an Agent runtime. User-facing ids: pi | codex | claude. A missing executable is rejected and does not write config.`,
65
60
  effort: `Usage: larkin effort [<level>|clear|default] [--agent <App ID>]
66
61
  Show or change an Agent reasoning effort; clear/default restores the Runtime default.`,
67
62
  chats: `Usage: larkin chats [--agent <App ID>]
@@ -83,9 +78,6 @@ Credentials, internal paths, serverId, activeAgent, and raw config are never exp
83
78
  session: `Usage: larkin session reset --agent <App ID> --json [--wait-ready <seconds>]
84
79
  larkin session recover --agent <App ID> --reason context-overflow --json [--wait-ready <seconds>]
85
80
  Reset replaces one idle, zero-backlog Runtime session. Recover is an explicit operator-only context-window recovery that preserves and replays canonical Inbox deliveries.`,
86
- "pi-auth": `Usage: larkin pi-auth status [--agent <App ID>] [--json]
87
- larkin pi-auth logout <provider> [--agent <App ID>]
88
- Show non-sensitive official Pi credential metadata or remove one target provider credential.`,
89
81
  comment: `Usage: larkin comment reply --message-id <doc_comment_message_id> --text '<reply>' --json
90
82
  Reply as the Runtime-bound Bot to the exact cloud-document comment locator supplied by canonical Inbox. Retrying the same body is idempotent; a different body appends a follow-up reply to the same comment.`,
91
83
  telemetry: `Usage: larkin telemetry <status|export|import|flush>
@@ -110,9 +102,10 @@ if (!runtimeAgentCommand && command === "config" && ["runtime", "model", "effort
110
102
  // At a user terminal, unregistered non-flag commands keep the legacy lark-cli passthrough.
111
103
  // Inside an Agent Runtime, unknown Feishu command groups enter the same Larkin-owned
112
104
  // identity and freshness AOP before delegation to the verified global official CLI.
105
+ const retiredPublicCommands = new Set(["pi-auth", "pi-distribution"]);
113
106
  const wantsHelp = command === "help" || command === "--help" || command === "-h"
114
107
  || (!runtimeAgentAuthority && command.startsWith("-"));
115
- if (!routes[command] && !wantsHelp) {
108
+ if (!routes[command] && !wantsHelp && !retiredPublicCommands.has(command)) {
116
109
  routes[command] = runtimeAgentAuthority ? ["lark-cli", command] : ["lark", command];
117
110
  }
118
111
  if (!routes[command]) {
@@ -130,7 +123,6 @@ Usage: larkin <command>
130
123
  config Inspect effective config/source, edit mention inheritance, or explicitly apply runtime changes
131
124
  session reset Replace one idle, zero-backlog Agent Runtime session for a fresh scenario
132
125
  session recover Explicitly recover a context-overflowed session and replay retained Inbox deliveries
133
- pi-auth Show non-sensitive built-in Pi auth status or logout one provider
134
126
  comment reply Reply to the exact cloud-document comment bound by a polled Inbox message
135
127
  telemetry Inspect, export, import, or flush the durable OpenTelemetry trace queue
136
128
  <lark-cli 命令组> im/docs/wiki/drive 等 lark-cli 命令原样转发,机器人身份已锁定(如 larkin im +chat-list)
@@ -144,7 +136,9 @@ Process persistence is managed externally. larkin start stays in the foreground
144
136
  }
145
137
  const [mode, ...presetArguments] = routes[command];
146
138
  const childSpec = internalCommandSpec(mode, [...presetArguments, ...rest]);
147
- const child = spawn(childSpec.command, childSpec.args, { stdio: "inherit" });
139
+ const child = spawn(childSpec.command, childSpec.args, {
140
+ stdio: ["inherit", "inherit", "inherit"],
141
+ });
148
142
  // Package-manager bin shims add a wrapper process. Forward terminal signals so the actual command
149
143
  // does not become an orphan that keeps the machine lock or Feishu connection.
150
144
  const signalForwarders = new Map();
@@ -24,8 +24,6 @@ exports.INTERNAL_MODES = [
24
24
  "setup-bind",
25
25
  "grant-scopes",
26
26
  "lark-channel-secret",
27
- "pi-rpc",
28
- "pi-auth",
29
27
  "telemetry",
30
28
  ];
31
29
  exports.PROCESS_COMMAND_TOKENS = Object.freeze({
@@ -15,8 +15,6 @@ export const INTERNAL_MODES = [
15
15
  "setup-bind",
16
16
  "grant-scopes",
17
17
  "lark-channel-secret",
18
- "pi-rpc",
19
- "pi-auth",
20
18
  "telemetry",
21
19
  ];
22
20
  export const PROCESS_COMMAND_TOKENS = Object.freeze({
@@ -14,6 +14,7 @@ const WINDOWS_SOCKET_BINDING = { device: "0", inode: "0", owner: "0", changeTime
14
14
  const AGENT_ID = /^cli_[A-Za-z0-9]+$/;
15
15
  const OPERATION_ID = /^[A-Za-z0-9_-]{8,128}$/;
16
16
  const AUTHORIZATION = /^[A-Za-z0-9_-]{43,128}$/;
17
+ const CONFIG_SIGNATURE = /^sha256:[a-f0-9]{64}$/;
17
18
  const UNIX_SOCKET_PATH_MAX_BYTES = process.platform === "darwin" ? 103 : 107;
18
19
  function recoveryErrorText(code) {
19
20
  switch (code) {
@@ -159,9 +160,13 @@ function parseRequest(line) {
159
160
  if (value.waitReadyMs !== undefined) {
160
161
  throw new Error("invalid session reset waitReadyMs");
161
162
  }
162
- if (Object.keys(value).some((key) => !["operationId", "agentId", "authorization"].includes(key))) {
163
+ if (Object.keys(value).some((key) => !["operationId", "agentId", "authorization", "expectedSignature"].includes(key))) {
163
164
  throw new Error("agent control request 包含未知字段");
164
165
  }
166
+ if (value.expectedSignature !== undefined
167
+ && (typeof value.expectedSignature !== "string" || !CONFIG_SIGNATURE.test(value.expectedSignature))) {
168
+ throw new Error("invalid agent upsert expectedSignature");
169
+ }
165
170
  return value;
166
171
  }
167
172
  function atomicWritePrivateJson(file, value) {
@@ -741,7 +746,11 @@ export function createAgentControlServer({ larkinHome, authorityToken, upsert, r
741
746
  }
742
747
  const execute = async () => {
743
748
  try {
744
- await upsert({ operationId: upsertRequest.operationId, agentId: upsertRequest.agentId });
749
+ await upsert({
750
+ operationId: upsertRequest.operationId,
751
+ agentId: upsertRequest.agentId,
752
+ ...(upsertRequest.expectedSignature ? { expectedSignature: upsertRequest.expectedSignature } : {}),
753
+ });
745
754
  return { ok: true, operationId: upsertRequest.operationId, agentId: upsertRequest.agentId };
746
755
  }
747
756
  catch (error) {
@@ -926,7 +935,11 @@ async function requestAgentControl({ larkinHome, timeoutMs = 30_000, request, })
926
935
  }
927
936
  export async function requestAgentUpsert(input) {
928
937
  return requestAgentControl({ larkinHome: input.larkinHome, timeoutMs: input.timeoutMs,
929
- request: { operationId: input.operationId ?? crypto.randomUUID(), agentId: input.agentId } });
938
+ request: {
939
+ operationId: input.operationId ?? crypto.randomUUID(),
940
+ agentId: input.agentId,
941
+ ...(input.expectedSignature ? { expectedSignature: input.expectedSignature } : {}),
942
+ } });
930
943
  }
931
944
  export async function requestSessionReset({ larkinHome, agentId, waitReadyMs = 30_000, }) {
932
945
  return requestAgentControl({
@@ -2,12 +2,9 @@
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
- import { loadConfig, resolveConfigDir } from "../platform/config.mjs";
6
5
  import { discoverClaudeModelCatalog } from "../runtime/claude-model-catalog.mjs";
7
6
  import { discoverPiModelCatalog } from "../runtime/pi-model-catalog.mjs";
8
7
  import { discoverCodexModelCatalog } from "../runtime/codex-model-catalog.mjs";
9
- import { ownedPiCatalogAgentDir, piCatalogCommandSpec } from "../runtime/pi-provider-config.mjs";
10
- import { piCatalogDistributionForUserRuntime } from "../runtime/user-runtime.mjs";
11
8
  function readTestModelDirectory(env) {
12
9
  const fixture = env?.LARKIN_TEST_RUNTIME_MODEL_DIRECTORY_FILE;
13
10
  if (!fixture)
@@ -37,22 +34,11 @@ export async function discoverRuntimeModelDirectory(input) {
37
34
  ...catalog.models.map(({ id, label, supportedReasoningEfforts }) => ({ id, label, supportedReasoningEfforts })),
38
35
  ];
39
36
  }
40
- if (input.runtime === "pi" || input.runtime === "builtin-pi") {
37
+ if (input.runtime === "pi") {
41
38
  const env = input.env ?? process.env;
42
- const agentId = input.agentId || "";
43
- if (!agentId)
44
- throw new Error("runtime model directory requires agentId for Pi");
45
- const configDir = resolveConfigDir(env);
46
- const { config } = loadConfig(env);
47
- const agent = config.agents[agentId];
48
- if (!agent)
49
- throw new Error(`unknown agent: ${agentId}`);
50
- const catalogCommand = piCatalogCommandSpec(piCatalogDistributionForUserRuntime(input.runtime), env);
51
39
  const catalog = await discoverPiModelCatalog({
52
40
  cwd: input.cwd,
53
- agentDir: ownedPiCatalogAgentDir(configDir, agentId),
54
- command: catalogCommand.command,
55
- commandArgs: catalogCommand.commandArgs,
41
+ command: env.LARKIN_PI_COMMAND ?? process.env.LARKIN_PI_COMMAND ?? "pi",
56
42
  env,
57
43
  });
58
44
  if (!catalog.effectiveModel)
@@ -6,22 +6,58 @@ import { ContextPromptBuilder } from "../agent/context-prompt.mjs";
6
6
  import { createNativeRuntimeAdapter } from "../runtime/runtime-adapters.mjs";
7
7
  import { createRuntimeHost } from "../runtime/runtime-host.mjs";
8
8
  import { createAgentStateStore } from "../agent/agent-state-store.mjs";
9
- import { loadConfig, markConfigApplied, runtimeConfigSignature } from "../platform/config.mjs";
9
+ import { loadConfig, markConfigApplied, runtimeConfigSignature, withConfigMutationLock, withConfigMutationLockAsync, } from "../platform/config.mjs";
10
10
  import { traceProcessBoundary } from "../platform/process-boundary-trace.mjs";
11
11
  import { createAgentControlServer, requestSupervisorAgentUpsert } from "./local-control.mjs";
12
- import { hydrateRuntimeAgent, syncAgentProfile } from "./runtime-agent-config.mjs";
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";
15
15
  import { packageVersion } from "../platform/build-info.mjs";
16
- export function loadAndSyncRuntimeAgent(env, agentId, dependencies = {}) {
16
+ function readRuntimeAgent(env, agentId, expectedSignature) {
17
17
  const loaded = loadConfig(env);
18
18
  const stored = loaded.config.agents[agentId];
19
19
  if (!stored)
20
20
  throw new Error(`Agent ${agentId} 不存在于 canonical config`);
21
+ if (expectedSignature) {
22
+ const current = runtimeConfigSignature(loaded.config, agentId);
23
+ if (current !== expectedSignature) {
24
+ throw new Error("配置在 apply 期间发生变化;未热加载已切换的 Agent");
25
+ }
26
+ }
27
+ return { loaded, stored };
28
+ }
29
+ function hydrateAndSyncRuntimeAgent(env, loaded, stored, dependencies) {
21
30
  const agent = hydrateRuntimeAgent(loaded.configDir, stored);
22
31
  syncAgentProfile(agent, { ...env, LARKIN_CONFIG_DIR: loaded.configDir }, dependencies);
23
32
  return agent;
24
33
  }
34
+ export function loadAndSyncRuntimeAgent(env, agentId, dependencies = {}, options = {}) {
35
+ const snapshot = readRuntimeAgent(env, agentId, options.expectedSignature);
36
+ options.afterCanonicalValidate?.();
37
+ if (!options.expectedSignature) {
38
+ return hydrateAndSyncRuntimeAgent(env, snapshot.loaded, snapshot.stored, dependencies);
39
+ }
40
+ return withConfigMutationLock(env, () => {
41
+ const locked = readRuntimeAgent(env, agentId, options.expectedSignature);
42
+ return hydrateAndSyncRuntimeAgent(env, locked.loaded, locked.stored, dependencies);
43
+ });
44
+ }
45
+ /** 签名校验贯穿实际 Host upsert;并发 config mutation 不能热挂载已失效的 snapshot。 */
46
+ export async function applyRuntimeAgentUpsert(env, agentId, dependencies = {}, options = {}, upsert = () => undefined) {
47
+ const snapshot = readRuntimeAgent(env, agentId, options.expectedSignature);
48
+ options.afterCanonicalValidate?.();
49
+ if (!options.expectedSignature) {
50
+ const agent = hydrateAndSyncRuntimeAgent(env, snapshot.loaded, snapshot.stored, dependencies);
51
+ await upsert(agent);
52
+ return agent;
53
+ }
54
+ return withConfigMutationLockAsync(env, async () => {
55
+ const locked = readRuntimeAgent(env, agentId, options.expectedSignature);
56
+ const agent = hydrateAndSyncRuntimeAgent(env, locked.loaded, locked.stored, dependencies);
57
+ await upsert(agent);
58
+ return agent;
59
+ });
60
+ }
25
61
  export async function markConfigAppliedAfterRuntimeReady(env, runningAgents, runtimeReady) {
26
62
  await runtimeReady;
27
63
  try {
@@ -29,7 +65,6 @@ export async function markConfigAppliedAfterRuntimeReady(env, runningAgents, run
29
65
  for (const running of runningAgents) {
30
66
  const current = loaded.config.agents[running.agentId];
31
67
  if (!current || current.runtime !== running.runtime || current.model !== running.model
32
- || (current.piDistribution || "external") !== (running.piDistribution || "external")
33
68
  || (current.effort || null) !== (running.effort || null))
34
69
  continue;
35
70
  markConfigApplied(env, running.agentId, runtimeConfigSignature(loaded.config, running.agentId));
@@ -100,10 +135,11 @@ export async function main(env = process.env, overrides = {}) {
100
135
  larkinHome: env.LARKIN_HOME,
101
136
  authorityToken: env.LARKIN_CONTROL_AUTHORIZATION,
102
137
  async upsert(request) {
103
- const agent = loadAndSyncRuntimeAgent(env, request.agentId);
138
+ const agent = await applyRuntimeAgentUpsert(env, request.agentId, {}, {
139
+ ...(request.expectedSignature ? { expectedSignature: request.expectedSignature } : {}),
140
+ }, (candidate) => hostShell.upsertAgent(candidate));
104
141
  // Only the selected profile is synchronized; active profiles and their directory
105
142
  // are never quarantined or rebuilt during hot attach.
106
- await hostShell.upsertAgent(agent);
107
143
  const tracked = await requestSupervisorAgentUpsert({
108
144
  larkinHome: env.LARKIN_HOME, agentId: agent.agentId, operationId: request.operationId,
109
145
  });