larkin 0.4.27 → 0.4.29

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 (34) hide show
  1. package/dist/app/agent-cli.mjs +4 -2
  2. package/dist/app/agent-config.mjs +45 -29
  3. package/dist/app/cli.mjs +16 -6
  4. package/dist/app/local-control.mjs +16 -3
  5. package/dist/app/pi-auth-cli.mjs +227 -17
  6. package/dist/app/runtime-model-directory.mjs +3 -2
  7. package/dist/app/runtime-process.mjs +42 -5
  8. package/dist/app/setup.mjs +15 -14
  9. package/dist/dashboard/dashboard-config-controller.mjs +145 -13
  10. package/dist/dashboard/dashboard-view-model.mjs +3 -2
  11. package/dist/dashboard/web/assets/dashboard.css +1 -1
  12. package/dist/dashboard/web/assets/dashboard.js +15 -15
  13. package/dist/feishu/host-shell.cjs +68 -16
  14. package/dist/feishu/host-shell.mjs +69 -17
  15. package/dist/platform/config.cjs +67 -16
  16. package/dist/platform/config.mjs +57 -16
  17. package/dist/runtime/pi-official-auth.cjs +594 -0
  18. package/dist/runtime/pi-official-auth.mjs +53 -0
  19. package/dist/runtime/pi-provider-config.cjs +26 -3
  20. package/dist/runtime/pi-provider-config.mjs +22 -2
  21. package/dist/runtime/pi-provider-login.mjs +240 -0
  22. package/dist/runtime/runtime-adapters.mjs +24 -4
  23. package/dist/runtime/runtime-host.cjs +215 -16
  24. package/dist/runtime/runtime-host.mjs +216 -17
  25. package/dist/runtime/runtime-model-catalog.cjs +4 -3
  26. package/dist/runtime/runtime-model-catalog.mjs +4 -3
  27. package/dist/runtime/runtime-readiness.cjs +123 -3
  28. package/dist/runtime/runtime-readiness.mjs +116 -4
  29. package/dist/runtime/user-runtime.cjs +51 -0
  30. package/dist/runtime/user-runtime.mjs +41 -0
  31. package/dist/setup/bot-register.mjs +3 -3
  32. package/dist/setup/setup-agent-choice.mjs +10 -9
  33. package/dist/setup/setup-bind.mjs +2 -2
  34. package/package.json +1 -1
