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.
- package/dist/app/agent-cli.mjs +4 -2
- package/dist/app/agent-config.mjs +45 -29
- package/dist/app/cli.mjs +16 -6
- package/dist/app/local-control.mjs +16 -3
- package/dist/app/pi-auth-cli.mjs +227 -17
- package/dist/app/runtime-model-directory.mjs +3 -2
- package/dist/app/runtime-process.mjs +42 -5
- package/dist/app/setup.mjs +15 -14
- package/dist/dashboard/dashboard-config-controller.mjs +145 -13
- package/dist/dashboard/dashboard-view-model.mjs +3 -2
- package/dist/dashboard/web/assets/dashboard.css +1 -1
- package/dist/dashboard/web/assets/dashboard.js +15 -15
- package/dist/feishu/host-shell.cjs +68 -16
- package/dist/feishu/host-shell.mjs +69 -17
- package/dist/platform/config.cjs +67 -16
- package/dist/platform/config.mjs +57 -16
- package/dist/runtime/pi-official-auth.cjs +594 -0
- package/dist/runtime/pi-official-auth.mjs +53 -0
- package/dist/runtime/pi-provider-config.cjs +26 -3
- package/dist/runtime/pi-provider-config.mjs +22 -2
- package/dist/runtime/pi-provider-login.mjs +240 -0
- package/dist/runtime/runtime-adapters.mjs +24 -4
- package/dist/runtime/runtime-host.cjs +215 -16
- package/dist/runtime/runtime-host.mjs +216 -17
- package/dist/runtime/runtime-model-catalog.cjs +4 -3
- package/dist/runtime/runtime-model-catalog.mjs +4 -3
- package/dist/runtime/runtime-readiness.cjs +123 -3
- package/dist/runtime/runtime-readiness.mjs +116 -4
- package/dist/runtime/user-runtime.cjs +51 -0
- package/dist/runtime/user-runtime.mjs +41 -0
- package/dist/setup/bot-register.mjs +3 -3
- package/dist/setup/setup-agent-choice.mjs +10 -9
- package/dist/setup/setup-bind.mjs +2 -2
- package/package.json +1 -1
package/dist/app/agent-cli.mjs
CHANGED
|
@@ -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,
|
|
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
|
|
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({
|
|
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(
|
|
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
|
-
|
|
496
|
-
|
|
497
|
-
|
|
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(
|
|
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
|
-
|
|
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 =
|
|
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(
|
|
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}" 不是 ${
|
|
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
|
|
677
|
-
say(` ${runtime.padEnd(12)}${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 (!
|
|
683
|
-
console.error(`✗ "${value}" 不是合法 runtime。合法值: ${
|
|
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 !==
|
|
714
|
+
if (value !== userRuntime)
|
|
699
715
|
say(`model 随 runtime 重置为默认: ${agent.model} → ${model}(可用 --model 显式指定)`);
|
|
700
716
|
}
|
|
701
|
-
if (value ===
|
|
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|
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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({
|
|
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: {
|
|
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({
|
package/dist/app/pi-auth-cli.mjs
CHANGED
|
@@ -1,13 +1,234 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
1
2
|
import * as larkinConfig from "../platform/config.mjs";
|
|
2
|
-
import {
|
|
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
|
-
|
|
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(
|
|
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("--")
|
|
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(
|
|
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),
|