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
|
@@ -0,0 +1,594 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.officialPiHasStoredProvider = officialPiHasStoredProvider;
|
|
40
|
+
exports.beginBuiltinPiCredentialTransaction = beginBuiltinPiCredentialTransaction;
|
|
41
|
+
exports.createOfficialPiRegistryRuntime = createOfficialPiRegistryRuntime;
|
|
42
|
+
exports.createOfficialPiCredentialRuntime = createOfficialPiCredentialRuntime;
|
|
43
|
+
exports.createOfficialPiLogoutRuntime = createOfficialPiLogoutRuntime;
|
|
44
|
+
exports.createOfficialPiModelRuntime = createOfficialPiModelRuntime;
|
|
45
|
+
exports.listOfficialPiAuthProviders = listOfficialPiAuthProviders;
|
|
46
|
+
exports.createOfficialPiAuthInteraction = createOfficialPiAuthInteraction;
|
|
47
|
+
exports.runOfficialPiLogin = runOfficialPiLogin;
|
|
48
|
+
exports.verifyOfficialPiProviderTurn = verifyOfficialPiProviderTurn;
|
|
49
|
+
exports.officialPiAuthStatus = officialPiAuthStatus;
|
|
50
|
+
exports.logoutOfficialPiProvider = logoutOfficialPiProvider;
|
|
51
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
52
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
53
|
+
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
54
|
+
const node_url_1 = require("node:url");
|
|
55
|
+
const secure_metadata_js_1 = require("../platform/secure-metadata.cjs");
|
|
56
|
+
// proper-lockfile does not publish TypeScript declarations.
|
|
57
|
+
// @ts-expect-error bundled CommonJS dependency
|
|
58
|
+
const proper_lockfile_1 = __importDefault(require("proper-lockfile"));
|
|
59
|
+
const pi_coding_agent_1 = require("@earendil-works/pi-coding-agent");
|
|
60
|
+
const pi_ai_1 = require("@earendil-works/pi-ai");
|
|
61
|
+
const pi_provider_config_js_1 = require("./pi-provider-config.cjs");
|
|
62
|
+
const SNAPSHOT_FILES = ["auth.json", "models.json", "models-store.json"];
|
|
63
|
+
const LOCK_WAIT_MS = 15_000;
|
|
64
|
+
const LOCK_RETRY_MS = 25;
|
|
65
|
+
const heldCredentialLocks = new Map();
|
|
66
|
+
const credentialQueues = new Map();
|
|
67
|
+
function credentialLockPath(configDir, agentId) {
|
|
68
|
+
if (!/^cli_[A-Za-z0-9]+$/.test(agentId))
|
|
69
|
+
throw new Error("invalid Agent ID for Pi credential lock");
|
|
70
|
+
return node_path_1.default.join((0, pi_provider_config_js_1.piAgentDirectory)(configDir, agentId), "auth.json");
|
|
71
|
+
}
|
|
72
|
+
function ensureLockDirectory(lockPath) {
|
|
73
|
+
const directory = node_path_1.default.dirname(lockPath);
|
|
74
|
+
node_fs_1.default.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
75
|
+
node_fs_1.default.chmodSync(directory, 0o700);
|
|
76
|
+
if (secure_metadata_js_1.isWindows) {
|
|
77
|
+
// Windows 无 POSIX 权限位(mode 恒 0o666):改用 icacls 收紧 ACL 并回读校验。
|
|
78
|
+
(0, secure_metadata_js_1.secureWindowsDirectoryAcl)(directory, { label: "Pi credential lock 目录" });
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const stat = node_fs_1.default.lstatSync(directory);
|
|
82
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()
|
|
83
|
+
|| (typeof process.getuid === "function" && stat.uid !== process.getuid())
|
|
84
|
+
|| (stat.mode & 0o777) !== 0o700)
|
|
85
|
+
throw new Error("Pi credential lock directory must be an owned 0700 directory");
|
|
86
|
+
}
|
|
87
|
+
function acquireCredentialLockSync(lockPath) {
|
|
88
|
+
const existing = heldCredentialLocks.get(lockPath);
|
|
89
|
+
if (existing) {
|
|
90
|
+
existing.depth += 1;
|
|
91
|
+
return existing.owner;
|
|
92
|
+
}
|
|
93
|
+
ensureLockDirectory(lockPath);
|
|
94
|
+
const owner = { pid: process.pid, token: node_crypto_1.default.randomUUID(), createdAt: Date.now() };
|
|
95
|
+
const deadline = Date.now() + LOCK_WAIT_MS;
|
|
96
|
+
while (true) {
|
|
97
|
+
try {
|
|
98
|
+
const release = proper_lockfile_1.default.lockSync(lockPath, { realpath: false, stale: 30_000, update: 10_000 });
|
|
99
|
+
heldCredentialLocks.set(lockPath, { owner, depth: 1, release });
|
|
100
|
+
return owner;
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
if (error.code !== "ELOCKED")
|
|
104
|
+
throw error;
|
|
105
|
+
if (Date.now() >= deadline)
|
|
106
|
+
throw new Error("Pi credential store is locked by another live process");
|
|
107
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, LOCK_RETRY_MS);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function releaseCredentialLock(lockPath, owner) {
|
|
112
|
+
const held = heldCredentialLocks.get(lockPath);
|
|
113
|
+
if (!held || held.owner.token !== owner.token)
|
|
114
|
+
throw new Error("Pi credential lock ownership mismatch");
|
|
115
|
+
held.depth -= 1;
|
|
116
|
+
if (held.depth > 0)
|
|
117
|
+
return;
|
|
118
|
+
try {
|
|
119
|
+
held.release();
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
heldCredentialLocks.delete(lockPath);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function withCredentialLock(lockPath, operation) {
|
|
126
|
+
const previous = credentialQueues.get(lockPath) ?? Promise.resolve();
|
|
127
|
+
const result = previous.catch(() => undefined).then(async () => {
|
|
128
|
+
const owner = acquireCredentialLockSync(lockPath);
|
|
129
|
+
try {
|
|
130
|
+
return await operation();
|
|
131
|
+
}
|
|
132
|
+
finally {
|
|
133
|
+
releaseCredentialLock(lockPath, owner);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
credentialQueues.set(lockPath, result.catch(() => undefined));
|
|
137
|
+
return result;
|
|
138
|
+
}
|
|
139
|
+
function ensurePrivateDirectory(directory) {
|
|
140
|
+
node_fs_1.default.mkdirSync(node_path_1.default.dirname(directory), { recursive: true, mode: 0o700 });
|
|
141
|
+
node_fs_1.default.chmodSync(node_path_1.default.dirname(directory), 0o700);
|
|
142
|
+
node_fs_1.default.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
143
|
+
node_fs_1.default.chmodSync(directory, 0o700);
|
|
144
|
+
if (secure_metadata_js_1.isWindows) {
|
|
145
|
+
// Windows 无 POSIX 权限位:icacls 收紧为「当前用户 + SYSTEM」并回读校验。
|
|
146
|
+
(0, secure_metadata_js_1.secureWindowsDirectoryAcl)(directory, { label: "Pi official credential 目录" });
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const stat = node_fs_1.default.lstatSync(directory);
|
|
150
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()
|
|
151
|
+
|| (typeof process.getuid === "function" && stat.uid !== process.getuid())
|
|
152
|
+
|| (stat.mode & 0o777) !== 0o700) {
|
|
153
|
+
throw new Error("Pi official credential directory must be an owned 0700 directory");
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function safeRead(file) {
|
|
157
|
+
try {
|
|
158
|
+
const stat = node_fs_1.default.lstatSync(file);
|
|
159
|
+
if (!stat.isFile() || stat.isSymbolicLink()
|
|
160
|
+
|| (typeof process.getuid === "function" && stat.uid !== process.getuid())
|
|
161
|
+
|| (!secure_metadata_js_1.isWindows && (stat.mode & 0o777) !== 0o600))
|
|
162
|
+
throw new Error(`unsafe Pi auth file: ${node_path_1.default.basename(file)}`);
|
|
163
|
+
return node_fs_1.default.readFileSync(file);
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
if (error.code === "ENOENT")
|
|
167
|
+
return null;
|
|
168
|
+
throw error;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
function restoreFile(file, bytes) {
|
|
172
|
+
if (bytes === null) {
|
|
173
|
+
try {
|
|
174
|
+
node_fs_1.default.unlinkSync(file);
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
if (error.code !== "ENOENT")
|
|
178
|
+
throw error;
|
|
179
|
+
}
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const temporary = `${file}.${process.pid}.${node_crypto_1.default.randomUUID()}.tmp`;
|
|
183
|
+
try {
|
|
184
|
+
node_fs_1.default.writeFileSync(temporary, bytes, { mode: 0o600, flag: "wx" });
|
|
185
|
+
node_fs_1.default.renameSync(temporary, file);
|
|
186
|
+
node_fs_1.default.chmodSync(file, 0o600);
|
|
187
|
+
}
|
|
188
|
+
finally {
|
|
189
|
+
try {
|
|
190
|
+
node_fs_1.default.unlinkSync(temporary);
|
|
191
|
+
}
|
|
192
|
+
catch { /* renamed or absent */ }
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
function readCredentialData(authPath) {
|
|
196
|
+
const bytes = safeRead(authPath);
|
|
197
|
+
if (bytes === null)
|
|
198
|
+
return {};
|
|
199
|
+
const parsed = JSON.parse(bytes.toString("utf8"));
|
|
200
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
201
|
+
throw new Error("Pi auth.json must contain an object");
|
|
202
|
+
return parsed;
|
|
203
|
+
}
|
|
204
|
+
/** 只读校验 Agent 凭证路径:每级必须是当前用户拥有的非 symlink 目录,不创建、不改权限。 */
|
|
205
|
+
function officialPiOwnedAuthPath(configDir, agentId) {
|
|
206
|
+
if (!/^cli_[A-Za-z0-9]+$/.test(agentId))
|
|
207
|
+
return null;
|
|
208
|
+
const root = node_path_1.default.resolve(configDir);
|
|
209
|
+
const expected = node_path_1.default.join(root, "providers", "pi", agentId, "auth.json");
|
|
210
|
+
const segments = ["providers", "pi", agentId];
|
|
211
|
+
let current = root;
|
|
212
|
+
try {
|
|
213
|
+
const rootStat = node_fs_1.default.lstatSync(current);
|
|
214
|
+
if (!rootStat.isDirectory() || rootStat.isSymbolicLink())
|
|
215
|
+
return null;
|
|
216
|
+
if (typeof process.getuid === "function" && rootStat.uid !== process.getuid())
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
for (let index = 0; index < segments.length; index += 1) {
|
|
223
|
+
current = node_path_1.default.join(current, segments[index]);
|
|
224
|
+
let stat;
|
|
225
|
+
try {
|
|
226
|
+
stat = node_fs_1.default.lstatSync(current);
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
232
|
+
return null;
|
|
233
|
+
if (typeof process.getuid === "function" && stat.uid !== process.getuid())
|
|
234
|
+
return null;
|
|
235
|
+
if (index === segments.length - 1 && !secure_metadata_js_1.isWindows && (stat.mode & 0o777) !== 0o700)
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
const authPath = node_path_1.default.join(current, "auth.json");
|
|
239
|
+
return authPath === expected ? authPath : null;
|
|
240
|
+
}
|
|
241
|
+
/** Presence-only check against the Agent-owned official store. Never returns credential bytes. */
|
|
242
|
+
function officialPiHasStoredProvider(configDir, agentId, providerId) {
|
|
243
|
+
const provider = typeof providerId === "string" ? providerId.trim() : "";
|
|
244
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$/.test(provider))
|
|
245
|
+
return false;
|
|
246
|
+
try {
|
|
247
|
+
const authPath = officialPiOwnedAuthPath(configDir, agentId);
|
|
248
|
+
if (!authPath)
|
|
249
|
+
return false;
|
|
250
|
+
const credential = readCredentialData(authPath)[provider];
|
|
251
|
+
return credential?.type === "api_key" || credential?.type === "oauth";
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
function writeCredentialData(authPath, data) {
|
|
258
|
+
ensurePrivateDirectory(node_path_1.default.dirname(authPath));
|
|
259
|
+
const temporary = `${authPath}.${process.pid}.${node_crypto_1.default.randomUUID()}.tmp`;
|
|
260
|
+
try {
|
|
261
|
+
node_fs_1.default.writeFileSync(temporary, `${JSON.stringify(data, null, 2)}\n`, { mode: 0o600, flag: "wx" });
|
|
262
|
+
node_fs_1.default.renameSync(temporary, authPath);
|
|
263
|
+
node_fs_1.default.chmodSync(authPath, 0o600);
|
|
264
|
+
}
|
|
265
|
+
finally {
|
|
266
|
+
try {
|
|
267
|
+
node_fs_1.default.unlinkSync(temporary);
|
|
268
|
+
}
|
|
269
|
+
catch { /* renamed or absent */ }
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
/** Official CredentialStore contract backed by raw auth.json bytes.
|
|
273
|
+
* Reads never resolve `!command` API-key values and absent stores stay absent.
|
|
274
|
+
*/
|
|
275
|
+
class RawFileCredentialStore {
|
|
276
|
+
authPath;
|
|
277
|
+
lockPath;
|
|
278
|
+
constructor(authPath, lockPath) {
|
|
279
|
+
this.authPath = authPath;
|
|
280
|
+
this.lockPath = lockPath;
|
|
281
|
+
}
|
|
282
|
+
read(providerId) {
|
|
283
|
+
if (!node_fs_1.default.existsSync(this.authPath))
|
|
284
|
+
return Promise.resolve(undefined);
|
|
285
|
+
return withCredentialLock(this.lockPath, async () => {
|
|
286
|
+
safeRead(this.authPath); // enforce owner/type/mode before the official raw reader
|
|
287
|
+
return (0, pi_coding_agent_1.readStoredCredential)(providerId, this.authPath);
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
list() {
|
|
291
|
+
if (!node_fs_1.default.existsSync(this.authPath))
|
|
292
|
+
return Promise.resolve([]);
|
|
293
|
+
return withCredentialLock(this.lockPath, async () => Object.entries(readCredentialData(this.authPath)).flatMap(([providerId, credential]) => credential?.type === "api_key" || credential?.type === "oauth" ? [{ providerId, type: credential.type }] : []));
|
|
294
|
+
}
|
|
295
|
+
modify(providerId, fn) {
|
|
296
|
+
ensurePrivateDirectory(node_path_1.default.dirname(this.authPath));
|
|
297
|
+
return withCredentialLock(this.lockPath, async () => {
|
|
298
|
+
const data = readCredentialData(this.authPath);
|
|
299
|
+
const current = data[providerId];
|
|
300
|
+
const next = await fn(current);
|
|
301
|
+
if (next === undefined)
|
|
302
|
+
return current;
|
|
303
|
+
data[providerId] = next;
|
|
304
|
+
writeCredentialData(this.authPath, data);
|
|
305
|
+
return next;
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
delete(providerId) {
|
|
309
|
+
if (!node_fs_1.default.existsSync(this.authPath))
|
|
310
|
+
return Promise.resolve();
|
|
311
|
+
return withCredentialLock(this.lockPath, async () => {
|
|
312
|
+
const data = readCredentialData(this.authPath);
|
|
313
|
+
if (!(providerId in data))
|
|
314
|
+
return;
|
|
315
|
+
delete data[providerId];
|
|
316
|
+
writeCredentialData(this.authPath, data);
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
/** Logout only sees an empty credential view; the official logout mutation is
|
|
321
|
+
* delegated to the raw store for its one requested provider. This prevents
|
|
322
|
+
* ModelRuntime's create/post-logout refresh from resolving unrelated entries.
|
|
323
|
+
*/
|
|
324
|
+
class DeleteOnlyCredentialStore {
|
|
325
|
+
target;
|
|
326
|
+
constructor(target) {
|
|
327
|
+
this.target = target;
|
|
328
|
+
}
|
|
329
|
+
async read() { return undefined; }
|
|
330
|
+
async list() { return []; }
|
|
331
|
+
async modify() { return undefined; }
|
|
332
|
+
delete(providerId) { return this.target.delete(providerId); }
|
|
333
|
+
}
|
|
334
|
+
function beginBuiltinPiCredentialTransaction(configDir, agentId) {
|
|
335
|
+
const directory = (0, pi_provider_config_js_1.piAgentDirectory)(configDir, agentId);
|
|
336
|
+
const lockPath = credentialLockPath(configDir, agentId);
|
|
337
|
+
const existed = node_fs_1.default.existsSync(directory);
|
|
338
|
+
ensurePrivateDirectory(directory);
|
|
339
|
+
const lockOwner = acquireCredentialLockSync(lockPath);
|
|
340
|
+
let snapshots;
|
|
341
|
+
try {
|
|
342
|
+
snapshots = new Map(SNAPSHOT_FILES.map((name) => [name, safeRead(node_path_1.default.join(directory, name))]));
|
|
343
|
+
}
|
|
344
|
+
catch (error) {
|
|
345
|
+
releaseCredentialLock(lockPath, lockOwner);
|
|
346
|
+
throw error;
|
|
347
|
+
}
|
|
348
|
+
let active = true;
|
|
349
|
+
return {
|
|
350
|
+
directory,
|
|
351
|
+
commit() {
|
|
352
|
+
if (!active)
|
|
353
|
+
return;
|
|
354
|
+
active = false;
|
|
355
|
+
releaseCredentialLock(lockPath, lockOwner);
|
|
356
|
+
if (!existed) {
|
|
357
|
+
try {
|
|
358
|
+
node_fs_1.default.rmdirSync(directory);
|
|
359
|
+
}
|
|
360
|
+
catch { /* keep non-empty provider state */ }
|
|
361
|
+
}
|
|
362
|
+
},
|
|
363
|
+
rollback() {
|
|
364
|
+
if (!active)
|
|
365
|
+
return;
|
|
366
|
+
try {
|
|
367
|
+
for (const name of [...SNAPSHOT_FILES].reverse())
|
|
368
|
+
restoreFile(node_path_1.default.join(directory, name), snapshots.get(name) ?? null);
|
|
369
|
+
}
|
|
370
|
+
finally {
|
|
371
|
+
active = false;
|
|
372
|
+
releaseCredentialLock(lockPath, lockOwner);
|
|
373
|
+
}
|
|
374
|
+
if (!existed) {
|
|
375
|
+
try {
|
|
376
|
+
node_fs_1.default.rmdirSync(directory);
|
|
377
|
+
}
|
|
378
|
+
catch { /* keep a non-empty safe directory */ }
|
|
379
|
+
}
|
|
380
|
+
},
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
async function createOfficialRuntime(options) {
|
|
384
|
+
const previousOffline = process.env.PI_OFFLINE;
|
|
385
|
+
process.env.PI_OFFLINE = "1";
|
|
386
|
+
try {
|
|
387
|
+
const runtime = await pi_coding_agent_1.ModelRuntime.create({ ...options, allowModelNetwork: false });
|
|
388
|
+
if (process.env.LARKIN_TEST_BOT_REGISTER_MODULE && process.env.LARKIN_TEST_PI_AUTH_PROVIDER_MODULE) {
|
|
389
|
+
const fixture = await Promise.resolve(`${(0, node_url_1.pathToFileURL)(node_path_1.default.resolve(process.env.LARKIN_TEST_PI_AUTH_PROVIDER_MODULE)).href}`).then(s => __importStar(require(s)));
|
|
390
|
+
await fixture.configure?.(runtime);
|
|
391
|
+
}
|
|
392
|
+
return runtime;
|
|
393
|
+
}
|
|
394
|
+
finally {
|
|
395
|
+
if (previousOffline === undefined)
|
|
396
|
+
delete process.env.PI_OFFLINE;
|
|
397
|
+
else
|
|
398
|
+
process.env.PI_OFFLINE = previousOffline;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
/** Provider catalog only: no Agent credential/model path is opened or created. */
|
|
402
|
+
function createOfficialPiRegistryRuntime() {
|
|
403
|
+
return createOfficialRuntime({ credentials: new pi_ai_1.InMemoryCredentialStore(), modelsPath: null });
|
|
404
|
+
}
|
|
405
|
+
/** Status/logout runtime: raw credentials are injected without resolving API-key commands. */
|
|
406
|
+
function createOfficialPiCredentialRuntime(configDir, agentId) {
|
|
407
|
+
const directory = (0, pi_provider_config_js_1.piAgentDirectory)(configDir, agentId);
|
|
408
|
+
return createOfficialRuntime({
|
|
409
|
+
credentials: new RawFileCredentialStore(node_path_1.default.join(directory, "auth.json"), credentialLockPath(configDir, agentId)),
|
|
410
|
+
modelsPath: null,
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
/** Logout runtime deletes through the official ModelRuntime API while keeping
|
|
414
|
+
* unrelated stored credentials invisible to its automatic refreshes. */
|
|
415
|
+
function createOfficialPiLogoutRuntime(configDir, agentId) {
|
|
416
|
+
const directory = (0, pi_provider_config_js_1.piAgentDirectory)(configDir, agentId);
|
|
417
|
+
const store = new RawFileCredentialStore(node_path_1.default.join(directory, "auth.json"), credentialLockPath(configDir, agentId));
|
|
418
|
+
return createOfficialRuntime({ credentials: new DeleteOnlyCredentialStore(store), modelsPath: null });
|
|
419
|
+
}
|
|
420
|
+
/** Login/request runtime: official auth orchestration with a command-free persistent CredentialStore. */
|
|
421
|
+
function createOfficialPiModelRuntime(configDir, agentId) {
|
|
422
|
+
const directory = (0, pi_provider_config_js_1.piAgentDirectory)(configDir, agentId);
|
|
423
|
+
return createOfficialRuntime({
|
|
424
|
+
credentials: new RawFileCredentialStore(node_path_1.default.join(directory, "auth.json"), credentialLockPath(configDir, agentId)),
|
|
425
|
+
modelsPath: node_fs_1.default.existsSync(node_path_1.default.join(directory, "models.json")) ? node_path_1.default.join(directory, "models.json") : null,
|
|
426
|
+
modelsStorePath: node_path_1.default.join(directory, "models-store.json"),
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
function listOfficialPiAuthProviders(runtime) {
|
|
430
|
+
return runtime.getProviders().map((provider) => {
|
|
431
|
+
const methods = [];
|
|
432
|
+
if (provider.auth.apiKey?.login)
|
|
433
|
+
methods.push({ type: "api_key", name: provider.auth.apiKey.name });
|
|
434
|
+
if (provider.auth.oauth?.login)
|
|
435
|
+
methods.push({ type: "oauth", name: provider.auth.oauth.loginLabel || provider.auth.oauth.name });
|
|
436
|
+
return {
|
|
437
|
+
id: provider.id,
|
|
438
|
+
name: provider.name,
|
|
439
|
+
methods,
|
|
440
|
+
models: provider.getModels().map((model) => `${model.provider}/${model.id}`),
|
|
441
|
+
ambientOnly: methods.length === 0 && Boolean(provider.auth.apiKey),
|
|
442
|
+
};
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
function abortError() {
|
|
446
|
+
const error = new Error("Pi auth login cancelled");
|
|
447
|
+
error.name = "AbortError";
|
|
448
|
+
return error;
|
|
449
|
+
}
|
|
450
|
+
async function abortable(promise, signals) {
|
|
451
|
+
const active = signals.filter((signal) => Boolean(signal));
|
|
452
|
+
if (active.some((signal) => signal.aborted))
|
|
453
|
+
throw abortError();
|
|
454
|
+
let cleanup = () => { };
|
|
455
|
+
const aborted = new Promise((_resolve, reject) => {
|
|
456
|
+
const listener = () => reject(abortError());
|
|
457
|
+
for (const signal of active)
|
|
458
|
+
signal.addEventListener("abort", listener, { once: true });
|
|
459
|
+
cleanup = () => { for (const signal of active)
|
|
460
|
+
signal.removeEventListener("abort", listener); };
|
|
461
|
+
});
|
|
462
|
+
try {
|
|
463
|
+
return await Promise.race([promise, aborted]);
|
|
464
|
+
}
|
|
465
|
+
finally {
|
|
466
|
+
cleanup();
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
function safeUrl(raw) {
|
|
470
|
+
try {
|
|
471
|
+
const url = new URL(raw);
|
|
472
|
+
url.username = "";
|
|
473
|
+
url.password = "";
|
|
474
|
+
url.search = "";
|
|
475
|
+
url.hash = "";
|
|
476
|
+
return url.toString();
|
|
477
|
+
}
|
|
478
|
+
catch {
|
|
479
|
+
return "(invalid URL)";
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
function copyableAuthUrl(raw) {
|
|
483
|
+
try {
|
|
484
|
+
const url = new URL(raw);
|
|
485
|
+
if (url.protocol !== "https:" && url.protocol !== "http:")
|
|
486
|
+
return null;
|
|
487
|
+
if (url.username || url.password || /[\0\r\n]/.test(raw))
|
|
488
|
+
return null;
|
|
489
|
+
return url.toString();
|
|
490
|
+
}
|
|
491
|
+
catch {
|
|
492
|
+
return null;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
function promptLabel(prompt) {
|
|
496
|
+
return `${prompt.message}${"placeholder" in prompt && prompt.placeholder ? `(${prompt.placeholder})` : ""}`;
|
|
497
|
+
}
|
|
498
|
+
function createOfficialPiAuthInteraction(input) {
|
|
499
|
+
const prompt = async (request) => {
|
|
500
|
+
if (request.type === "select") {
|
|
501
|
+
const lines = request.options.map((option, index) => ` ${index + 1}. ${option.label}${option.description ? ` — ${option.description}` : ""}`).join("\n");
|
|
502
|
+
const answer = await abortable(input.questioner.ask(`${request.message}\n${lines}\n> `, request.signal), [input.signal, request.signal]);
|
|
503
|
+
const index = Number(answer.trim()) - 1;
|
|
504
|
+
if (!Number.isInteger(index) || index < 0 || index >= request.options.length)
|
|
505
|
+
throw new Error(`请选择 1-${request.options.length}`);
|
|
506
|
+
return request.options[index].id;
|
|
507
|
+
}
|
|
508
|
+
const question = `${promptLabel(request)}\n> `;
|
|
509
|
+
const operation = request.type === "secret" || request.type === "manual_code"
|
|
510
|
+
? input.questioner.secret(question, request.signal)
|
|
511
|
+
: input.questioner.ask(question, request.signal);
|
|
512
|
+
return abortable(operation, [input.signal, request.signal]);
|
|
513
|
+
};
|
|
514
|
+
const notify = (event) => {
|
|
515
|
+
if (event.type === "info") {
|
|
516
|
+
input.report(event.message);
|
|
517
|
+
for (const link of event.links ?? [])
|
|
518
|
+
input.report(`${link.label || "More information"}: ${safeUrl(link.url)}`);
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
if (event.type === "auth_url") {
|
|
522
|
+
const fullUrl = copyableAuthUrl(event.url);
|
|
523
|
+
const opened = Boolean(fullUrl) && input.openUrl?.(fullUrl) === true;
|
|
524
|
+
input.report(`${opened ? "登录地址已在浏览器打开" : "请复制完整登录地址"}: ${opened ? safeUrl(fullUrl) : fullUrl || "(invalid URL)"}`);
|
|
525
|
+
if (event.instructions)
|
|
526
|
+
input.report(event.instructions);
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
if (event.type === "device_code") {
|
|
530
|
+
input.report(`设备登录码:${event.userCode}`);
|
|
531
|
+
input.report(`验证地址:${safeUrl(event.verificationUri)}`);
|
|
532
|
+
if (event.expiresInSeconds)
|
|
533
|
+
input.report(`有效期:${event.expiresInSeconds} 秒`);
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
input.report(event.message);
|
|
537
|
+
};
|
|
538
|
+
return { signal: input.signal, prompt, notify };
|
|
539
|
+
}
|
|
540
|
+
function runOfficialPiLogin(runtime, providerId, authType, interaction) {
|
|
541
|
+
return runtime.login(providerId, authType, interaction);
|
|
542
|
+
}
|
|
543
|
+
async function verifyOfficialPiProviderTurn(runtime, modelId, signal) {
|
|
544
|
+
const separator = modelId.indexOf("/");
|
|
545
|
+
const providerId = separator > 0 ? modelId.slice(0, separator) : "";
|
|
546
|
+
const providerModelId = separator > 0 ? modelId.slice(separator + 1) : "";
|
|
547
|
+
const model = runtime.getModel(providerId, providerModelId);
|
|
548
|
+
if (!model)
|
|
549
|
+
throw new Error("selected Pi provider model is not registered");
|
|
550
|
+
const timeout = new AbortController();
|
|
551
|
+
const timer = setTimeout(() => timeout.abort(), 60_000);
|
|
552
|
+
const combined = signal ? AbortSignal.any([signal, timeout.signal]) : timeout.signal;
|
|
553
|
+
try {
|
|
554
|
+
const response = await runtime.completeSimple(model, {
|
|
555
|
+
systemPrompt: "This is a setup readiness check. Do not use tools.",
|
|
556
|
+
messages: [{ role: "user", content: "Reply exactly: LARKIN_READY", timestamp: Date.now() }],
|
|
557
|
+
}, { maxTokens: 32, signal: combined });
|
|
558
|
+
const text = response.content.flatMap((entry) => entry.type === "text" ? [entry.text] : []).join("").trim();
|
|
559
|
+
if (response.stopReason === "error" || response.stopReason === "aborted" || !text) {
|
|
560
|
+
throw new Error(response.errorMessage || "provider readiness turn returned no authenticated text");
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
finally {
|
|
564
|
+
clearTimeout(timer);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
function safeSource(value) {
|
|
568
|
+
if (!value || value.length > 120 || /[\0\r\n]/.test(value) || /[?&#=]/.test(value))
|
|
569
|
+
return "configured";
|
|
570
|
+
return value;
|
|
571
|
+
}
|
|
572
|
+
async function officialPiAuthStatus(runtime) {
|
|
573
|
+
const providers = new Map(runtime.getProviders().map((provider) => [provider.id, provider.name]));
|
|
574
|
+
const stored = new Map((await runtime.listCredentials()).map((entry) => [entry.providerId, entry.type]));
|
|
575
|
+
const providerIds = [...new Set([...providers.keys(), ...stored.keys()])];
|
|
576
|
+
const statuses = [];
|
|
577
|
+
for (const providerId of providerIds) {
|
|
578
|
+
const check = await runtime.checkAuth(providerId);
|
|
579
|
+
const credentialType = stored.get(providerId) || check?.type;
|
|
580
|
+
if (!credentialType)
|
|
581
|
+
continue;
|
|
582
|
+
statuses.push({
|
|
583
|
+
providerId,
|
|
584
|
+
providerName: providers.get(providerId) || providerId,
|
|
585
|
+
credentialType,
|
|
586
|
+
source: safeSource(check?.source),
|
|
587
|
+
stored: stored.has(providerId),
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
return statuses;
|
|
591
|
+
}
|
|
592
|
+
function logoutOfficialPiProvider(runtime, providerId) {
|
|
593
|
+
return runtime.logout(providerId);
|
|
594
|
+
}
|
|
@@ -151,6 +151,59 @@ function readCredentialData(authPath) {
|
|
|
151
151
|
throw new Error("Pi auth.json must contain an object");
|
|
152
152
|
return parsed;
|
|
153
153
|
}
|
|
154
|
+
/** 只读校验 Agent 凭证路径:每级必须是当前用户拥有的非 symlink 目录,不创建、不改权限。 */
|
|
155
|
+
function officialPiOwnedAuthPath(configDir, agentId) {
|
|
156
|
+
if (!/^cli_[A-Za-z0-9]+$/.test(agentId))
|
|
157
|
+
return null;
|
|
158
|
+
const root = path.resolve(configDir);
|
|
159
|
+
const expected = path.join(root, "providers", "pi", agentId, "auth.json");
|
|
160
|
+
const segments = ["providers", "pi", agentId];
|
|
161
|
+
let current = root;
|
|
162
|
+
try {
|
|
163
|
+
const rootStat = fs.lstatSync(current);
|
|
164
|
+
if (!rootStat.isDirectory() || rootStat.isSymbolicLink())
|
|
165
|
+
return null;
|
|
166
|
+
if (typeof process.getuid === "function" && rootStat.uid !== process.getuid())
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
for (let index = 0; index < segments.length; index += 1) {
|
|
173
|
+
current = path.join(current, segments[index]);
|
|
174
|
+
let stat;
|
|
175
|
+
try {
|
|
176
|
+
stat = fs.lstatSync(current);
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
182
|
+
return null;
|
|
183
|
+
if (typeof process.getuid === "function" && stat.uid !== process.getuid())
|
|
184
|
+
return null;
|
|
185
|
+
if (index === segments.length - 1 && !isWindows && (stat.mode & 0o777) !== 0o700)
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
const authPath = path.join(current, "auth.json");
|
|
189
|
+
return authPath === expected ? authPath : null;
|
|
190
|
+
}
|
|
191
|
+
/** Presence-only check against the Agent-owned official store. Never returns credential bytes. */
|
|
192
|
+
export function officialPiHasStoredProvider(configDir, agentId, providerId) {
|
|
193
|
+
const provider = typeof providerId === "string" ? providerId.trim() : "";
|
|
194
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$/.test(provider))
|
|
195
|
+
return false;
|
|
196
|
+
try {
|
|
197
|
+
const authPath = officialPiOwnedAuthPath(configDir, agentId);
|
|
198
|
+
if (!authPath)
|
|
199
|
+
return false;
|
|
200
|
+
const credential = readCredentialData(authPath)[provider];
|
|
201
|
+
return credential?.type === "api_key" || credential?.type === "oauth";
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
154
207
|
function writeCredentialData(authPath, data) {
|
|
155
208
|
ensurePrivateDirectory(path.dirname(authPath));
|
|
156
209
|
const temporary = `${authPath}.${process.pid}.${crypto.randomUUID()}.tmp`;
|