@@ -157,7 +157,7 @@ function agentConfigRequest(argv, env, config, agent, dependencies) {
157
157
  const target = config.agents[targetId];
158
158
  if (!target)
159
159
  throw new Error(`Agent 不存在:${targetId};运行 larkin config show --json 查看可用 Agent`);
160
- const currentDirectory = (runtime = target.runtime) => (dependencies.modelDirectory ?? ((input) => discoverRuntimeModelDirectorySync(input, env)))({
160
+ const currentDirectory = (runtime = larkinConfig.toUserRuntime(target.runtime, target.piDistribution)) => (dependencies.modelDirectory ?? ((input) => discoverRuntimeModelDirectorySync(input, env)))({
161
161
  agentId: target.agentId, cwd: target.workspaceDir, runtime,
162
162
  });
163
163
  if (operation === "show") {
@@ -593,7 +593,9 @@ export function runAgentCli(argv, env = process.env, dependencies = {}) {
593
593
  kind: "agent", id: agent.agentId, isSelf: true, name, displayName: name,
594
594
  openId: identity?.open_id ?? identity?.openId ?? null,
595
595
  avatarUrl: identity?.avatar_url ?? identity?.avatarUrl ?? null,
596
- runtime: agent.runtime, model: agent.model, reasoningEffort: agent.effort ?? null,
596
+ runtime: agent.runtime,
597
+ runtimeOption: larkinConfig.toUserRuntime(agent.runtime, agent.piDistribution),
598
+ model: agent.model, reasoningEffort: agent.effort ?? null,
597
599
  createdAt: agent.createdAt ?? "1970-01-01T00:00:00.000Z",
598
600
  });
599
601
  return 0;
@@ -11,7 +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, piDistributionLabel } from "../runtime/pi-provider-config.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";
15
16
  import { traceProcessBoundary } from "../platform/process-boundary-trace.mjs";
16
17
  function safeDocumentCommentCapabilities(configDir, profile) {
17
18
  try {
@@ -223,6 +224,7 @@ if (kind === "agents") {
223
224
  agent_id: agent.agentId,
224
225
  name: agent.name,
225
226
  runtime: agent.runtime,
227
+ runtimeOption: toUserRuntime(agent.runtime, agent.piDistribution),
226
228
  model: effectiveModel,
227
229
  document_comment: {
228
230
  event: "drive.notice.comment_add_v1",
@@ -293,7 +295,7 @@ if (kind === "agents") {
293
295
  say(` ${agent.name}${agent.name === config.activeAgent ? " [active]" : ""}`);
294
296
  const effectiveModel = status?.session?.runtime === agent.runtime && status.session.model ? status.session.model : agent.model;
295
297
  const effectiveEffort = status?.session?.runtime === agent.runtime && status.session.reasoningEffort ? status.session.reasoningEffort : agent.effort;
296
- say(` runtime=${agent.runtime} model=${effectiveModel}${effectiveModel !== agent.model ? ` stored=${agent.model}` : ""}${effectiveEffort ? ` effort=${effectiveEffort}` : ""}`);
298
+ say(` runtime=${toUserRuntime(agent.runtime, agent.piDistribution)} model=${effectiveModel}${effectiveModel !== agent.model ? ` stored=${agent.model}` : ""}${effectiveEffort ? ` effort=${effectiveEffort}` : ""}`);
297
299
  if (status?.runtimeReadiness) {
298
300
  const current = projectedReadiness.readiness.runtime_ready;
299
301
  const state = status.runtimeReadiness.state === "ready" && !current ? "unavailable" : status.runtimeReadiness.state || "incompatible";
@@ -427,7 +429,7 @@ if (!key) {
427
429
  say(`共 ${keys.length} 个 agent(用 --agent <appId> 查看/修改指定一个):`);
428
430
  for (const candidate of keys) {
429
431
  const item = config.agents[candidate];
430
- say(` ${candidate}${candidate === config.activeAgent ? " [active]" : ""} runtime=${item.runtime} model=${item.model}`);
432
+ say(` ${candidate}${candidate === config.activeAgent ? " [active]" : ""} runtime=${toUserRuntime(item.runtime, item.piDistribution)} model=${item.model}`);
431
433
  }
432
434
  process.exit(0);
433
435
  }
@@ -441,7 +443,12 @@ if (kind === "pi-distribution") {
441
443
  if (importExternalProfile && requested !== "builtin")
442
444
  die("--import-external-profile 只允许与 builtin 一起使用");
443
445
  if (requested === "show") {
444
- say(JSON.stringify({ agentId: selectedKey, runtime: agent.runtime, piDistribution: agent.piDistribution ?? "external" }));
446
+ say(JSON.stringify({
447
+ agentId: selectedKey,
448
+ runtime: agent.runtime,
449
+ runtimeOption: toUserRuntime(agent.runtime, agent.piDistribution),
450
+ piDistribution: agent.piDistribution ?? "external",
451
+ }));
445
452
  process.exit(0);
446
453
  }
447
454
  if (requested !== "builtin" && requested !== "external")
@@ -455,7 +462,9 @@ if (kind === "pi-distribution") {
455
462
  assertBuiltinPiAgentDirectory(piAgentDirectory(configDir, selectedKey));
456
463
  }
457
464
  catch {
458
- die("内置 Pi provider 尚未为该 Agent 配置;请先运行 larkin setup 完成内置 Pi provider 配置,或使用 --import-external-profile");
465
+ die(agent.piDistribution === "builtin"
466
+ ? "内置 Pi provider 尚未为该 Agent 配置。请使用 Dashboard Provider Credentials 或 larkin pi-auth login。"
467
+ : "内置 Pi provider 尚未为该 Agent 配置。");
459
468
  }
460
469
  }
461
470
  try {
@@ -492,11 +501,18 @@ if ((["model", "effort"].includes(kind || "") && agent.runtime === "claude") ||
492
501
  }
493
502
  }
494
503
  let piCatalog = null;
495
- if (agent.runtime === "pi" || (kind === "runtime" && value === "pi")) {
496
- if (kind === "model" && !value)
497
- say(`发行版: ${piDistributionLabel(agent.piDistribution)}`);
504
+ const userRuntime = toUserRuntime(agent.runtime, agent.piDistribution);
505
+ const catalogUserRuntime = kind === "runtime" && (value === "pi" || value === "builtin-pi")
506
+ ? value
507
+ : userRuntime;
508
+ if ((kind === "model" || kind === "effort" || kind === "runtime") && !value) {
509
+ say(`agent=${selectedKey} runtime=${userRuntime} model=${agent.model}`);
510
+ }
511
+ const needsLivePiCatalog = (["model", "effort"].includes(kind) && agent.runtime === "pi")
512
+ || (kind === "runtime" && (value === "pi" || value === "builtin-pi") && Boolean(flagModel));
513
+ if (needsLivePiCatalog) {
498
514
  try {
499
- const catalogCommand = piCatalogCommandSpec(agent.piDistribution, process.env);
515
+ const catalogCommand = piCatalogCommandSpec(piCatalogDistributionForUserRuntime(catalogUserRuntime), process.env);
500
516
  piCatalog = await discoverPiModelCatalog({
501
517
  cwd: String(agent.workspaceDir),
502
518
  agentDir: ownedPiCatalogAgentDir(configDir, selectedKey),
@@ -513,10 +529,14 @@ if (agent.runtime === "pi" || (kind === "runtime" && value === "pi")) {
513
529
  const loadedPiCatalog = piCatalog;
514
530
  for (const diagnostic of loadedPiCatalog.diagnostics)
515
531
  console.error(`Pi diagnostic: ${diagnostic}`);
516
- catalog.pi = [{ id: "default", label: `default: ${loadedPiCatalog.effectiveModel}` }, ...loadedPiCatalog.models];
532
+ const piModels = [{ id: "default", label: `default: ${loadedPiCatalog.effectiveModel}` }, ...loadedPiCatalog.models];
533
+ catalog.pi = piModels;
534
+ }
535
+ function catalogRuntimeKey(runtime) {
536
+ return isUserRuntime(runtime) ? fromUserRuntime(runtime).runtime : runtime;
517
537
  }
518
538
  function listModels(runtime, current) {
519
- const items = catalog[runtime];
539
+ const items = catalog[catalogRuntimeKey(runtime)];
520
540
  if (!items) {
521
541
  say(` (无 ${runtime} 的目录数据)`);
522
542
  return;
@@ -536,7 +556,7 @@ function writeAndHint(mutation) {
536
556
  say(" 运行 larkin config apply --agent <App ID> 后应用到新 Runtime session");
537
557
  }
538
558
  function effortChoicesFor(runtime, model) {
539
- const entry = catalog[runtime]?.find((candidate) => candidate.id === model);
559
+ const entry = catalog[catalogRuntimeKey(runtime)]?.find((candidate) => candidate.id === model);
540
560
  if (!entry?.supportedReasoningEfforts?.length)
541
561
  return null;
542
562
  return { list: entry.supportedReasoningEfforts, note: "模型声明", def: entry.defaultReasoningEffort || null };
@@ -554,15 +574,14 @@ function dropEffortIfInvalid(next) {
554
574
  return withoutEffort;
555
575
  }
556
576
  if (kind === "model") {
557
- const runtime = agent.runtime;
577
+ const runtime = userRuntime;
558
578
  if (!value) {
559
- say(`agent=${selectedKey} runtime=${runtime} model=${agent.model}`);
560
579
  say(`\n${runtime} 可选模型:`);
561
580
  listModels(runtime, agent.model);
562
581
  say("\n切换: larkin model <id>");
563
582
  process.exit(0);
564
583
  }
565
- const items = catalog[runtime];
584
+ const items = catalog[catalogRuntimeKey(runtime)];
566
585
  if (items && !items.some((model) => model.id === value)) {
567
586
  console.error(`✗ "${value}" 不是 ${runtime} 的合法模型。合法值:`);
568
587
  for (const model of items)
@@ -588,13 +607,11 @@ else if (kind === "effort") {
588
607
  }
589
608
  const choices = effortChoicesFor(agent.runtime, agent.model);
590
609
  if (!choices) {
591
- say(`agent=${selectedKey} runtime=${agent.runtime} model=${agent.model}`);
592
- say(`模型 ${agent.runtime}/${agent.model} 未显式声明 supportedReasoningEfforts,不能设置 effort。`);
610
+ say(`模型 ${userRuntime}/${agent.model} 未显式声明 supportedReasoningEfforts,不能设置 effort。`);
593
611
  process.exit(value ? 1 : 0);
594
612
  }
595
613
  const { list, note, def } = choices;
596
614
  if (!value) {
597
- say(`agent=${selectedKey} runtime=${agent.runtime} model=${agent.model}`);
598
615
  say(`effort=${agent.effort || `(未设置,runtime 默认${def ? `,该模型默认 ${def}` : ""})`}`);
599
616
  say(`\n${agent.model} 可选档位(${note}):`);
600
617
  for (const level of list)
@@ -603,7 +620,7 @@ else if (kind === "effort") {
603
620
  process.exit(0);
604
621
  }
605
622
  if (!list.includes(value)) {
606
- console.error(`✗ "${value}" 不是 ${agent.runtime}/${agent.model} 的合法档位。合法值: ${list.join(", ")}`);
623
+ console.error(`✗ "${value}" 不是 ${userRuntime}/${agent.model} 的合法档位。合法值: ${list.join(", ")}`);
607
624
  process.exit(1);
608
625
  }
609
626
  if (value === agent.effort) {
@@ -671,34 +688,33 @@ else if (kind === "chats") {
671
688
  }
672
689
  else {
673
690
  if (!value) {
674
- say(`agent=${selectedKey} runtime=${agent.runtime} model=${agent.model}`);
675
691
  say("\n可选 runtime:");
676
- for (const runtime of Object.keys(catalog)) {
677
- say(` ${runtime.padEnd(12)}${runtime === agent.runtime ? " [← 当前]" : ""} 默认模型=${larkinConfig.defaultModelFor(runtime)}`);
692
+ for (const runtime of RUNTIME_OPTIONS) {
693
+ say(` ${runtime.padEnd(12)}${runtime === userRuntime ? " [← 当前]" : ""} 默认模型=${larkinConfig.defaultModelFor(runtime)}`);
678
694
  }
679
695
  say("\n切换: larkin runtime <id> [--model <id>]");
680
696
  process.exit(0);
681
697
  }
682
- if (!catalog[value]) {
683
- console.error(`✗ "${value}" 不是合法 runtime。合法值: ${Object.keys(catalog).join(", ")}`);
698
+ if (!isUserRuntime(value)) {
699
+ console.error(`✗ "${value}" 不是合法 runtime。合法值: ${RUNTIME_OPTIONS.join(", ")}`);
684
700
  process.exit(1);
685
701
  }
686
702
  let model = flagModel;
687
703
  if (model) {
688
- const items = catalog[value];
689
- if (!items.some((candidate) => candidate.id === model)) {
704
+ const items = catalog[catalogRuntimeKey(value)];
705
+ if (!items || !items.some((candidate) => candidate.id === model)) {
690
706
  console.error(`✗ "${model}" 不是 ${value} 的合法模型。合法值:`);
691
- for (const item of items)
707
+ for (const item of items || [])
692
708
  console.error(` ${item.id}`);
693
709
  process.exit(1);
694
710
  }
695
711
  }
696
712
  else {
697
713
  model = larkinConfig.defaultModelFor(value);
698
- if (value !== agent.runtime)
714
+ if (value !== userRuntime)
699
715
  say(`model 随 runtime 重置为默认: ${agent.model} → ${model}(可用 --model 显式指定)`);
700
716
  }
701
- if (value === agent.runtime && model === agent.model) {
717
+ if (value === userRuntime && model === agent.model) {
702
718
  say(`runtime 已经是 ${value},无需修改`);
703
719
  process.exit(0);
704
720
  }
package/dist/app/cli.mjs CHANGED
@@ -49,8 +49,8 @@ if (runtimeAgentAuthority && command === "comment")
49
49
  const commandHelp = {
50
50
  start: `Usage: larkin start [--agent <App ID> | --agents <App ID,...>]
51
51
  Start one foreground supervisor for the daemon and local dashboard, or reuse it.`,
52
- setup: `Usage: larkin setup [--tenant feishu|lark] [--runtime <builtin-pi|external-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 external-pi).`,
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).`,
54
54
  status: `Usage: larkin status [--json]
55
55
  Show Agent configuration, bot identity, credentials, and connection status. Use --json for readiness automation.`,
56
56
  agents: `Usage: larkin agents [--json]
@@ -58,7 +58,7 @@ List every configured Agent and its current local status. Use --json for daemon/
58
58
  model: `Usage: larkin model [<model>] [--agent <App ID>]
59
59
  Show or change an Agent model.`,
60
60
  runtime: `Usage: larkin runtime [<runtime>] [--agent <App ID>] [--model <model>]
61
- Show or change an Agent runtime.`,
61
+ Show or change an Agent runtime. User-facing ids: pi | builtin-pi | codex | claude.`,
62
62
  "pi-distribution": `Usage: larkin pi-distribution [show|builtin|external] [--agent <App ID>] [--snapshot <private-file>] [--import-external-profile]
63
63
  larkin pi-distribution rollback --snapshot <private-file>
64
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.`,
@@ -84,8 +84,10 @@ Credentials, internal paths, serverId, activeAgent, and raw config are never exp
84
84
  larkin session recover --agent <App ID> --reason context-overflow --json [--wait-ready <seconds>]
85
85
  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
86
  "pi-auth": `Usage: larkin pi-auth status [--agent <App ID>] [--json]
87
+ larkin pi-auth providers [--json]
88
+ larkin pi-auth login <preset|custom> --agent <App ID> [--model <model>] [--base-url <url>] [--api-key-stdin] [--json]
87
89
  larkin pi-auth logout <provider> [--agent <App ID>]
88
- Show non-sensitive official Pi credential metadata or remove one target provider credential.`,
90
+ Show a redacted builtin Pi provider catalog, configure one existing Agent with a known preset or custom OpenAI-compatible endpoint, or remove one target provider credential. API keys are accepted only from a TTY prompt or --api-key-stdin.`,
89
91
  comment: `Usage: larkin comment reply --message-id <doc_comment_message_id> --text '<reply>' --json
90
92
  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
93
  telemetry: `Usage: larkin telemetry <status|export|import|flush>
@@ -130,7 +132,7 @@ Usage: larkin <command>
130
132
  config Inspect effective config/source, edit mention inheritance, or explicitly apply runtime changes
131
133
  session reset Replace one idle, zero-backlog Agent Runtime session for a fresh scenario
132
134
  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
135
+ pi-auth Show, configure, or logout one built-in Pi Agent provider credential
134
136
  comment reply Reply to the exact cloud-document comment bound by a polled Inbox message
135
137
  telemetry Inspect, export, import, or flush the durable OpenTelemetry trace queue
136
138
  <lark-cli 命令组> im/docs/wiki/drive 等 lark-cli 命令原样转发,机器人身份已锁定(如 larkin im +chat-list)
@@ -144,7 +146,15 @@ Process persistence is managed externally. larkin start stays in the foreground
144
146
  }
145
147
  const [mode, ...presetArguments] = routes[command];
146
148
  const childSpec = internalCommandSpec(mode, [...presetArguments, ...rest]);
147
- const child = spawn(childSpec.command, childSpec.args, { stdio: "inherit" });
149
+ // 管道 stdin 必须显式转发:Linux 上 inherit 会在父进程排空后再交给子进程,导致 --api-key-stdin 读到空。
150
+ const pipeStdin = command === "pi-auth" && rest.includes("--api-key-stdin") && !process.stdin.isTTY;
151
+ if (pipeStdin && typeof process.stdin.pause === "function")
152
+ process.stdin.pause();
153
+ const child = spawn(childSpec.command, childSpec.args, {
154
+ stdio: [pipeStdin ? "pipe" : "inherit", "inherit", "inherit"],
155
+ });
156
+ if (pipeStdin && child.stdin)
157
+ process.stdin.pipe(child.stdin);
148
158
  // Package-manager bin shims add a wrapper process. Forward terminal signals so the actual command
149
159
  // does not become an orphan that keeps the machine lock or Feishu connection.
150
160
  const signalForwarders = new Map();
@@ -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({
@@ -1,13 +1,234 @@
1
+ import fs from "node:fs";
1
2
  import * as larkinConfig from "../platform/config.mjs";
2
- import { beginBuiltinPiCredentialTransaction, createOfficialPiCredentialRuntime, createOfficialPiLogoutRuntime, logoutOfficialPiProvider, officialPiAuthStatus, } from "../runtime/pi-official-auth.mjs";
3
+ import { createOfficialPiCredentialRuntime, officialPiAuthStatus, } from "../runtime/pi-official-auth.mjs";
4
+ import { configureBuiltinPiProvider, listBuiltinPiProviderCatalog, logoutBuiltinPiProvider, } from "../runtime/pi-provider-login.mjs";
5
+ import { MAX_BUILTIN_PI_API_KEY_LENGTH } from "../runtime/pi-provider-config.mjs";
3
6
  const APP_ID = /^cli_[A-Za-z0-9]+$/;
7
+ const USAGE = "Usage: larkin pi-auth status [--agent <App ID>] [--json] | larkin pi-auth providers [--json] | larkin pi-auth login <preset|custom> --agent <App ID> [--model <model>] [--base-url <url>] [--api-key-stdin] [--json] | larkin pi-auth logout <provider> [--agent <App ID>]";
8
+ const API_KEY_ARGV_MESSAGE = "existing-Agent login does not accept --api-key. Use --api-key-stdin or an interactive TTY prompt.";
9
+ const API_KEY_BUDGET_MESSAGE = "API Key 不能为空或包含控制字符";
10
+ const STDIN_CHUNK_BYTES = 1024;
11
+ const STDIN_BOM_BYTES = 3;
12
+ const STDIN_TRAILING_NEWLINE_SLACK = 32;
13
+ const MAX_STDIN_API_KEY_BYTES = MAX_BUILTIN_PI_API_KEY_LENGTH + STDIN_BOM_BYTES + STDIN_TRAILING_NEWLINE_SLACK;
4
14
  function flag(args, name) {
5
15
  const index = args.indexOf(name);
6
16
  return index >= 0 ? args[index + 1] : undefined;
7
17
  }
8
- export async function main(args = process.argv.slice(2), env = process.env) {
18
+ function hasApiKeyArgv(args) {
19
+ return args.some((argument) => argument === "--api-key" || argument.startsWith("--api-key="));
20
+ }
21
+ function consumeFlags(args, allowed, unary) {
22
+ let index = 0;
23
+ while (index < args.length) {
24
+ const argument = args[index];
25
+ if (!argument.startsWith("--"))
26
+ return false;
27
+ if (unary.has(argument)) {
28
+ index += 1;
29
+ continue;
30
+ }
31
+ if (allowed.has(argument) && args[index + 1] && !args[index + 1].startsWith("--")) {
32
+ index += 2;
33
+ continue;
34
+ }
35
+ return false;
36
+ }
37
+ return true;
38
+ }
39
+ /** 去掉 BOM 与全部尾部换行;保留内部控制字符给后续校验拒绝。 */
40
+ export function normalizeStdinApiKey(raw) {
41
+ return raw.replace(/^\uFEFF/, "").replace(/[\r\n]+$/g, "");
42
+ }
43
+ function assertStdinApiKeyBudget(size) {
44
+ if (size > MAX_STDIN_API_KEY_BYTES)
45
+ throw new Error(API_KEY_BUDGET_MESSAGE);
46
+ }
47
+ function finishStdinApiKey(chunks) {
48
+ const normalized = normalizeStdinApiKey(Buffer.concat(chunks).toString("utf8"));
49
+ if (normalized.length > MAX_BUILTIN_PI_API_KEY_LENGTH)
50
+ throw new Error(API_KEY_BUDGET_MESSAGE);
51
+ return normalized;
52
+ }
53
+ function readFdChunk(fd, buffer) {
54
+ return new Promise((resolve, reject) => {
55
+ fs.read(fd, buffer, 0, buffer.length, null, (error, bytesRead) => {
56
+ if (error)
57
+ reject(error);
58
+ else
59
+ resolve(bytesRead);
60
+ });
61
+ });
62
+ }
63
+ /** 按块读取 fd,超过 key 上限立即停止,不等待管道 EOF。 */
64
+ async function readBoundedFdSecret(fd) {
65
+ const chunks = [];
66
+ let size = 0;
67
+ const buffer = Buffer.alloc(STDIN_CHUNK_BYTES);
68
+ while (true) {
69
+ const bytesRead = await readFdChunk(fd, buffer);
70
+ if (bytesRead === 0)
71
+ break;
72
+ size += bytesRead;
73
+ assertStdinApiKeyBudget(size);
74
+ chunks.push(Buffer.from(buffer.subarray(0, bytesRead)));
75
+ }
76
+ return finishStdinApiKey(chunks);
77
+ }
78
+ async function readBoundedStreamSecret(stdin) {
79
+ const chunks = [];
80
+ let size = 0;
81
+ for await (const chunk of stdin) {
82
+ const bytes = Buffer.from(chunk);
83
+ size += bytes.length;
84
+ assertStdinApiKeyBudget(size);
85
+ chunks.push(bytes);
86
+ }
87
+ return finishStdinApiKey(chunks);
88
+ }
89
+ export async function readStdinSecret(stdin) {
90
+ const stream = stdin;
91
+ if (typeof stream.fd === "number" && stream.fd >= 0 && !stream.isTTY) {
92
+ if (typeof stream.pause === "function")
93
+ stream.pause();
94
+ try {
95
+ return await readBoundedFdSecret(stream.fd);
96
+ }
97
+ catch (error) {
98
+ if (error instanceof Error && error.message === API_KEY_BUDGET_MESSAGE)
99
+ throw error;
100
+ // 回退到异步收集,例如测试传入的假 stream。
101
+ }
102
+ }
103
+ return readBoundedStreamSecret(stdin);
104
+ }
105
+ function createNoEchoQuestioner(stdin, stdout) {
106
+ return {
107
+ close() { },
108
+ secret(prompt) {
109
+ if (!stdin.isTTY || typeof stdin.setRawMode !== "function") {
110
+ throw new Error("non-interactive API-key login requires --api-key-stdin");
111
+ }
112
+ stdout.write(prompt);
113
+ const wasRaw = stdin.isRaw;
114
+ stdin.setRawMode(true);
115
+ stdin.resume();
116
+ return new Promise((resolve, reject) => {
117
+ let value = "";
118
+ const finish = (error) => {
119
+ stdin.off("data", onData);
120
+ stdin.setRawMode(Boolean(wasRaw));
121
+ stdin.pause();
122
+ stdout.write("\n");
123
+ if (error)
124
+ reject(error);
125
+ else
126
+ resolve(value);
127
+ };
128
+ const onData = (chunk) => {
129
+ const bytes = Buffer.from(chunk);
130
+ for (const byte of bytes) {
131
+ if (byte === 3)
132
+ return finish(new Error("pi-auth login cancelled"));
133
+ if (byte === 10 || byte === 13)
134
+ return finish();
135
+ if (byte === 8 || byte === 127) {
136
+ value = value.slice(0, -1);
137
+ continue;
138
+ }
139
+ if (byte >= 32 && byte <= 126)
140
+ value += String.fromCharCode(byte);
141
+ }
142
+ };
143
+ stdin.on("data", onData);
144
+ });
145
+ },
146
+ };
147
+ }
148
+ function resolveAgentId(args, env) {
149
+ const loaded = larkinConfig.loadConfig(env);
150
+ const agentId = flag(args, "--agent") || loaded.config.activeAgent || undefined;
151
+ if (!agentId || !APP_ID.test(agentId))
152
+ throw new Error("请用 --agent <App ID> 选择 Agent");
153
+ const agent = loaded.config.agents[agentId];
154
+ if (!agent)
155
+ throw new Error(`Agent ${agentId} 不存在`);
156
+ if (agent.runtime !== "pi" || agent.piDistribution !== "builtin")
157
+ throw new Error(`Agent ${agentId} 不是内置 Pi`);
158
+ return agentId;
159
+ }
160
+ export async function main(args = process.argv.slice(2), env = process.env, io = { stdin: process.stdin, stdout: process.stdout, isTTY: Boolean(process.stdin.isTTY) }) {
9
161
  const action = args[0] || "status";
10
162
  const json = args.includes("--json");
163
+ if (hasApiKeyArgv(args))
164
+ throw new Error(API_KEY_ARGV_MESSAGE);
165
+ if (action === "providers") {
166
+ if (!consumeFlags(args.slice(1), new Set(), new Set(["--json"])))
167
+ throw new Error(USAGE);
168
+ const providers = listBuiltinPiProviderCatalog();
169
+ if (json) {
170
+ console.log(JSON.stringify({ providers }));
171
+ return;
172
+ }
173
+ console.log("Builtin Pi providers:");
174
+ for (const entry of providers) {
175
+ const model = entry.defaultModel ? ` default=${entry.defaultModel}` : " (requires --base-url and --model)";
176
+ console.log(` ${entry.id} ${entry.name}${model}`);
177
+ }
178
+ return;
179
+ }
180
+ if (action === "login") {
181
+ const preset = args[1];
182
+ const rest = args.slice(2);
183
+ if (!preset || preset.startsWith("--")
184
+ || !consumeFlags(rest, new Set(["--agent", "--model", "--base-url"]), new Set(["--json", "--api-key-stdin"]))) {
185
+ throw new Error(USAGE);
186
+ }
187
+ const agentId = resolveAgentId(rest, env);
188
+ const stdinKey = rest.includes("--api-key-stdin");
189
+ let apiKey = "";
190
+ if (stdinKey)
191
+ apiKey = await readStdinSecret(io.stdin);
192
+ else if (io.isTTY) {
193
+ const questioner = createNoEchoQuestioner(io.stdin, io.stdout);
194
+ try {
195
+ apiKey = await questioner.secret("API Key:\n> ");
196
+ }
197
+ finally {
198
+ questioner.close();
199
+ }
200
+ }
201
+ else {
202
+ throw new Error("non-interactive API-key login requires --api-key-stdin");
203
+ }
204
+ const result = await configureBuiltinPiProvider({
205
+ agentId,
206
+ preset,
207
+ apiKey,
208
+ model: flag(rest, "--model"),
209
+ baseUrl: flag(rest, "--base-url"),
210
+ env,
211
+ });
212
+ const payload = {
213
+ agentId: result.agentId,
214
+ provider: result.provider,
215
+ model: result.model,
216
+ credentialType: result.credentialType,
217
+ applyState: result.applyState,
218
+ ...(result.applyError ? { applyError: result.applyError } : {}),
219
+ };
220
+ if (json)
221
+ console.log(JSON.stringify(payload));
222
+ else {
223
+ const pending = result.applyState === "applied"
224
+ ? "applied"
225
+ : result.applyState === "pending"
226
+ ? `pending${result.applyError ? ` (${result.applyError})` : ""}`
227
+ : "saved; will apply on larkin start";
228
+ console.log(`Agent ${result.agentId} provider=${result.provider} model=${result.model} credential=${result.credentialType} apply=${pending}`);
229
+ }
230
+ return;
231
+ }
11
232
  const requestedAgent = flag(args, "--agent");
12
233
  let index = action === "logout" ? 2 : 1;
13
234
  let valid = action === "status" || action === "logout";
@@ -23,9 +244,8 @@ export async function main(args = process.argv.slice(2), env = process.env) {
23
244
  }
24
245
  valid = false;
25
246
  }
26
- if (!valid) {
27
- throw new Error("Usage: larkin pi-auth status [--agent <App ID>] [--json] | larkin pi-auth logout <provider> [--agent <App ID>]");
28
- }
247
+ if (!valid)
248
+ throw new Error(USAGE);
29
249
  const loaded = larkinConfig.loadConfig(env);
30
250
  const agentId = requestedAgent || loaded.config.activeAgent || undefined;
31
251
  if (!agentId || !APP_ID.test(agentId))
@@ -50,18 +270,8 @@ export async function main(args = process.argv.slice(2), env = process.env) {
50
270
  return;
51
271
  }
52
272
  const providerId = args[1];
53
- if (!providerId || providerId.startsWith("--") || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(providerId)) {
273
+ if (!providerId || providerId.startsWith("--"))
54
274
  throw new Error("logout 需要安全的 provider ID");
55
- }
56
- const transaction = beginBuiltinPiCredentialTransaction(loaded.configDir, agentId);
57
- try {
58
- const runtime = await createOfficialPiLogoutRuntime(loaded.configDir, agentId);
59
- await logoutOfficialPiProvider(runtime, providerId);
60
- transaction.commit();
61
- }
62
- catch (error) {
63
- transaction.rollback();
64
- throw error;
65
- }
275
+ await logoutBuiltinPiProvider({ agentId, providerId, env });
66
276
  console.log(`Agent ${agentId} 已退出 ${providerId};其他 provider 未修改`);
67
277
  }
@@ -7,6 +7,7 @@ import { discoverClaudeModelCatalog } from "../runtime/claude-model-catalog.mjs"
7
7
  import { discoverPiModelCatalog } from "../runtime/pi-model-catalog.mjs";
8
8
  import { discoverCodexModelCatalog } from "../runtime/codex-model-catalog.mjs";
9
9
  import { ownedPiCatalogAgentDir, piCatalogCommandSpec } from "../runtime/pi-provider-config.mjs";
10
+ import { piCatalogDistributionForUserRuntime } from "../runtime/user-runtime.mjs";
10
11
  function readTestModelDirectory(env) {
11
12
  const fixture = env?.LARKIN_TEST_RUNTIME_MODEL_DIRECTORY_FILE;
12
13
  if (!fixture)
@@ -36,7 +37,7 @@ export async function discoverRuntimeModelDirectory(input) {
36
37
  ...catalog.models.map(({ id, label, supportedReasoningEfforts }) => ({ id, label, supportedReasoningEfforts })),
37
38
  ];
38
39
  }
39
- if (input.runtime === "pi") {
40
+ if (input.runtime === "pi" || input.runtime === "builtin-pi") {
40
41
  const env = input.env ?? process.env;
41
42
  const agentId = input.agentId || "";
42
43
  if (!agentId)
@@ -46,7 +47,7 @@ export async function discoverRuntimeModelDirectory(input) {
46
47
  const agent = config.agents[agentId];
47
48
  if (!agent)
48
49
  throw new Error(`unknown agent: ${agentId}`);
49
- const catalogCommand = piCatalogCommandSpec(agent.piDistribution, env);
50
+ const catalogCommand = piCatalogCommandSpec(piCatalogDistributionForUserRuntime(input.runtime), env);
50
51
  const catalog = await discoverPiModelCatalog({
51
52
  cwd: input.cwd,
52
53
  agentDir: ownedPiCatalogAgentDir(configDir, agentId),