larkin 0.2.70
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/CONTRIBUTING.md +48 -0
- package/LICENSE +201 -0
- package/README.md +122 -0
- package/SECURITY.md +14 -0
- package/artifacts/release/THIRD_PARTY_NOTICES.txt +5488 -0
- package/dist/agent/agent-cli-capabilities.cjs +43 -0
- package/dist/agent/agent-cli-capabilities.mjs +35 -0
- package/dist/agent/agent-state-store.cjs +961 -0
- package/dist/agent/agent-state-store.mjs +923 -0
- package/dist/agent/agent-transport.cjs +201 -0
- package/dist/agent/agent-transport.mjs +194 -0
- package/dist/agent/config-cli-contract.cjs +19 -0
- package/dist/agent/config-cli-contract.mjs +16 -0
- package/dist/agent/context-prompt.cjs +159 -0
- package/dist/agent/context-prompt.mjs +155 -0
- package/dist/agent/freshness-gate.mjs +24 -0
- package/dist/agent/history-projection.cjs +72 -0
- package/dist/agent/history-projection.mjs +65 -0
- package/dist/agent/host-reminder-orchestrator.cjs +289 -0
- package/dist/agent/host-reminder-orchestrator.mjs +252 -0
- package/dist/agent/inbox-projection.cjs +92 -0
- package/dist/agent/inbox-projection.mjs +85 -0
- package/dist/agent/interaction-state-machine.cjs +710 -0
- package/dist/agent/interaction-state-machine.mjs +705 -0
- package/dist/agent/reminder-routes.cjs +192 -0
- package/dist/agent/reminder-routes.mjs +156 -0
- package/dist/agent/reminder-store.cjs +220 -0
- package/dist/agent/reminder-store.mjs +205 -0
- package/dist/agent/transport-business-context.cjs +486 -0
- package/dist/agent/transport-business-context.mjs +446 -0
- package/dist/agent/transport-shell.cjs +45 -0
- package/dist/agent/transport-shell.mjs +39 -0
- package/dist/app/agent-cli.mjs +596 -0
- package/dist/app/agent-config.mjs +633 -0
- package/dist/app/agent-lark-cli-workspace.cjs +95 -0
- package/dist/app/agent-lark-cli-workspace.mjs +85 -0
- package/dist/app/agent-readiness.cjs +42 -0
- package/dist/app/agent-readiness.mjs +38 -0
- package/dist/app/binary-entry.mjs +86 -0
- package/dist/app/cli.mjs +176 -0
- package/dist/app/dashboard.mjs +205 -0
- package/dist/app/internal-command.cjs +57 -0
- package/dist/app/internal-command.mjs +48 -0
- package/dist/app/lark-channel-secret.mjs +22 -0
- package/dist/app/lark-cli.mjs +887 -0
- package/dist/app/lark.mjs +45 -0
- package/dist/app/local-control.mjs +728 -0
- package/dist/app/official-lark-cli.cjs +138 -0
- package/dist/app/official-lark-cli.mjs +128 -0
- package/dist/app/pi-auth-cli.mjs +67 -0
- package/dist/app/run.mjs +364 -0
- package/dist/app/runtime-agent-config.mjs +416 -0
- package/dist/app/runtime-model-directory.mjs +78 -0
- package/dist/app/runtime-process.mjs +113 -0
- package/dist/app/session-cli.mjs +83 -0
- package/dist/app/setup-dashboard.mjs +26 -0
- package/dist/app/setup.mjs +166 -0
- package/dist/app/telemetry.mjs +83 -0
- package/dist/dashboard/dashboard-assets.mjs +21 -0
- package/dist/dashboard/dashboard-avatar.mjs +128 -0
- package/dist/dashboard/dashboard-config-controller.mjs +525 -0
- package/dist/dashboard/dashboard-lifecycle.cjs +97 -0
- package/dist/dashboard/dashboard-lifecycle.mjs +88 -0
- package/dist/dashboard/dashboard-template.mjs +29 -0
- package/dist/dashboard/dashboard-view-model.mjs +556 -0
- package/dist/dashboard/dashboard-workspace.mjs +104 -0
- package/dist/dashboard/web/assets/dashboard.css +3 -0
- package/dist/dashboard/web/assets/dashboard.js +49 -0
- package/dist/feishu/channel-transport.cjs +151 -0
- package/dist/feishu/channel-transport.mjs +148 -0
- package/dist/feishu/document-comment.cjs +120 -0
- package/dist/feishu/document-comment.mjs +109 -0
- package/dist/feishu/host-business-state.cjs +477 -0
- package/dist/feishu/host-business-state.mjs +462 -0
- package/dist/feishu/host-channel-business.cjs +161 -0
- package/dist/feishu/host-channel-business.mjs +157 -0
- package/dist/feishu/host-processing-eye.cjs +185 -0
- package/dist/feishu/host-processing-eye.mjs +181 -0
- package/dist/feishu/host-shell.cjs +1425 -0
- package/dist/feishu/host-shell.mjs +1385 -0
- package/dist/feishu/im-freshness-adapter.mjs +89 -0
- package/dist/feishu/interaction-orchestrator.cjs +279 -0
- package/dist/feishu/interaction-orchestrator.mjs +242 -0
- package/dist/feishu/lark-passthrough.mjs +70 -0
- package/dist/feishu/message-policy.cjs +111 -0
- package/dist/feishu/message-policy.mjs +98 -0
- package/dist/feishu/outbound-transport.cjs +221 -0
- package/dist/feishu/outbound-transport.mjs +215 -0
- package/dist/platform/build-info.cjs +25 -0
- package/dist/platform/build-info.mjs +18 -0
- package/dist/platform/callback-capability.cjs +301 -0
- package/dist/platform/callback-capability.mjs +252 -0
- package/dist/platform/check-bun-version.cjs +12 -0
- package/dist/platform/config.cjs +751 -0
- package/dist/platform/config.mjs +694 -0
- package/dist/platform/process-inspect.cjs +89 -0
- package/dist/platform/process-state.cjs +237 -0
- package/dist/platform/process-state.mjs +223 -0
- package/dist/platform/release-artifacts.mjs +61 -0
- package/dist/platform/root-layout.cjs +111 -0
- package/dist/platform/root-layout.mjs +72 -0
- package/dist/platform/telemetry-config.cjs +64 -0
- package/dist/platform/telemetry-config.mjs +56 -0
- package/dist/platform/telemetry-spool.cjs +661 -0
- package/dist/platform/telemetry-spool.mjs +653 -0
- package/dist/platform/telemetry-tracing.cjs +602 -0
- package/dist/platform/telemetry-tracing.mjs +596 -0
- package/dist/platform/telemetry-uploader.cjs +150 -0
- package/dist/platform/telemetry-uploader.mjs +146 -0
- package/dist/platform/workspace-service.cjs +469 -0
- package/dist/platform/workspace-service.mjs +430 -0
- package/dist/runtime/builtin-pi-assets.mjs +33 -0
- package/dist/runtime/claude-model-catalog.cjs +102 -0
- package/dist/runtime/claude-model-catalog.mjs +99 -0
- package/dist/runtime/codex-model-catalog.cjs +108 -0
- package/dist/runtime/codex-model-catalog.mjs +105 -0
- package/dist/runtime/pi-bash-timeout-extension.mjs +60 -0
- package/dist/runtime/pi-bash-timeout-injection.mjs +60 -0
- package/dist/runtime/pi-bash-timeout.bundle.js +43 -0
- package/dist/runtime/pi-model-catalog.cjs +116 -0
- package/dist/runtime/pi-model-catalog.mjs +75 -0
- package/dist/runtime/pi-official-auth.mjs +480 -0
- package/dist/runtime/pi-provider-config.cjs +252 -0
- package/dist/runtime/pi-provider-config.mjs +239 -0
- package/dist/runtime/pi-rpc-client.cjs +255 -0
- package/dist/runtime/pi-rpc-client.mjs +251 -0
- package/dist/runtime/pi-subagent-injection.mjs +129 -0
- package/dist/runtime/pi-subagents.bundle.js +8990 -0
- package/dist/runtime/runtime-adapters.mjs +1019 -0
- package/dist/runtime/runtime-contracts.cjs +2 -0
- package/dist/runtime/runtime-contracts.mjs +1 -0
- package/dist/runtime/runtime-host.cjs +930 -0
- package/dist/runtime/runtime-host.mjs +922 -0
- package/dist/runtime/runtime-model-catalog.cjs +43 -0
- package/dist/runtime/runtime-model-catalog.mjs +40 -0
- package/dist/runtime/runtime-readiness.cjs +177 -0
- package/dist/runtime/runtime-readiness.mjs +133 -0
- package/dist/setup/bot-register.mjs +655 -0
- package/dist/setup/grant-scopes.mjs +133 -0
- package/dist/setup/run-credential-preflight.mjs +89 -0
- package/dist/setup/setup-agent-choice.mjs +172 -0
- package/dist/setup/setup-bind.mjs +337 -0
- package/dist/setup/setup-binding.mjs +52 -0
- package/package.json +112 -0
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import crypto from "node:crypto";
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
5
|
+
// proper-lockfile does not publish TypeScript declarations.
|
|
6
|
+
// @ts-expect-error bundled CommonJS dependency
|
|
7
|
+
import properLockfile from "proper-lockfile";
|
|
8
|
+
import { ModelRuntime, readStoredCredential } from "@earendil-works/pi-coding-agent";
|
|
9
|
+
import { InMemoryCredentialStore } from "@earendil-works/pi-ai";
|
|
10
|
+
import { piAgentDirectory } from "./pi-provider-config.mjs";
|
|
11
|
+
const SNAPSHOT_FILES = ["auth.json", "models.json", "models-store.json"];
|
|
12
|
+
const LOCK_WAIT_MS = 15_000;
|
|
13
|
+
const LOCK_RETRY_MS = 25;
|
|
14
|
+
const heldCredentialLocks = new Map();
|
|
15
|
+
const credentialQueues = new Map();
|
|
16
|
+
function credentialLockPath(configDir, agentId) {
|
|
17
|
+
if (!/^cli_[A-Za-z0-9]+$/.test(agentId))
|
|
18
|
+
throw new Error("invalid Agent ID for Pi credential lock");
|
|
19
|
+
return path.join(piAgentDirectory(configDir, agentId), "auth.json");
|
|
20
|
+
}
|
|
21
|
+
function ensureLockDirectory(lockPath) {
|
|
22
|
+
const directory = path.dirname(lockPath);
|
|
23
|
+
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
24
|
+
fs.chmodSync(directory, 0o700);
|
|
25
|
+
const stat = fs.lstatSync(directory);
|
|
26
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()
|
|
27
|
+
|| (typeof process.getuid === "function" && stat.uid !== process.getuid())
|
|
28
|
+
|| (stat.mode & 0o777) !== 0o700)
|
|
29
|
+
throw new Error("Pi credential lock directory must be an owned 0700 directory");
|
|
30
|
+
}
|
|
31
|
+
function acquireCredentialLockSync(lockPath) {
|
|
32
|
+
const existing = heldCredentialLocks.get(lockPath);
|
|
33
|
+
if (existing) {
|
|
34
|
+
existing.depth += 1;
|
|
35
|
+
return existing.owner;
|
|
36
|
+
}
|
|
37
|
+
ensureLockDirectory(lockPath);
|
|
38
|
+
const owner = { pid: process.pid, token: crypto.randomUUID(), createdAt: Date.now() };
|
|
39
|
+
const deadline = Date.now() + LOCK_WAIT_MS;
|
|
40
|
+
while (true) {
|
|
41
|
+
try {
|
|
42
|
+
const release = properLockfile.lockSync(lockPath, { realpath: false, stale: 30_000, update: 10_000 });
|
|
43
|
+
heldCredentialLocks.set(lockPath, { owner, depth: 1, release });
|
|
44
|
+
return owner;
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
if (error.code !== "ELOCKED")
|
|
48
|
+
throw error;
|
|
49
|
+
if (Date.now() >= deadline)
|
|
50
|
+
throw new Error("Pi credential store is locked by another live process");
|
|
51
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, LOCK_RETRY_MS);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function releaseCredentialLock(lockPath, owner) {
|
|
56
|
+
const held = heldCredentialLocks.get(lockPath);
|
|
57
|
+
if (!held || held.owner.token !== owner.token)
|
|
58
|
+
throw new Error("Pi credential lock ownership mismatch");
|
|
59
|
+
held.depth -= 1;
|
|
60
|
+
if (held.depth > 0)
|
|
61
|
+
return;
|
|
62
|
+
try {
|
|
63
|
+
held.release();
|
|
64
|
+
}
|
|
65
|
+
finally {
|
|
66
|
+
heldCredentialLocks.delete(lockPath);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function withCredentialLock(lockPath, operation) {
|
|
70
|
+
const previous = credentialQueues.get(lockPath) ?? Promise.resolve();
|
|
71
|
+
const result = previous.catch(() => undefined).then(async () => {
|
|
72
|
+
const owner = acquireCredentialLockSync(lockPath);
|
|
73
|
+
try {
|
|
74
|
+
return await operation();
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
releaseCredentialLock(lockPath, owner);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
credentialQueues.set(lockPath, result.catch(() => undefined));
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
83
|
+
function ensurePrivateDirectory(directory) {
|
|
84
|
+
fs.mkdirSync(path.dirname(directory), { recursive: true, mode: 0o700 });
|
|
85
|
+
fs.chmodSync(path.dirname(directory), 0o700);
|
|
86
|
+
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
87
|
+
fs.chmodSync(directory, 0o700);
|
|
88
|
+
const stat = fs.lstatSync(directory);
|
|
89
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()
|
|
90
|
+
|| (typeof process.getuid === "function" && stat.uid !== process.getuid())
|
|
91
|
+
|| (stat.mode & 0o777) !== 0o700) {
|
|
92
|
+
throw new Error("Pi official credential directory must be an owned 0700 directory");
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function safeRead(file) {
|
|
96
|
+
try {
|
|
97
|
+
const stat = fs.lstatSync(file);
|
|
98
|
+
if (!stat.isFile() || stat.isSymbolicLink()
|
|
99
|
+
|| (typeof process.getuid === "function" && stat.uid !== process.getuid())
|
|
100
|
+
|| (stat.mode & 0o777) !== 0o600)
|
|
101
|
+
throw new Error(`unsafe Pi auth file: ${path.basename(file)}`);
|
|
102
|
+
return fs.readFileSync(file);
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
if (error.code === "ENOENT")
|
|
106
|
+
return null;
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function restoreFile(file, bytes) {
|
|
111
|
+
if (bytes === null) {
|
|
112
|
+
try {
|
|
113
|
+
fs.unlinkSync(file);
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
if (error.code !== "ENOENT")
|
|
117
|
+
throw error;
|
|
118
|
+
}
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const temporary = `${file}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
122
|
+
try {
|
|
123
|
+
fs.writeFileSync(temporary, bytes, { mode: 0o600, flag: "wx" });
|
|
124
|
+
fs.renameSync(temporary, file);
|
|
125
|
+
fs.chmodSync(file, 0o600);
|
|
126
|
+
}
|
|
127
|
+
finally {
|
|
128
|
+
try {
|
|
129
|
+
fs.unlinkSync(temporary);
|
|
130
|
+
}
|
|
131
|
+
catch { /* renamed or absent */ }
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function readCredentialData(authPath) {
|
|
135
|
+
const bytes = safeRead(authPath);
|
|
136
|
+
if (bytes === null)
|
|
137
|
+
return {};
|
|
138
|
+
const parsed = JSON.parse(bytes.toString("utf8"));
|
|
139
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
140
|
+
throw new Error("Pi auth.json must contain an object");
|
|
141
|
+
return parsed;
|
|
142
|
+
}
|
|
143
|
+
function writeCredentialData(authPath, data) {
|
|
144
|
+
ensurePrivateDirectory(path.dirname(authPath));
|
|
145
|
+
const temporary = `${authPath}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
146
|
+
try {
|
|
147
|
+
fs.writeFileSync(temporary, `${JSON.stringify(data, null, 2)}\n`, { mode: 0o600, flag: "wx" });
|
|
148
|
+
fs.renameSync(temporary, authPath);
|
|
149
|
+
fs.chmodSync(authPath, 0o600);
|
|
150
|
+
}
|
|
151
|
+
finally {
|
|
152
|
+
try {
|
|
153
|
+
fs.unlinkSync(temporary);
|
|
154
|
+
}
|
|
155
|
+
catch { /* renamed or absent */ }
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/** Official CredentialStore contract backed by raw auth.json bytes.
|
|
159
|
+
* Reads never resolve `!command` API-key values and absent stores stay absent.
|
|
160
|
+
*/
|
|
161
|
+
class RawFileCredentialStore {
|
|
162
|
+
authPath;
|
|
163
|
+
lockPath;
|
|
164
|
+
constructor(authPath, lockPath) {
|
|
165
|
+
this.authPath = authPath;
|
|
166
|
+
this.lockPath = lockPath;
|
|
167
|
+
}
|
|
168
|
+
read(providerId) {
|
|
169
|
+
if (!fs.existsSync(this.authPath))
|
|
170
|
+
return Promise.resolve(undefined);
|
|
171
|
+
return withCredentialLock(this.lockPath, async () => {
|
|
172
|
+
safeRead(this.authPath); // enforce owner/type/mode before the official raw reader
|
|
173
|
+
return readStoredCredential(providerId, this.authPath);
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
list() {
|
|
177
|
+
if (!fs.existsSync(this.authPath))
|
|
178
|
+
return Promise.resolve([]);
|
|
179
|
+
return withCredentialLock(this.lockPath, async () => Object.entries(readCredentialData(this.authPath)).flatMap(([providerId, credential]) => credential?.type === "api_key" || credential?.type === "oauth" ? [{ providerId, type: credential.type }] : []));
|
|
180
|
+
}
|
|
181
|
+
modify(providerId, fn) {
|
|
182
|
+
ensurePrivateDirectory(path.dirname(this.authPath));
|
|
183
|
+
return withCredentialLock(this.lockPath, async () => {
|
|
184
|
+
const data = readCredentialData(this.authPath);
|
|
185
|
+
const current = data[providerId];
|
|
186
|
+
const next = await fn(current);
|
|
187
|
+
if (next === undefined)
|
|
188
|
+
return current;
|
|
189
|
+
data[providerId] = next;
|
|
190
|
+
writeCredentialData(this.authPath, data);
|
|
191
|
+
return next;
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
delete(providerId) {
|
|
195
|
+
if (!fs.existsSync(this.authPath))
|
|
196
|
+
return Promise.resolve();
|
|
197
|
+
return withCredentialLock(this.lockPath, async () => {
|
|
198
|
+
const data = readCredentialData(this.authPath);
|
|
199
|
+
if (!(providerId in data))
|
|
200
|
+
return;
|
|
201
|
+
delete data[providerId];
|
|
202
|
+
writeCredentialData(this.authPath, data);
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
/** Logout only sees an empty credential view; the official logout mutation is
|
|
207
|
+
* delegated to the raw store for its one requested provider. This prevents
|
|
208
|
+
* ModelRuntime's create/post-logout refresh from resolving unrelated entries.
|
|
209
|
+
*/
|
|
210
|
+
class DeleteOnlyCredentialStore {
|
|
211
|
+
target;
|
|
212
|
+
constructor(target) {
|
|
213
|
+
this.target = target;
|
|
214
|
+
}
|
|
215
|
+
async read() { return undefined; }
|
|
216
|
+
async list() { return []; }
|
|
217
|
+
async modify() { return undefined; }
|
|
218
|
+
delete(providerId) { return this.target.delete(providerId); }
|
|
219
|
+
}
|
|
220
|
+
export function beginBuiltinPiCredentialTransaction(configDir, agentId) {
|
|
221
|
+
const directory = piAgentDirectory(configDir, agentId);
|
|
222
|
+
const lockPath = credentialLockPath(configDir, agentId);
|
|
223
|
+
const existed = fs.existsSync(directory);
|
|
224
|
+
ensurePrivateDirectory(directory);
|
|
225
|
+
const lockOwner = acquireCredentialLockSync(lockPath);
|
|
226
|
+
let snapshots;
|
|
227
|
+
try {
|
|
228
|
+
snapshots = new Map(SNAPSHOT_FILES.map((name) => [name, safeRead(path.join(directory, name))]));
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
releaseCredentialLock(lockPath, lockOwner);
|
|
232
|
+
throw error;
|
|
233
|
+
}
|
|
234
|
+
let active = true;
|
|
235
|
+
return {
|
|
236
|
+
directory,
|
|
237
|
+
commit() {
|
|
238
|
+
if (!active)
|
|
239
|
+
return;
|
|
240
|
+
active = false;
|
|
241
|
+
releaseCredentialLock(lockPath, lockOwner);
|
|
242
|
+
if (!existed) {
|
|
243
|
+
try {
|
|
244
|
+
fs.rmdirSync(directory);
|
|
245
|
+
}
|
|
246
|
+
catch { /* keep non-empty provider state */ }
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
rollback() {
|
|
250
|
+
if (!active)
|
|
251
|
+
return;
|
|
252
|
+
try {
|
|
253
|
+
for (const name of [...SNAPSHOT_FILES].reverse())
|
|
254
|
+
restoreFile(path.join(directory, name), snapshots.get(name) ?? null);
|
|
255
|
+
}
|
|
256
|
+
finally {
|
|
257
|
+
active = false;
|
|
258
|
+
releaseCredentialLock(lockPath, lockOwner);
|
|
259
|
+
}
|
|
260
|
+
if (!existed) {
|
|
261
|
+
try {
|
|
262
|
+
fs.rmdirSync(directory);
|
|
263
|
+
}
|
|
264
|
+
catch { /* keep a non-empty safe directory */ }
|
|
265
|
+
}
|
|
266
|
+
},
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
async function createOfficialRuntime(options) {
|
|
270
|
+
const previousOffline = process.env.PI_OFFLINE;
|
|
271
|
+
process.env.PI_OFFLINE = "1";
|
|
272
|
+
try {
|
|
273
|
+
const runtime = await ModelRuntime.create({ ...options, allowModelNetwork: false });
|
|
274
|
+
if (process.env.LARKIN_TEST_BOT_REGISTER_MODULE && process.env.LARKIN_TEST_PI_AUTH_PROVIDER_MODULE) {
|
|
275
|
+
const fixture = await import(pathToFileURL(path.resolve(process.env.LARKIN_TEST_PI_AUTH_PROVIDER_MODULE)).href);
|
|
276
|
+
await fixture.configure?.(runtime);
|
|
277
|
+
}
|
|
278
|
+
return runtime;
|
|
279
|
+
}
|
|
280
|
+
finally {
|
|
281
|
+
if (previousOffline === undefined)
|
|
282
|
+
delete process.env.PI_OFFLINE;
|
|
283
|
+
else
|
|
284
|
+
process.env.PI_OFFLINE = previousOffline;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
/** Provider catalog only: no Agent credential/model path is opened or created. */
|
|
288
|
+
export function createOfficialPiRegistryRuntime() {
|
|
289
|
+
return createOfficialRuntime({ credentials: new InMemoryCredentialStore(), modelsPath: null });
|
|
290
|
+
}
|
|
291
|
+
/** Status/logout runtime: raw credentials are injected without resolving API-key commands. */
|
|
292
|
+
export function createOfficialPiCredentialRuntime(configDir, agentId) {
|
|
293
|
+
const directory = piAgentDirectory(configDir, agentId);
|
|
294
|
+
return createOfficialRuntime({
|
|
295
|
+
credentials: new RawFileCredentialStore(path.join(directory, "auth.json"), credentialLockPath(configDir, agentId)),
|
|
296
|
+
modelsPath: null,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
/** Logout runtime deletes through the official ModelRuntime API while keeping
|
|
300
|
+
* unrelated stored credentials invisible to its automatic refreshes. */
|
|
301
|
+
export function createOfficialPiLogoutRuntime(configDir, agentId) {
|
|
302
|
+
const directory = piAgentDirectory(configDir, agentId);
|
|
303
|
+
const store = new RawFileCredentialStore(path.join(directory, "auth.json"), credentialLockPath(configDir, agentId));
|
|
304
|
+
return createOfficialRuntime({ credentials: new DeleteOnlyCredentialStore(store), modelsPath: null });
|
|
305
|
+
}
|
|
306
|
+
/** Login/request runtime: official auth orchestration with a command-free persistent CredentialStore. */
|
|
307
|
+
export function createOfficialPiModelRuntime(configDir, agentId) {
|
|
308
|
+
const directory = piAgentDirectory(configDir, agentId);
|
|
309
|
+
return createOfficialRuntime({
|
|
310
|
+
credentials: new RawFileCredentialStore(path.join(directory, "auth.json"), credentialLockPath(configDir, agentId)),
|
|
311
|
+
modelsPath: fs.existsSync(path.join(directory, "models.json")) ? path.join(directory, "models.json") : null,
|
|
312
|
+
modelsStorePath: path.join(directory, "models-store.json"),
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
export function listOfficialPiAuthProviders(runtime) {
|
|
316
|
+
return runtime.getProviders().map((provider) => {
|
|
317
|
+
const methods = [];
|
|
318
|
+
if (provider.auth.apiKey?.login)
|
|
319
|
+
methods.push({ type: "api_key", name: provider.auth.apiKey.name });
|
|
320
|
+
if (provider.auth.oauth?.login)
|
|
321
|
+
methods.push({ type: "oauth", name: provider.auth.oauth.loginLabel || provider.auth.oauth.name });
|
|
322
|
+
return {
|
|
323
|
+
id: provider.id,
|
|
324
|
+
name: provider.name,
|
|
325
|
+
methods,
|
|
326
|
+
models: provider.getModels().map((model) => `${model.provider}/${model.id}`),
|
|
327
|
+
ambientOnly: methods.length === 0 && Boolean(provider.auth.apiKey),
|
|
328
|
+
};
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
function abortError() {
|
|
332
|
+
const error = new Error("Pi auth login cancelled");
|
|
333
|
+
error.name = "AbortError";
|
|
334
|
+
return error;
|
|
335
|
+
}
|
|
336
|
+
async function abortable(promise, signals) {
|
|
337
|
+
const active = signals.filter((signal) => Boolean(signal));
|
|
338
|
+
if (active.some((signal) => signal.aborted))
|
|
339
|
+
throw abortError();
|
|
340
|
+
let cleanup = () => { };
|
|
341
|
+
const aborted = new Promise((_resolve, reject) => {
|
|
342
|
+
const listener = () => reject(abortError());
|
|
343
|
+
for (const signal of active)
|
|
344
|
+
signal.addEventListener("abort", listener, { once: true });
|
|
345
|
+
cleanup = () => { for (const signal of active)
|
|
346
|
+
signal.removeEventListener("abort", listener); };
|
|
347
|
+
});
|
|
348
|
+
try {
|
|
349
|
+
return await Promise.race([promise, aborted]);
|
|
350
|
+
}
|
|
351
|
+
finally {
|
|
352
|
+
cleanup();
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
function safeUrl(raw) {
|
|
356
|
+
try {
|
|
357
|
+
const url = new URL(raw);
|
|
358
|
+
url.username = "";
|
|
359
|
+
url.password = "";
|
|
360
|
+
url.search = "";
|
|
361
|
+
url.hash = "";
|
|
362
|
+
return url.toString();
|
|
363
|
+
}
|
|
364
|
+
catch {
|
|
365
|
+
return "(invalid URL)";
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
function copyableAuthUrl(raw) {
|
|
369
|
+
try {
|
|
370
|
+
const url = new URL(raw);
|
|
371
|
+
if (url.protocol !== "https:" && url.protocol !== "http:")
|
|
372
|
+
return null;
|
|
373
|
+
if (url.username || url.password || /[\0\r\n]/.test(raw))
|
|
374
|
+
return null;
|
|
375
|
+
return url.toString();
|
|
376
|
+
}
|
|
377
|
+
catch {
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
function promptLabel(prompt) {
|
|
382
|
+
return `${prompt.message}${"placeholder" in prompt && prompt.placeholder ? `(${prompt.placeholder})` : ""}`;
|
|
383
|
+
}
|
|
384
|
+
export function createOfficialPiAuthInteraction(input) {
|
|
385
|
+
const prompt = async (request) => {
|
|
386
|
+
if (request.type === "select") {
|
|
387
|
+
const lines = request.options.map((option, index) => ` ${index + 1}. ${option.label}${option.description ? ` — ${option.description}` : ""}`).join("\n");
|
|
388
|
+
const answer = await abortable(input.questioner.ask(`${request.message}\n${lines}\n> `, request.signal), [input.signal, request.signal]);
|
|
389
|
+
const index = Number(answer.trim()) - 1;
|
|
390
|
+
if (!Number.isInteger(index) || index < 0 || index >= request.options.length)
|
|
391
|
+
throw new Error(`请选择 1-${request.options.length}`);
|
|
392
|
+
return request.options[index].id;
|
|
393
|
+
}
|
|
394
|
+
const question = `${promptLabel(request)}\n> `;
|
|
395
|
+
const operation = request.type === "secret" || request.type === "manual_code"
|
|
396
|
+
? input.questioner.secret(question, request.signal)
|
|
397
|
+
: input.questioner.ask(question, request.signal);
|
|
398
|
+
return abortable(operation, [input.signal, request.signal]);
|
|
399
|
+
};
|
|
400
|
+
const notify = (event) => {
|
|
401
|
+
if (event.type === "info") {
|
|
402
|
+
input.report(event.message);
|
|
403
|
+
for (const link of event.links ?? [])
|
|
404
|
+
input.report(`${link.label || "More information"}: ${safeUrl(link.url)}`);
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
if (event.type === "auth_url") {
|
|
408
|
+
const fullUrl = copyableAuthUrl(event.url);
|
|
409
|
+
const opened = Boolean(fullUrl) && input.openUrl?.(fullUrl) === true;
|
|
410
|
+
input.report(`${opened ? "登录地址已在浏览器打开" : "请复制完整登录地址"}: ${opened ? safeUrl(fullUrl) : fullUrl || "(invalid URL)"}`);
|
|
411
|
+
if (event.instructions)
|
|
412
|
+
input.report(event.instructions);
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
if (event.type === "device_code") {
|
|
416
|
+
input.report(`设备登录码:${event.userCode}`);
|
|
417
|
+
input.report(`验证地址:${safeUrl(event.verificationUri)}`);
|
|
418
|
+
if (event.expiresInSeconds)
|
|
419
|
+
input.report(`有效期:${event.expiresInSeconds} 秒`);
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
input.report(event.message);
|
|
423
|
+
};
|
|
424
|
+
return { signal: input.signal, prompt, notify };
|
|
425
|
+
}
|
|
426
|
+
export function runOfficialPiLogin(runtime, providerId, authType, interaction) {
|
|
427
|
+
return runtime.login(providerId, authType, interaction);
|
|
428
|
+
}
|
|
429
|
+
export async function verifyOfficialPiProviderTurn(runtime, modelId, signal) {
|
|
430
|
+
const separator = modelId.indexOf("/");
|
|
431
|
+
const providerId = separator > 0 ? modelId.slice(0, separator) : "";
|
|
432
|
+
const providerModelId = separator > 0 ? modelId.slice(separator + 1) : "";
|
|
433
|
+
const model = runtime.getModel(providerId, providerModelId);
|
|
434
|
+
if (!model)
|
|
435
|
+
throw new Error("selected Pi provider model is not registered");
|
|
436
|
+
const timeout = new AbortController();
|
|
437
|
+
const timer = setTimeout(() => timeout.abort(), 60_000);
|
|
438
|
+
const combined = signal ? AbortSignal.any([signal, timeout.signal]) : timeout.signal;
|
|
439
|
+
try {
|
|
440
|
+
const response = await runtime.completeSimple(model, {
|
|
441
|
+
systemPrompt: "This is a setup readiness check. Do not use tools.",
|
|
442
|
+
messages: [{ role: "user", content: "Reply exactly: LARKIN_READY", timestamp: Date.now() }],
|
|
443
|
+
}, { maxTokens: 32, signal: combined });
|
|
444
|
+
const text = response.content.flatMap((entry) => entry.type === "text" ? [entry.text] : []).join("").trim();
|
|
445
|
+
if (response.stopReason === "error" || response.stopReason === "aborted" || !text) {
|
|
446
|
+
throw new Error(response.errorMessage || "provider readiness turn returned no authenticated text");
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
finally {
|
|
450
|
+
clearTimeout(timer);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
function safeSource(value) {
|
|
454
|
+
if (!value || value.length > 120 || /[\0\r\n]/.test(value) || /[?&#=]/.test(value))
|
|
455
|
+
return "configured";
|
|
456
|
+
return value;
|
|
457
|
+
}
|
|
458
|
+
export async function officialPiAuthStatus(runtime) {
|
|
459
|
+
const providers = new Map(runtime.getProviders().map((provider) => [provider.id, provider.name]));
|
|
460
|
+
const stored = new Map((await runtime.listCredentials()).map((entry) => [entry.providerId, entry.type]));
|
|
461
|
+
const providerIds = [...new Set([...providers.keys(), ...stored.keys()])];
|
|
462
|
+
const statuses = [];
|
|
463
|
+
for (const providerId of providerIds) {
|
|
464
|
+
const check = await runtime.checkAuth(providerId);
|
|
465
|
+
const credentialType = stored.get(providerId) || check?.type;
|
|
466
|
+
if (!credentialType)
|
|
467
|
+
continue;
|
|
468
|
+
statuses.push({
|
|
469
|
+
providerId,
|
|
470
|
+
providerName: providers.get(providerId) || providerId,
|
|
471
|
+
credentialType,
|
|
472
|
+
source: safeSource(check?.source),
|
|
473
|
+
stored: stored.has(providerId),
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
return statuses;
|
|
477
|
+
}
|
|
478
|
+
export function logoutOfficialPiProvider(runtime, providerId) {
|
|
479
|
+
return runtime.logout(providerId);
|
|
480
|
+
}
|