larkin 0.4.28 → 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-config.mjs +3 -1
- package/dist/app/cli.mjs +13 -3
- package/dist/app/local-control.mjs +16 -3
- package/dist/app/pi-auth-cli.mjs +227 -17
- package/dist/app/runtime-process.mjs +42 -5
- package/dist/dashboard/dashboard-config-controller.mjs +130 -5
- 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 +38 -10
- package/dist/platform/config.mjs +36 -10
- 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-readiness.cjs +123 -3
- package/dist/runtime/runtime-readiness.mjs +116 -4
- package/package.json +1 -1
|
@@ -462,7 +462,9 @@ if (kind === "pi-distribution") {
|
|
|
462
462
|
assertBuiltinPiAgentDirectory(piAgentDirectory(configDir, selectedKey));
|
|
463
463
|
}
|
|
464
464
|
catch {
|
|
465
|
-
die(
|
|
465
|
+
die(agent.piDistribution === "builtin"
|
|
466
|
+
? "内置 Pi provider 尚未为该 Agent 配置。请使用 Dashboard Provider Credentials 或 larkin pi-auth login。"
|
|
467
|
+
: "内置 Pi provider 尚未为该 Agent 配置。");
|
|
466
468
|
}
|
|
467
469
|
}
|
|
468
470
|
try {
|
package/dist/app/cli.mjs
CHANGED
|
@@ -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
|
}
|
|
@@ -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
|
-
|
|
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 {
|
|
@@ -100,10 +136,11 @@ export async function main(env = process.env, overrides = {}) {
|
|
|
100
136
|
larkinHome: env.LARKIN_HOME,
|
|
101
137
|
authorityToken: env.LARKIN_CONTROL_AUTHORIZATION,
|
|
102
138
|
async upsert(request) {
|
|
103
|
-
const agent =
|
|
139
|
+
const agent = await applyRuntimeAgentUpsert(env, request.agentId, {}, {
|
|
140
|
+
...(request.expectedSignature ? { expectedSignature: request.expectedSignature } : {}),
|
|
141
|
+
}, (candidate) => hostShell.upsertAgent(candidate));
|
|
104
142
|
// Only the selected profile is synchronized; active profiles and their directory
|
|
105
143
|
// are never quarantined or rebuilt during hot attach.
|
|
106
|
-
await hostShell.upsertAgent(agent);
|
|
107
144
|
const tracked = await requestSupervisorAgentUpsert({
|
|
108
145
|
larkinHome: env.LARKIN_HOME, agentId: agent.agentId, operationId: request.operationId,
|
|
109
146
|
});
|
|
@@ -9,6 +9,8 @@ import { discoverPiModelCatalog } from "../runtime/pi-model-catalog.mjs";
|
|
|
9
9
|
import { managedOfficialLarkCli } from "../app/agent-lark-cli-workspace.mjs";
|
|
10
10
|
import { ownedPiCatalogAgentDir, piCatalogCommandSpec } from "../runtime/pi-provider-config.mjs";
|
|
11
11
|
import { RUNTIME_OPTIONS, piCatalogDistributionForUserRuntime, toUserRuntime } from "../runtime/user-runtime.mjs";
|
|
12
|
+
import { configureBuiltinPiProvider, listBuiltinPiProviderCatalog, logoutBuiltinPiProvider, sanitizeProviderLoginError, } from "../runtime/pi-provider-login.mjs";
|
|
13
|
+
import { createOfficialPiCredentialRuntime, officialPiAuthStatus, } from "../runtime/pi-official-auth.mjs";
|
|
12
14
|
function pruneCache(cache, expired, maxEntries) {
|
|
13
15
|
for (const [key, value] of cache)
|
|
14
16
|
if (expired(value))
|
|
@@ -170,9 +172,38 @@ export function createPiModelDirectoryResolver(options = {}) {
|
|
|
170
172
|
const cache = new Map();
|
|
171
173
|
const failures = new Map();
|
|
172
174
|
const inFlight = new Map();
|
|
175
|
+
const generations = new Map();
|
|
176
|
+
// 单调序号:targeted 后再 global 时,in-flight 的 started 不能与新 generation 相等。
|
|
177
|
+
let seq = 0;
|
|
178
|
+
let baseline = 0;
|
|
179
|
+
const generationOf = (agentId) => Math.max(baseline, generations.get(agentId) ?? 0);
|
|
180
|
+
const dropPrefixed = (store, agentId) => {
|
|
181
|
+
if (!agentId) {
|
|
182
|
+
store.clear();
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
const prefix = `${agentId}\u0000`;
|
|
186
|
+
for (const key of store.keys())
|
|
187
|
+
if (key.startsWith(prefix))
|
|
188
|
+
store.delete(key);
|
|
189
|
+
};
|
|
173
190
|
return {
|
|
191
|
+
invalidate(agentId) {
|
|
192
|
+
dropPrefixed(cache, agentId);
|
|
193
|
+
dropPrefixed(failures, agentId);
|
|
194
|
+
dropPrefixed(inFlight, agentId);
|
|
195
|
+
seq += 1;
|
|
196
|
+
if (!agentId) {
|
|
197
|
+
baseline = seq;
|
|
198
|
+
generations.clear();
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
generations.set(agentId, seq);
|
|
202
|
+
}
|
|
203
|
+
},
|
|
174
204
|
async resolve(input) {
|
|
175
205
|
const key = [input.agentId, input.cwd, input.agentDir, input.command ?? "", ...(input.commandArgs ?? [])].join("\u0000");
|
|
206
|
+
const started = generationOf(input.agentId);
|
|
176
207
|
pruneCache(cache, (entry) => now() >= entry.expiresAt, 64);
|
|
177
208
|
pruneCache(failures, (entry) => now() >= entry.expiresAt, 64);
|
|
178
209
|
const cached = cache.get(key);
|
|
@@ -196,17 +227,23 @@ export function createPiModelDirectoryResolver(options = {}) {
|
|
|
196
227
|
id, label, ...(contextWindow ? { contextWindow } : {}), supportedReasoningEfforts, ...(defaultReasoningEffort ? { defaultReasoningEffort } : {}),
|
|
197
228
|
})),
|
|
198
229
|
];
|
|
199
|
-
|
|
200
|
-
|
|
230
|
+
if (generationOf(input.agentId) === started) {
|
|
231
|
+
failures.delete(key);
|
|
232
|
+
cache.set(key, { expiresAt: now() + ttlMs, value: models });
|
|
233
|
+
}
|
|
201
234
|
return models;
|
|
202
235
|
}
|
|
203
236
|
catch (error) {
|
|
204
|
-
|
|
237
|
+
if (generationOf(input.agentId) === started)
|
|
238
|
+
failures.set(key, { expiresAt: now() + negativeTtlMs });
|
|
205
239
|
throw error;
|
|
206
240
|
}
|
|
207
241
|
})();
|
|
208
242
|
inFlight.set(key, pending);
|
|
209
|
-
void pending.finally(() =>
|
|
243
|
+
void pending.finally(() => {
|
|
244
|
+
if (inFlight.get(key) === pending)
|
|
245
|
+
inFlight.delete(key);
|
|
246
|
+
}).catch(() => undefined);
|
|
210
247
|
}
|
|
211
248
|
return await pending;
|
|
212
249
|
},
|
|
@@ -383,7 +420,7 @@ function dashboardMutation(value) {
|
|
|
383
420
|
return { kind: operation, agentId, model: String(value.model || "") };
|
|
384
421
|
return { kind: "set-agent-effort", agentId, effort: value.effort === null || value.effort === "default" ? null : String(value.effort || "") };
|
|
385
422
|
}
|
|
386
|
-
export function createDashboardConfigController({ csrfCapability, env = process.env, requestUpsert = requestAgentUpsert, chatDirectoryResolver = createChatDirectoryResolver(), claudeModelDirectoryResolver = createClaudeModelDirectoryResolver(), codexModelDirectoryResolver = createCodexModelDirectoryResolver(), piModelDirectoryResolver = createPiModelDirectoryResolver(), }) {
|
|
423
|
+
export function createDashboardConfigController({ csrfCapability, env = process.env, requestUpsert = requestAgentUpsert, chatDirectoryResolver = createChatDirectoryResolver(), claudeModelDirectoryResolver = createClaudeModelDirectoryResolver(), codexModelDirectoryResolver = createCodexModelDirectoryResolver(), piModelDirectoryResolver = createPiModelDirectoryResolver(), configureProvider = configureBuiltinPiProvider, logoutProvider = logoutBuiltinPiProvider, listProviderCatalog = listBuiltinPiProviderCatalog, loadProviderStatus = async (configDir, agentId) => officialPiAuthStatus(await createOfficialPiCredentialRuntime(configDir, agentId)), }) {
|
|
387
424
|
const resolvePiModelDirectory = async (agentId, userRuntime) => {
|
|
388
425
|
const { config, configDir } = loadConfig(env);
|
|
389
426
|
const agent = config.agents[agentId];
|
|
@@ -511,6 +548,94 @@ export function createDashboardConfigController({ csrfCapability, env = process.
|
|
|
511
548
|
}
|
|
512
549
|
return true;
|
|
513
550
|
}
|
|
551
|
+
if (requestUrl.pathname === "/api/pi-auth/providers" && req.method === "GET") {
|
|
552
|
+
try {
|
|
553
|
+
assertPrivateReadRequest(req, csrfCapability);
|
|
554
|
+
json(res, 200, { providers: listProviderCatalog() });
|
|
555
|
+
}
|
|
556
|
+
catch (error) {
|
|
557
|
+
json(res, error instanceof Error && /host|capability/.test(error.message) ? 403 : 500, { error: "provider catalog unavailable" });
|
|
558
|
+
}
|
|
559
|
+
return true;
|
|
560
|
+
}
|
|
561
|
+
if (requestUrl.pathname === "/api/pi-auth/status" && req.method === "GET") {
|
|
562
|
+
try {
|
|
563
|
+
assertPrivateReadRequest(req, csrfCapability);
|
|
564
|
+
const agentId = requestUrl.searchParams.get("agent") || "";
|
|
565
|
+
const { config, configDir } = loadConfig(env);
|
|
566
|
+
const agent = config.agents[agentId];
|
|
567
|
+
if (!agent || agent.runtime !== "pi" || agent.piDistribution !== "builtin")
|
|
568
|
+
throw new Error("unknown agent");
|
|
569
|
+
const credentials = await loadProviderStatus(configDir, agentId);
|
|
570
|
+
json(res, 200, { agentId, model: agent.model, credentials });
|
|
571
|
+
}
|
|
572
|
+
catch (error) {
|
|
573
|
+
json(res, error instanceof Error && /host|capability/.test(error.message) ? 403 : 500, { error: "provider status unavailable" });
|
|
574
|
+
}
|
|
575
|
+
return true;
|
|
576
|
+
}
|
|
577
|
+
if (requestUrl.pathname === "/api/pi-auth/login" && req.method === "POST") {
|
|
578
|
+
let secret = "";
|
|
579
|
+
try {
|
|
580
|
+
assertWriteRequest(req, csrfCapability);
|
|
581
|
+
const body = await boundedJson(req);
|
|
582
|
+
const allowed = ["agentId", "preset", "apiKey", "model", "baseUrl"];
|
|
583
|
+
if (Object.keys(body).some((key) => !allowed.includes(key)))
|
|
584
|
+
throw new Error("unsupported field");
|
|
585
|
+
const agentId = String(body.agentId || "");
|
|
586
|
+
const preset = String(body.preset || "");
|
|
587
|
+
const apiKey = typeof body.apiKey === "string" ? body.apiKey : "";
|
|
588
|
+
secret = apiKey;
|
|
589
|
+
if (!/^cli_[A-Za-z0-9]+$/.test(agentId) || !preset)
|
|
590
|
+
throw new Error("invalid login request");
|
|
591
|
+
const { config } = loadConfig(env);
|
|
592
|
+
const agent = config.agents[agentId];
|
|
593
|
+
if (!agent || agent.runtime !== "pi" || agent.piDistribution !== "builtin")
|
|
594
|
+
throw new Error("Agent is not builtin Pi");
|
|
595
|
+
const result = await configureProvider({
|
|
596
|
+
agentId,
|
|
597
|
+
preset,
|
|
598
|
+
apiKey,
|
|
599
|
+
...(typeof body.model === "string" ? { model: body.model } : {}),
|
|
600
|
+
...(typeof body.baseUrl === "string" ? { baseUrl: body.baseUrl } : {}),
|
|
601
|
+
env,
|
|
602
|
+
});
|
|
603
|
+
piModelDirectoryResolver.invalidate?.(agentId);
|
|
604
|
+
json(res, 200, {
|
|
605
|
+
ok: true,
|
|
606
|
+
agentId: result.agentId,
|
|
607
|
+
provider: result.provider,
|
|
608
|
+
preset: result.preset,
|
|
609
|
+
model: result.model,
|
|
610
|
+
credentialType: result.credentialType,
|
|
611
|
+
applyState: result.applyState,
|
|
612
|
+
...(result.applyError ? { applyError: sanitizeProviderLoginError(result.applyError, secret || undefined) } : {}),
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
catch (error) {
|
|
616
|
+
json(res, 400, { error: sanitizeProviderLoginError(error, secret || undefined) });
|
|
617
|
+
}
|
|
618
|
+
return true;
|
|
619
|
+
}
|
|
620
|
+
if (requestUrl.pathname === "/api/pi-auth/logout" && req.method === "POST") {
|
|
621
|
+
try {
|
|
622
|
+
assertWriteRequest(req, csrfCapability);
|
|
623
|
+
const body = await boundedJson(req);
|
|
624
|
+
if (Object.keys(body).sort().join(",") !== "agentId,provider")
|
|
625
|
+
throw new Error("invalid logout request");
|
|
626
|
+
const agentId = String(body.agentId || "");
|
|
627
|
+
const provider = String(body.provider || "");
|
|
628
|
+
if (!/^cli_[A-Za-z0-9]+$/.test(agentId))
|
|
629
|
+
throw new Error("invalid logout request");
|
|
630
|
+
const result = await logoutProvider({ agentId, providerId: provider, env });
|
|
631
|
+
piModelDirectoryResolver.invalidate?.(agentId);
|
|
632
|
+
json(res, 200, { ok: true, agentId: result.agentId, provider: result.provider });
|
|
633
|
+
}
|
|
634
|
+
catch (error) {
|
|
635
|
+
json(res, 400, { error: sanitizeProviderLoginError(error) });
|
|
636
|
+
}
|
|
637
|
+
return true;
|
|
638
|
+
}
|
|
514
639
|
if (requestUrl.pathname === "/api/config/apply" && req.method === "POST") {
|
|
515
640
|
try {
|
|
516
641
|
assertWriteRequest(req, csrfCapability);
|