larkin 0.4.28 → 0.5.0
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/CHANGELOG.md +11 -0
- package/README.md +5 -9
- package/artifacts/release/THIRD_PARTY_NOTICES.txt +751 -4425
- package/dist/app/agent-cli.mjs +14 -2
- package/dist/app/agent-config.mjs +26 -84
- package/dist/app/binary-entry.mjs +0 -20
- package/dist/app/cli.mjs +8 -14
- package/dist/app/internal-command.cjs +0 -2
- package/dist/app/internal-command.mjs +0 -2
- package/dist/app/local-control.mjs +16 -3
- package/dist/app/runtime-model-directory.mjs +2 -16
- package/dist/app/runtime-process.mjs +42 -6
- package/dist/app/setup.mjs +52 -57
- package/dist/dashboard/dashboard-config-controller.mjs +83 -24
- package/dist/dashboard/dashboard-view-model.mjs +4 -7
- package/dist/dashboard/web/assets/dashboard.js +1 -1
- package/dist/feishu/host-shell.cjs +68 -17
- package/dist/feishu/host-shell.mjs +69 -18
- package/dist/platform/config.cjs +201 -138
- package/dist/platform/config.mjs +199 -138
- package/dist/runtime/pi-artifact-provenance.mjs +3 -3
- package/dist/runtime/pi-bash-timeout-extension.mjs +4 -5
- package/dist/runtime/pi-compaction-recovery.cjs +9 -11
- package/dist/runtime/pi-compaction-recovery.mjs +8 -10
- package/dist/runtime/pi-extension-api.mjs +1 -0
- package/dist/runtime/pi-model-catalog.cjs +7 -8
- package/dist/runtime/pi-model-catalog.mjs +7 -8
- package/dist/runtime/pi-subagent-injection.mjs +17 -4
- package/dist/runtime/runtime-adapters.mjs +41 -38
- package/dist/runtime/runtime-host.cjs +197 -20
- package/dist/runtime/runtime-host.mjs +198 -21
- package/dist/runtime/runtime-model-catalog.cjs +1 -1
- package/dist/runtime/runtime-model-catalog.mjs +1 -1
- package/dist/runtime/runtime-readiness.cjs +112 -49
- package/dist/runtime/runtime-readiness.mjs +103 -49
- package/dist/runtime/user-runtime.cjs +7 -24
- package/dist/runtime/user-runtime.mjs +7 -23
- package/dist/setup/bot-register.mjs +15 -123
- package/dist/setup/setup-agent-choice.mjs +48 -74
- package/dist/setup/setup-bind.mjs +51 -151
- package/dist/setup/setup-binding.mjs +6 -9
- package/package.json +3 -2
- package/dist/app/pi-auth-cli.mjs +0 -67
- package/dist/platform/process-boundary-trace.cjs +0 -108
- package/dist/runtime/builtin-pi-assets.cjs +0 -117
- package/dist/runtime/builtin-pi-assets.mjs +0 -105
- package/dist/runtime/pi-artifact-provenance.cjs +0 -147
- package/dist/runtime/pi-distribution-label.cjs +0 -6
- package/dist/runtime/pi-distribution-label.mjs +0 -3
- package/dist/runtime/pi-inline-extensions.mjs +0 -78
- package/dist/runtime/pi-official-auth.mjs +0 -491
- package/dist/runtime/pi-profile-migration.cjs +0 -764
- package/dist/runtime/pi-profile-migration.mjs +0 -719
- package/dist/runtime/pi-provider-config.cjs +0 -331
- package/dist/runtime/pi-provider-config.mjs +0 -314
package/dist/platform/config.mjs
CHANGED
|
@@ -5,15 +5,14 @@ import * as path from "node:path";
|
|
|
5
5
|
import { TargetRootLayout, resolveConfigDir as resolveRootConfigDir } from "./root-layout.mjs";
|
|
6
6
|
import { exactMode, fsyncDirectoryOf } from "./secure-metadata.mjs";
|
|
7
7
|
import { CURRENT_RUNTIME_MODELS } from "../runtime/runtime-model-catalog.mjs";
|
|
8
|
-
import { assertBuiltinPiAgentDirectory, piAgentDirectory } from "../runtime/pi-provider-config.mjs";
|
|
9
8
|
import { fromUserRuntime, isAdapterRuntime, isUserRuntime, toUserRuntime } from "../runtime/user-runtime.mjs";
|
|
10
9
|
import processInspect from "./process-inspect.cjs";
|
|
11
|
-
import { applyPiProfileMigration, assertPiProfileMigrationAfterState, preparePiProfileMigration, releasePiProfileMigrationLock, rollbackPiProfileMigration, validatePiProfileMigrationState, } from "../runtime/pi-profile-migration.mjs";
|
|
12
10
|
export { RUNTIME_OPTIONS, fromUserRuntime, isAdapterRuntime, isUserRuntime, runtimeOptionOf, runtimeOptionTarget, toUserRuntime, } from "../runtime/user-runtime.mjs";
|
|
13
11
|
const TOP_FIELDS_V3 = new Set(["version", "serverId", "activeAgent", "agents"]);
|
|
14
12
|
const TOP_FIELDS_V4 = new Set(["version", "serverId", "mentionPolicy", "activeAgent", "agents"]);
|
|
15
13
|
const AGENT_FIELDS_V3 = new Set(["runtime", "model", "effort", "noMentionChats", "createdAt"]);
|
|
16
|
-
const AGENT_FIELDS_V4 = new Set(["runtime", "model", "
|
|
14
|
+
const AGENT_FIELDS_V4 = new Set(["runtime", "model", "effort", "mentionPolicy", "chatMentionPolicies", "createdAt"]);
|
|
15
|
+
const LEGACY_AGENT_FIELDS = new Set(["piDistribution", "runtimeOption"]);
|
|
17
16
|
const APP_ID = /^cli_[A-Za-z0-9]+$/;
|
|
18
17
|
const CHAT_ID = /^oc_[A-Za-z0-9_-]+$/;
|
|
19
18
|
const PI_EFFORTS = new Set(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
|
|
@@ -91,31 +90,51 @@ function runtimeSupportsEffort(runtime, effort) {
|
|
|
91
90
|
: runtime === "codex" ? CODEX_EFFORTS.has(effort)
|
|
92
91
|
: runtime === "claude" ? CLAUDE_EFFORTS.has(effort) : false;
|
|
93
92
|
}
|
|
93
|
+
function takeLegacyAgentFields(agent) {
|
|
94
|
+
const cleaned = {};
|
|
95
|
+
for (const [field, value] of Object.entries(agent)) {
|
|
96
|
+
if (!LEGACY_AGENT_FIELDS.has(field))
|
|
97
|
+
cleaned[field] = value;
|
|
98
|
+
}
|
|
99
|
+
return cleaned;
|
|
100
|
+
}
|
|
101
|
+
function isLegacyBuiltinAgentRecord(agent) {
|
|
102
|
+
if (!isPlainObject(agent))
|
|
103
|
+
return false;
|
|
104
|
+
return agent.piDistribution === "builtin" || agent.runtime === "builtin-pi" || agent.runtimeOption === "builtin-pi";
|
|
105
|
+
}
|
|
106
|
+
function canonicalizeLegacyStoredAgent(key, agent) {
|
|
107
|
+
const cleaned = takeLegacyAgentFields(agent);
|
|
108
|
+
if (cleaned.runtime === "builtin-pi" || isLegacyBuiltinAgentRecord(agent))
|
|
109
|
+
cleaned.runtime = "pi";
|
|
110
|
+
if (typeof cleaned.runtime !== "string" || !cleaned.runtime)
|
|
111
|
+
throw new Error(`Agent ${key}.runtime 必须是非空字符串`);
|
|
112
|
+
return cleaned;
|
|
113
|
+
}
|
|
94
114
|
function validateStoredAgent(key, agent, version) {
|
|
95
115
|
if (!APP_ID.test(key))
|
|
96
116
|
throw new Error(`Agent key 必须是安全的飞书 App ID(cli_ + ASCII 字母数字):${key}`);
|
|
97
117
|
if (!isPlainObject(agent))
|
|
98
118
|
throw new Error(`Agent ${key} 必须是普通 object`);
|
|
99
|
-
|
|
100
|
-
|
|
119
|
+
const canonical = canonicalizeLegacyStoredAgent(key, agent);
|
|
120
|
+
assertAllowedFields(canonical, version === 3 ? AGENT_FIELDS_V3 : AGENT_FIELDS_V4, `Agent ${key}`);
|
|
121
|
+
if (typeof canonical.runtime !== "string" || !canonical.runtime)
|
|
101
122
|
throw new Error(`Agent ${key}.runtime 必须是非空字符串`);
|
|
102
|
-
if (!isAdapterRuntime(
|
|
103
|
-
throw new Error(`Agent ${key}.runtime 未知:${
|
|
104
|
-
if (typeof
|
|
123
|
+
if (!isAdapterRuntime(canonical.runtime))
|
|
124
|
+
throw new Error(`Agent ${key}.runtime 未知:${canonical.runtime}`);
|
|
125
|
+
if (typeof canonical.model !== "string" || !canonical.model)
|
|
105
126
|
throw new Error(`Agent ${key}.model 必须是非空字符串`);
|
|
106
|
-
assertModel(
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}
|
|
111
|
-
}
|
|
127
|
+
assertModel(canonical.runtime, canonical.model);
|
|
128
|
+
Object.assign(agent, canonical);
|
|
129
|
+
for (const field of LEGACY_AGENT_FIELDS)
|
|
130
|
+
delete agent[field];
|
|
112
131
|
if (Object.hasOwn(agent, "effort") && (typeof agent.effort !== "string" || !agent.effort))
|
|
113
132
|
throw new Error(`Agent ${key}.effort 必须是非空字符串`);
|
|
114
133
|
if (agent.model === "default" && Object.hasOwn(agent, "effort"))
|
|
115
134
|
throw new Error(`Agent ${key}.model=default 时不能保存 effort`);
|
|
116
135
|
if (typeof agent.effort === "string") {
|
|
117
|
-
if (!runtimeSupportsEffort(
|
|
118
|
-
throw new Error(`Agent ${key}.effort=${agent.effort} 不在 ${
|
|
136
|
+
if (!runtimeSupportsEffort(canonical.runtime, agent.effort))
|
|
137
|
+
throw new Error(`Agent ${key}.effort=${agent.effort} 不在 ${canonical.runtime} 安全档位中`);
|
|
119
138
|
}
|
|
120
139
|
if (version === 3 && Object.hasOwn(agent, "noMentionChats")) {
|
|
121
140
|
if (!Array.isArray(agent.noMentionChats) || agent.noMentionChats.some((chat) => typeof chat !== "string" || !CHAT_ID.test(chat))) {
|
|
@@ -150,7 +169,6 @@ function hydratedStoredAgent(key, agent, version) {
|
|
|
150
169
|
return {
|
|
151
170
|
runtime: agent.runtime,
|
|
152
171
|
model: agent.model,
|
|
153
|
-
...(agent.piDistribution === "external" || agent.piDistribution === "builtin" ? { piDistribution: agent.piDistribution } : {}),
|
|
154
172
|
...(typeof agent.effort === "string" ? { effort: agent.effort } : {}),
|
|
155
173
|
...(version === 4 && (agent.mentionPolicy === "require" || agent.mentionPolicy === "free") ? { mentionPolicy: agent.mentionPolicy } : {}),
|
|
156
174
|
...(Object.keys(chatMentionPolicies).length ? { chatMentionPolicies, noMentionChats: Object.entries(chatMentionPolicies).filter(([, policy]) => policy === "free").map(([chatId]) => chatId) } : {}),
|
|
@@ -162,7 +180,6 @@ export function hydrateAgent(key, agent, configDir) {
|
|
|
162
180
|
return {
|
|
163
181
|
name: key, agentId: key, feishuAppId: key, feishuProfile: key,
|
|
164
182
|
runtime: agent.runtime, model: agent.model,
|
|
165
|
-
...(agent.piDistribution ? { piDistribution: agent.piDistribution } : {}),
|
|
166
183
|
workspaceDir: layout.workspaceDir(key), stateDir: layout.agentStateDir(key),
|
|
167
184
|
larkConfigDir: path.join(layout.agentStateDir(key), "lark-cli-config"),
|
|
168
185
|
...(agent.effort ? { effort: agent.effort } : {}),
|
|
@@ -283,9 +300,137 @@ function readConfigFile(file, root = path.dirname(file)) {
|
|
|
283
300
|
throw error;
|
|
284
301
|
}
|
|
285
302
|
}
|
|
303
|
+
function collectLegacyBuiltinAgentIds(raw) {
|
|
304
|
+
if (!isPlainObject(raw) || !isPlainObject(raw.agents))
|
|
305
|
+
return [];
|
|
306
|
+
return Object.entries(raw.agents).filter(([, agent]) => isLegacyBuiltinAgentRecord(agent)).map(([agentId]) => agentId);
|
|
307
|
+
}
|
|
308
|
+
function ownedPiProviderDirectory(configDir, agentId) {
|
|
309
|
+
if (!APP_ID.test(agentId))
|
|
310
|
+
throw new Error(`Pi Agent ID 格式无效:${agentId}`);
|
|
311
|
+
return path.join(path.resolve(configDir), "providers", "pi", agentId);
|
|
312
|
+
}
|
|
313
|
+
function userPiHome(env) {
|
|
314
|
+
const home = typeof env.HOME === "string" && env.HOME.trim() ? env.HOME : os.homedir();
|
|
315
|
+
return path.resolve(home, ".pi");
|
|
316
|
+
}
|
|
317
|
+
function isInsideOrEqual(candidate, root) {
|
|
318
|
+
const relative = path.relative(root, candidate);
|
|
319
|
+
return relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative));
|
|
320
|
+
}
|
|
321
|
+
function pathExists(target) {
|
|
322
|
+
try {
|
|
323
|
+
fs.lstatSync(target);
|
|
324
|
+
return true;
|
|
325
|
+
}
|
|
326
|
+
catch (error) {
|
|
327
|
+
if (error.code === "ENOENT")
|
|
328
|
+
return false;
|
|
329
|
+
throw error;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
function shouldSkipOwnedPiDeletion(configDir, expected, env) {
|
|
333
|
+
let userPi;
|
|
334
|
+
try {
|
|
335
|
+
userPi = fs.realpathSync(userPiHome(env));
|
|
336
|
+
}
|
|
337
|
+
catch {
|
|
338
|
+
userPi = path.resolve(userPiHome(env));
|
|
339
|
+
}
|
|
340
|
+
let configRoot;
|
|
341
|
+
try {
|
|
342
|
+
configRoot = fs.realpathSync(path.resolve(configDir));
|
|
343
|
+
}
|
|
344
|
+
catch {
|
|
345
|
+
configRoot = path.resolve(configDir);
|
|
346
|
+
}
|
|
347
|
+
let resolvedExpected;
|
|
348
|
+
try {
|
|
349
|
+
resolvedExpected = fs.realpathSync(path.resolve(expected));
|
|
350
|
+
}
|
|
351
|
+
catch {
|
|
352
|
+
resolvedExpected = path.resolve(expected);
|
|
353
|
+
}
|
|
354
|
+
if (isInsideOrEqual(configRoot, userPi))
|
|
355
|
+
return "config root is inside the user ~/.pi directory";
|
|
356
|
+
if (isInsideOrEqual(resolvedExpected, userPi))
|
|
357
|
+
return "owned Pi directory is inside the user ~/.pi directory";
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
function deleteOwnedPiProviderDirectory(configDir, agentId, env = process.env) {
|
|
361
|
+
const expected = ownedPiProviderDirectory(configDir, agentId);
|
|
362
|
+
const skip = shouldSkipOwnedPiDeletion(configDir, expected, env);
|
|
363
|
+
if (skip) {
|
|
364
|
+
process.stderr.write(`[larkin] skipped leftover builtin Pi directory for ${agentId}: ${skip}\n`);
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
const parent = path.dirname(expected);
|
|
368
|
+
if (!pathExists(parent) || !pathExists(expected))
|
|
369
|
+
return;
|
|
370
|
+
assertNoSymlinkAncestors(expected);
|
|
371
|
+
const rootReal = fs.realpathSync(path.resolve(configDir));
|
|
372
|
+
const parentReal = fs.realpathSync(parent);
|
|
373
|
+
const relative = path.relative(rootReal, parentReal);
|
|
374
|
+
if (relative.startsWith(`..${path.sep}`) || relative === ".." || path.isAbsolute(relative)) {
|
|
375
|
+
throw new Error("owned Pi provider directory escaped the config root");
|
|
376
|
+
}
|
|
377
|
+
if (path.basename(parent) !== "pi" || path.basename(path.dirname(parent)) !== "providers") {
|
|
378
|
+
throw new Error("owned Pi provider directory is not providers/pi/<agentId>");
|
|
379
|
+
}
|
|
380
|
+
const stat = fs.lstatSync(expected);
|
|
381
|
+
if (stat.isSymbolicLink())
|
|
382
|
+
throw new Error("owned Pi provider directory is a symlink");
|
|
383
|
+
if (!stat.isDirectory())
|
|
384
|
+
throw new Error("owned Pi provider directory must be a directory");
|
|
385
|
+
fs.rmSync(expected, { recursive: true, force: false });
|
|
386
|
+
}
|
|
387
|
+
function rewriteLegacyBuiltinAgents(raw) {
|
|
388
|
+
if (!isPlainObject(raw) || !isPlainObject(raw.agents))
|
|
389
|
+
return raw;
|
|
390
|
+
const legacyIds = new Set(collectLegacyBuiltinAgentIds(raw));
|
|
391
|
+
if (legacyIds.size === 0)
|
|
392
|
+
return raw;
|
|
393
|
+
const agents = {};
|
|
394
|
+
for (const [agentId, agent] of Object.entries(raw.agents)) {
|
|
395
|
+
if (!legacyIds.has(agentId) || !isPlainObject(agent)) {
|
|
396
|
+
agents[agentId] = agent;
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
const next = takeLegacyAgentFields(agent);
|
|
400
|
+
if (next.runtime === "builtin-pi" || isLegacyBuiltinAgentRecord(agent))
|
|
401
|
+
next.runtime = "pi";
|
|
402
|
+
agents[agentId] = next;
|
|
403
|
+
}
|
|
404
|
+
return { ...raw, agents };
|
|
405
|
+
}
|
|
406
|
+
function persistLegacyBuiltinMigration(layout, opts, env) {
|
|
407
|
+
const current = readConfigFile(layout.configFile, layout.root);
|
|
408
|
+
const migratedIds = collectLegacyBuiltinAgentIds(current.raw);
|
|
409
|
+
if (migratedIds.length === 0)
|
|
410
|
+
return current;
|
|
411
|
+
const rewritten = rewriteLegacyBuiltinAgents(current.raw);
|
|
412
|
+
const config = normalizeConfig(structuredClone(rewritten), layout.root, opts);
|
|
413
|
+
const bytes = atomicWriteConfig(layout.configFile, rewritten);
|
|
414
|
+
for (const agentId of migratedIds) {
|
|
415
|
+
const agent = config.agents[agentId];
|
|
416
|
+
const model = typeof agent?.model === "string" ? agent.model : "unknown";
|
|
417
|
+
process.stderr.write(`[larkin] migrated Agent ${agentId} from builtin-pi to pi (model=${model})\n`);
|
|
418
|
+
try {
|
|
419
|
+
deleteOwnedPiProviderDirectory(layout.root, agentId, env);
|
|
420
|
+
}
|
|
421
|
+
catch (error) {
|
|
422
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
423
|
+
process.stderr.write(`[larkin] failed to delete leftover builtin Pi directory for ${agentId}: ${reason}\n`);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
return { raw: rewritten, bytes };
|
|
427
|
+
}
|
|
286
428
|
export function loadConfig(env = process.env, opts = {}) {
|
|
287
429
|
const layout = TargetRootLayout.fromConfigDir(resolveConfigDir(env));
|
|
288
|
-
const
|
|
430
|
+
const initial = readConfigFile(layout.configFile, layout.root);
|
|
431
|
+
const { raw, bytes } = collectLegacyBuiltinAgentIds(initial.raw).length
|
|
432
|
+
? withConfigLock(layout, () => persistLegacyBuiltinMigration(layout, opts, env))
|
|
433
|
+
: initial;
|
|
289
434
|
return { configDir: layout.configDir, file: layout.configFile, revision: revision(bytes), config: normalizeConfig(raw, layout.root, opts) };
|
|
290
435
|
}
|
|
291
436
|
export function selectAgent(config, env = process.env) {
|
|
@@ -306,8 +451,6 @@ export function toStored(config) {
|
|
|
306
451
|
const out = { version: 4, serverId: config.serverId, mentionPolicy: config.mentionPolicy, activeAgent: config.activeAgent, agents: {} };
|
|
307
452
|
for (const [key, agent] of Object.entries(config.agents || {})) {
|
|
308
453
|
const stored = { runtime: agent.runtime, model: agent.model };
|
|
309
|
-
if (agent.piDistribution)
|
|
310
|
-
stored.piDistribution = agent.piDistribution;
|
|
311
454
|
if (typeof agent.effort === "string" && agent.effort)
|
|
312
455
|
stored.effort = agent.effort;
|
|
313
456
|
if (agent.mentionPolicy)
|
|
@@ -349,7 +492,8 @@ function agentSignature(config, agentId) {
|
|
|
349
492
|
throw new Error(`Agent 不存在:${agentId}`);
|
|
350
493
|
const chats = Object.fromEntries(Object.entries(agent.chatMentionPolicies || {}).sort(([left], [right]) => left.localeCompare(right)));
|
|
351
494
|
return revision(JSON.stringify({
|
|
352
|
-
|
|
495
|
+
schema: 2,
|
|
496
|
+
runtime: agent.runtime, model: agent.model, effort: agent.effort ?? null,
|
|
353
497
|
globalMentionPolicy: config.mentionPolicy, agentMentionPolicy: agent.mentionPolicy ?? null, chatMentionPolicies: chats,
|
|
354
498
|
}));
|
|
355
499
|
}
|
|
@@ -386,12 +530,6 @@ function writeApplyFile(configDir, value) {
|
|
|
386
530
|
atomicWriteConfig(applyStateFile(configDir), value);
|
|
387
531
|
}
|
|
388
532
|
function rollbackJournalFile(root) { return path.join(root, CONFIG_ROLLBACK_JOURNAL_FILE); }
|
|
389
|
-
function migrationJournalFile(root) { return path.join(root, ".pi-profile-migration-journal.json"); }
|
|
390
|
-
function assertMigrationTarget(root, agentId, migration) {
|
|
391
|
-
if (migration.agentId !== agentId || path.resolve(migration.targetDir) !== path.join(path.resolve(root), "providers", "pi", agentId)) {
|
|
392
|
-
throw new Error("Pi profile migration target is invalid");
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
533
|
function restoreTargetApplyState(root, targetAgentId, beforeRevision, before) {
|
|
396
534
|
const current = readApplyFile(root);
|
|
397
535
|
const agents = { ...current.agents };
|
|
@@ -430,15 +568,9 @@ function recoverRollbackJournal(root) {
|
|
|
430
568
|
const beforeRaw = JSON.parse(beforeBytes.toString("utf8"));
|
|
431
569
|
normalizeConfig(beforeRaw, root);
|
|
432
570
|
validateApplyState(journal.beforeApplyState);
|
|
433
|
-
if (journal.migration !== undefined) {
|
|
434
|
-
validatePiProfileMigrationState(journal.migration);
|
|
435
|
-
assertMigrationTarget(root, journal.targetAgentId, journal.migration);
|
|
436
|
-
}
|
|
437
571
|
const current = readConfigFile(path.join(root, "config.json"), root);
|
|
438
572
|
const currentRevision = revision(current.bytes);
|
|
439
573
|
if (currentRevision === journal.expectedAfterRevision) {
|
|
440
|
-
if (journal.migration)
|
|
441
|
-
rollbackPiProfileMigration(journal.migration);
|
|
442
574
|
atomicWriteBytes(path.join(root, "config.json"), beforeBytes);
|
|
443
575
|
restoreTargetApplyState(root, journal.targetAgentId, journal.beforeRevision, journal.beforeApplyState);
|
|
444
576
|
}
|
|
@@ -453,47 +585,6 @@ function recoverRollbackJournal(root) {
|
|
|
453
585
|
fs.unlinkSync(file);
|
|
454
586
|
fsyncDirectoryOf(file);
|
|
455
587
|
}
|
|
456
|
-
function recoverMigrationJournal(root) {
|
|
457
|
-
const file = migrationJournalFile(root);
|
|
458
|
-
const bytes = readPrivateFile(file, root, PROFILE_MIGRATION_ROLLBACK_LIMIT_BYTES, "Pi profile migration journal");
|
|
459
|
-
if (bytes === null)
|
|
460
|
-
return;
|
|
461
|
-
let journal;
|
|
462
|
-
try {
|
|
463
|
-
journal = JSON.parse(bytes.toString("utf8"));
|
|
464
|
-
}
|
|
465
|
-
catch {
|
|
466
|
-
throw new Error("Pi profile migration journal is invalid");
|
|
467
|
-
}
|
|
468
|
-
if (journal.version !== 1 || journal.phase !== "forward" || typeof journal.targetAgentId !== "string"
|
|
469
|
-
|| typeof journal.beforeRevision !== "string" || typeof journal.expectedAfterRevision !== "string" || !journal.migration) {
|
|
470
|
-
throw new Error("Pi profile migration journal is invalid");
|
|
471
|
-
}
|
|
472
|
-
validatePiProfileMigrationState(journal.migration);
|
|
473
|
-
assertMigrationTarget(root, journal.targetAgentId, journal.migration);
|
|
474
|
-
const current = readConfigFile(path.join(root, "config.json"), root);
|
|
475
|
-
const currentRevision = revision(current.bytes);
|
|
476
|
-
if (currentRevision === journal.beforeRevision) {
|
|
477
|
-
// Forward import did not publish config. The migration helper restores only
|
|
478
|
-
// files still matching its expected post-import hashes.
|
|
479
|
-
try {
|
|
480
|
-
rollbackPiProfileMigration(journal.migration);
|
|
481
|
-
}
|
|
482
|
-
catch (error) {
|
|
483
|
-
throw new Error(`Pi profile migration recovery refused: ${error instanceof Error ? error.message : String(error)}`);
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
else if (currentRevision === journal.expectedAfterRevision) {
|
|
487
|
-
// Config won the race; verify the provider target before finalizing.
|
|
488
|
-
const migration = journal.migration;
|
|
489
|
-
assertPiProfileMigrationAfterState(migration);
|
|
490
|
-
}
|
|
491
|
-
else
|
|
492
|
-
throw new Error("Pi profile migration journal conflicts with the current configuration");
|
|
493
|
-
releasePiProfileMigrationLock(journal.migration);
|
|
494
|
-
fs.unlinkSync(file);
|
|
495
|
-
fsyncDirectoryOf(file);
|
|
496
|
-
}
|
|
497
588
|
export function configApplyState(env, config) {
|
|
498
589
|
let state;
|
|
499
590
|
try {
|
|
@@ -570,32 +661,13 @@ function applyMutation(config, mutation) {
|
|
|
570
661
|
agent.noMentionChats = Object.entries(policies).filter(([, value]) => value === "free").map(([chatId]) => chatId);
|
|
571
662
|
return { scope: "chat", agentId: mutation.agentId, runtimeChange: false, affectedAgentIds: [mutation.agentId] };
|
|
572
663
|
}
|
|
573
|
-
if (mutation.kind === "set-agent-
|
|
574
|
-
if (agent.runtime !== "pi")
|
|
575
|
-
throw new Error(`Agent ${mutation.agentId} 不是 Pi runtime`);
|
|
576
|
-
if (mutation.distribution !== "builtin" && mutation.distribution !== "external")
|
|
577
|
-
throw new Error("Pi distribution 只允许 builtin/external");
|
|
578
|
-
agent.piDistribution = mutation.distribution;
|
|
579
|
-
}
|
|
580
|
-
else if (mutation.kind === "set-agent-runtime") {
|
|
664
|
+
if (mutation.kind === "set-agent-runtime") {
|
|
581
665
|
if (!isUserRuntime(mutation.runtime))
|
|
582
666
|
throw new Error(`未知 runtime:${mutation.runtime}`);
|
|
583
667
|
const stored = fromUserRuntime(mutation.runtime);
|
|
584
|
-
if (stored.piDistribution === "builtin") {
|
|
585
|
-
try {
|
|
586
|
-
assertBuiltinPiAgentDirectory(piAgentDirectory(config.configDir, mutation.agentId));
|
|
587
|
-
}
|
|
588
|
-
catch (error) {
|
|
589
|
-
throw new Error(`无法切换到 builtin-pi:${error instanceof Error ? error.message : String(error)}。请先运行 larkin setup 并为该 Agent 选择 builtin-pi 完成 provider 配置`);
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
668
|
assertModel(stored.runtime, mutation.model || "default");
|
|
593
669
|
agent.runtime = stored.runtime;
|
|
594
670
|
agent.model = mutation.model || "default";
|
|
595
|
-
if (stored.piDistribution)
|
|
596
|
-
agent.piDistribution = stored.piDistribution;
|
|
597
|
-
else
|
|
598
|
-
delete agent.piDistribution;
|
|
599
671
|
delete agent.effort;
|
|
600
672
|
}
|
|
601
673
|
else if (mutation.kind === "set-agent-model") {
|
|
@@ -749,7 +821,7 @@ function assertConfigRoot(root) {
|
|
|
749
821
|
if (typeof process.getuid === "function" && stat.uid !== process.getuid())
|
|
750
822
|
throw new Error("config root owner is unsafe");
|
|
751
823
|
}
|
|
752
|
-
function
|
|
824
|
+
function acquireConfigLock(layout) {
|
|
753
825
|
assertNoSymlinkAncestors(layout.root);
|
|
754
826
|
fs.mkdirSync(layout.root, { recursive: true, mode: 0o700 });
|
|
755
827
|
const rootStat = fs.lstatSync(layout.root);
|
|
@@ -788,20 +860,41 @@ function withConfigLock(layout, action) {
|
|
|
788
860
|
}
|
|
789
861
|
if (!acquired)
|
|
790
862
|
throw new Error("配置正被其他进程修改,请稍后重试");
|
|
863
|
+
return {
|
|
864
|
+
release() {
|
|
865
|
+
const current = readLockRecord(lock);
|
|
866
|
+
if (current?.record?.nonce === owner.nonce && current.record.pid === owner.pid) {
|
|
867
|
+
try {
|
|
868
|
+
fs.unlinkSync(lock);
|
|
869
|
+
fsyncDirectoryOf(lock);
|
|
870
|
+
}
|
|
871
|
+
catch { /* best effort */ }
|
|
872
|
+
}
|
|
873
|
+
},
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
function withConfigLock(layout, action) {
|
|
877
|
+
const held = acquireConfigLock(layout);
|
|
791
878
|
try {
|
|
792
|
-
recoverMigrationJournal(layout.root);
|
|
793
879
|
recoverRollbackJournal(layout.root);
|
|
794
880
|
return action();
|
|
795
881
|
}
|
|
796
882
|
finally {
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
883
|
+
held.release();
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
export function withConfigMutationLock(env, action) {
|
|
887
|
+
return withConfigLock(TargetRootLayout.fromConfigDir(resolveConfigDir(env)), action);
|
|
888
|
+
}
|
|
889
|
+
export async function withConfigMutationLockAsync(env, action) {
|
|
890
|
+
const layout = TargetRootLayout.fromConfigDir(resolveConfigDir(env));
|
|
891
|
+
const held = acquireConfigLock(layout);
|
|
892
|
+
try {
|
|
893
|
+
recoverRollbackJournal(layout.root);
|
|
894
|
+
return await action();
|
|
895
|
+
}
|
|
896
|
+
finally {
|
|
897
|
+
held.release();
|
|
805
898
|
}
|
|
806
899
|
}
|
|
807
900
|
const CONFIG_ROLLBACK_JOURNAL_FILE = ".config-rollback-journal.json";
|
|
@@ -896,10 +989,6 @@ function readConfigSnapshot(file, root) {
|
|
|
896
989
|
if (JSON.stringify(beforeRaw) !== JSON.stringify(parsed.beforeConfig))
|
|
897
990
|
throw new Error("config snapshot before config does not match bytes");
|
|
898
991
|
validateApplyState(parsed.beforeApplyState);
|
|
899
|
-
if (parsed.migration !== undefined) {
|
|
900
|
-
validatePiProfileMigrationState(parsed.migration);
|
|
901
|
-
assertMigrationTarget(root, parsed.targetAgentId, parsed.migration);
|
|
902
|
-
}
|
|
903
992
|
return parsed;
|
|
904
993
|
}
|
|
905
994
|
function atomicWriteBytes(file, bytes, limit = CONFIG_LIMIT_BYTES) {
|
|
@@ -943,12 +1032,6 @@ export function mutateConfig(env, mutation, authority, options = {}) {
|
|
|
943
1032
|
throw new Error(`没找到配置 ${layout.configFile},先跑 larkin setup`);
|
|
944
1033
|
const config = normalizeConfig(current.raw, layout.root);
|
|
945
1034
|
const priorSignatures = Object.fromEntries(Object.keys(config.agents).map((agentId) => [agentId, agentSignature(config, agentId)]));
|
|
946
|
-
if (options.importExternalProfile && (mutation.kind !== "set-agent-pi-distribution" || mutation.distribution !== "builtin")) {
|
|
947
|
-
throw new Error("external Pi profile import is only valid for builtin Pi distribution");
|
|
948
|
-
}
|
|
949
|
-
const migrationPlan = options.importExternalProfile && mutation.kind === "set-agent-pi-distribution"
|
|
950
|
-
? preparePiProfileMigration(env, layout.root, mutation.agentId, "builtin")
|
|
951
|
-
: undefined;
|
|
952
1035
|
const changed = applyMutation(config, mutation);
|
|
953
1036
|
const stored = toStored(config);
|
|
954
1037
|
normalizeConfig(stored, layout.root);
|
|
@@ -969,16 +1052,8 @@ export function mutateConfig(env, mutation, authority, options = {}) {
|
|
|
969
1052
|
version: 2, targetAgentId: changed.agentId, beforeRevision: revision(current.bytes), afterRevision: nextRevision,
|
|
970
1053
|
afterSignature: agentSignature(config, changed.agentId), beforeConfig: current.raw,
|
|
971
1054
|
beforeConfigBytes: current.bytes.toString("base64"), beforeApplyState,
|
|
972
|
-
...(migrationPlan ? { migration: migrationPlan.state } : {}),
|
|
973
1055
|
});
|
|
974
1056
|
}
|
|
975
|
-
if (migrationPlan) {
|
|
976
|
-
atomicWriteConfig(migrationJournalFile(layout.root), {
|
|
977
|
-
version: 1, phase: "forward", targetAgentId: changed.agentId, beforeRevision: revision(current.bytes),
|
|
978
|
-
expectedAfterRevision: nextRevision, migration: migrationPlan.state,
|
|
979
|
-
}, PROFILE_MIGRATION_ROLLBACK_LIMIT_BYTES);
|
|
980
|
-
applyPiProfileMigration(migrationPlan);
|
|
981
|
-
}
|
|
982
1057
|
atomicWriteConfig(layout.configFile, stored);
|
|
983
1058
|
let fullyApplied = changed.affectedAgentIds.length === 0;
|
|
984
1059
|
try {
|
|
@@ -1002,17 +1077,6 @@ export function mutateConfig(env, mutation, authority, options = {}) {
|
|
|
1002
1077
|
catch {
|
|
1003
1078
|
fullyApplied = false; /* Config persistence remains authoritative. */
|
|
1004
1079
|
}
|
|
1005
|
-
if (migrationPlan) {
|
|
1006
|
-
releasePiProfileMigrationLock(migrationPlan.state);
|
|
1007
|
-
try {
|
|
1008
|
-
fs.unlinkSync(migrationJournalFile(layout.root));
|
|
1009
|
-
fsyncDirectoryOf(migrationJournalFile(layout.root));
|
|
1010
|
-
}
|
|
1011
|
-
catch (error) {
|
|
1012
|
-
if (error.code !== "ENOENT")
|
|
1013
|
-
throw error;
|
|
1014
|
-
}
|
|
1015
|
-
}
|
|
1016
1080
|
return {
|
|
1017
1081
|
revision: nextRevision, previousRevision: revision(current.bytes), changedScope: changed.scope,
|
|
1018
1082
|
...(changed.agentId ? { agentId: changed.agentId } : {}), persisted: true,
|
|
@@ -1036,11 +1100,8 @@ export function rollbackConfig(env, snapshotFile) {
|
|
|
1036
1100
|
const journal = {
|
|
1037
1101
|
version: 1, targetAgentId: snapshot.targetAgentId, expectedAfterRevision: snapshot.afterRevision,
|
|
1038
1102
|
beforeRevision: snapshot.beforeRevision, beforeConfigBytes: snapshot.beforeConfigBytes, beforeApplyState: snapshot.beforeApplyState,
|
|
1039
|
-
...(snapshot.migration ? { migration: snapshot.migration } : {}),
|
|
1040
1103
|
};
|
|
1041
1104
|
atomicWriteConfig(rollbackJournalFile(layout.root), journal, PROFILE_MIGRATION_ROLLBACK_LIMIT_BYTES);
|
|
1042
|
-
if (snapshot.migration)
|
|
1043
|
-
rollbackPiProfileMigration(snapshot.migration);
|
|
1044
1105
|
const bytes = atomicWriteBytes(layout.configFile, beforeBytes);
|
|
1045
1106
|
restoreTargetApplyState(layout.root, snapshot.targetAgentId, snapshot.beforeRevision, snapshot.beforeApplyState);
|
|
1046
1107
|
fs.unlinkSync(rollbackJournalFile(layout.root));
|
|
@@ -1062,7 +1123,7 @@ export function commitSetupConfig(env, expectedRevision, nextStored) {
|
|
|
1062
1123
|
export function safeConfigView(config, onlyAgentId, chatId, applyState) {
|
|
1063
1124
|
const entries = onlyAgentId ? [[onlyAgentId, config.agents[onlyAgentId]]] : Object.entries(config.agents);
|
|
1064
1125
|
const agents = entries.filter((entry) => Boolean(entry[1])).map(([agentId, agent]) => ({
|
|
1065
|
-
agentId, runtime: agent.runtime, runtimeOption: toUserRuntime(agent.runtime
|
|
1126
|
+
agentId, runtime: agent.runtime, runtimeOption: toUserRuntime(agent.runtime), model: agent.model, effort: agent.effort ?? null,
|
|
1066
1127
|
mention: {
|
|
1067
1128
|
override: agent.mentionPolicy ?? "inherit",
|
|
1068
1129
|
effective: agent.mentionPolicy ?? config.mentionPolicy,
|
|
@@ -3,7 +3,7 @@ import fs from "node:fs";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { exactMode } from "../platform/secure-metadata.mjs";
|
|
5
5
|
export const PI_RUNTIME_ARTIFACT_MANIFEST = ".larkin-pi-owned-artifacts.json";
|
|
6
|
-
export const PI_RUNTIME_ARTIFACT_NAMES = ["
|
|
6
|
+
export const PI_RUNTIME_ARTIFACT_NAMES = ["models-store.json", "npm"];
|
|
7
7
|
const MAX_FILE_BYTES = 256 * 1024 * 1024;
|
|
8
8
|
const MAX_ENTRIES = 4096;
|
|
9
9
|
function digest(value) {
|
|
@@ -53,7 +53,7 @@ function readManifest(file) {
|
|
|
53
53
|
if (!stat.isFile() || !exactMode(stat, 0o600) || stat.size > 1024 * 1024)
|
|
54
54
|
throw new Error("Pi artifact provenance manifest is unsafe");
|
|
55
55
|
const value = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
56
|
-
if (value?.version !== 1 || value.owner !== "larkin-builtin-pi" || !value.directory || !value.artifacts)
|
|
56
|
+
if (value?.version !== 1 || (value.owner !== "larkin-pi" && value.owner !== "larkin-builtin-pi") || !value.directory || !value.artifacts)
|
|
57
57
|
throw new Error("Pi artifact provenance manifest is invalid");
|
|
58
58
|
return value;
|
|
59
59
|
}
|
|
@@ -83,7 +83,7 @@ export function recordPiRuntimeArtifactProvenance(targetDir, beforeEntries, boun
|
|
|
83
83
|
const manifestFile = path.join(targetDir, PI_RUNTIME_ARTIFACT_MANIFEST);
|
|
84
84
|
const existing = readManifest(manifestFile);
|
|
85
85
|
const manifest = existing ?? {
|
|
86
|
-
version: 1, owner: "larkin-
|
|
86
|
+
version: 1, owner: "larkin-pi", directory: { device: String(target.dev), inode: String(target.ino) },
|
|
87
87
|
recordedAt: new Date(boundaryAt).toISOString(), artifacts: {},
|
|
88
88
|
};
|
|
89
89
|
if (manifest.directory.device !== String(target.dev) || manifest.directory.inode !== String(target.ino))
|
|
@@ -2,16 +2,15 @@ import { createBashToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
|
2
2
|
/**
|
|
3
3
|
* pi bash 工具超时护栏扩展。
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* `pi --extension/-e <bundle>` 注入,覆盖内置 `bash` 工具:
|
|
5
|
+
* 通过用户安装的 Pi 的 `pi --extension/-e <bundle>` 注入,覆盖内置 `bash` 工具:
|
|
7
6
|
* - 无论模型是否传 `timeout`,都强制收窄到 <= MAX_BASH_SECONDS(默认 60s),
|
|
8
7
|
* 避免单个 bash 调用无限期占住整个 agent 回合(issue #55)。
|
|
9
8
|
* - 超时时 pi 会杀掉整个进程树(不残留卡死子进程,issue #56 的进程堆积),
|
|
10
9
|
* 并在返回的错误里明确提示:长任务必须改用后台 subagent
|
|
11
10
|
* (Agent(run_in_background: true),来自 pi-subagents 扩展)。
|
|
12
11
|
*
|
|
13
|
-
* 这是 pi 扩展,运行在 Pi RPC 子进程内部;build.mjs
|
|
14
|
-
*
|
|
12
|
+
* 这是 pi 扩展,运行在 Pi RPC 子进程内部;build.mjs 把 `@earendil-works/pi-*`
|
|
13
|
+
* 标为 external,由 host Pi 的 loader 在运行时提供模块。
|
|
15
14
|
*/
|
|
16
15
|
/** 前台 bash 的硬性上限(秒)。issue #55 建议默认 60s。 */
|
|
17
16
|
const MAX_BASH_SECONDS = 60;
|
|
@@ -24,7 +23,7 @@ function effectiveBashTimeout() {
|
|
|
24
23
|
}
|
|
25
24
|
export default function (pi) {
|
|
26
25
|
const cwd = process.cwd();
|
|
27
|
-
//
|
|
26
|
+
// Host bash tool definition (keeps renderCall/renderResult/system-prompt metadata).
|
|
28
27
|
const builtin = createBashToolDefinition(cwd);
|
|
29
28
|
const MAX = effectiveBashTimeout();
|
|
30
29
|
const SUBAGENT_GUIDANCE = `This command exceeded the ${MAX}s foreground hard limit. ` +
|
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.PiCompactionRecoveryMachine = exports.PiCompactionBreaker = exports.PI_COMPACTION_RECOVERY_FILE = exports.PI_COMPACTION_RECOVERY_VERSION = exports.PI_COMPACTION_THRESHOLD = exports.COMPACTION_KEEP_RECENT_TOKENS = exports.COMPACTION_RESERVE_TOKENS = exports.PI_CONTEXT_WINDOW = void 0;
|
|
6
|
+
exports.PiCompactionRecoveryMachine = exports.PiCompactionBreaker = exports.PI_COMPACTION_RECOVERY_FILE = exports.PI_COMPACTION_RECOVERY_VERSION = exports.PI_COMPACTION_THRESHOLD = exports.COMPACTION_KEEP_RECENT_TOKENS = exports.COMPACTION_RESERVE_TOKENS = exports.PI_CONTEXT_WINDOW = exports.PINNED_PI_VERSION = void 0;
|
|
7
7
|
exports.calculatePiCompactionSettings = calculatePiCompactionSettings;
|
|
8
8
|
exports.isPiNativeCompactionRequired = isPiNativeCompactionRequired;
|
|
9
9
|
exports.assertEffectivePiCompactionSettings = assertEffectivePiCompactionSettings;
|
|
@@ -24,7 +24,7 @@ const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
|
24
24
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
25
25
|
const node_os_1 = __importDefault(require("node:os"));
|
|
26
26
|
const node_path_1 = __importDefault(require("node:path"));
|
|
27
|
-
|
|
27
|
+
exports.PINNED_PI_VERSION = "0.84.2";
|
|
28
28
|
/** Fallback values used while importing a Pi profile, before Pi reports its model. */
|
|
29
29
|
exports.PI_CONTEXT_WINDOW = 272_000;
|
|
30
30
|
exports.COMPACTION_RESERVE_TOKENS = 40_800;
|
|
@@ -260,17 +260,17 @@ function parsePiExecutableVersion(output) {
|
|
|
260
260
|
throw new Error("Pi executable version output must contain exactly one version line");
|
|
261
261
|
const line = lines[0];
|
|
262
262
|
// Pi's bare version is canonical; permit only the fixed official display prefix.
|
|
263
|
-
const escapedVersion =
|
|
263
|
+
const escapedVersion = exports.PINNED_PI_VERSION.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
264
264
|
if (!new RegExp(`^(?:${escapedVersion}|pi(?:-coding-agent)?(?:\\s+version)?\\s+v?${escapedVersion})$`, "i").test(line)) {
|
|
265
|
-
throw new Error(`Pi executable version must be exactly ${
|
|
265
|
+
throw new Error(`Pi executable version must be exactly ${exports.PINNED_PI_VERSION}`);
|
|
266
266
|
}
|
|
267
|
-
return
|
|
267
|
+
return exports.PINNED_PI_VERSION;
|
|
268
268
|
}
|
|
269
269
|
function verifyPiCapabilities(capabilities) {
|
|
270
|
-
if (capabilities.version !==
|
|
271
|
-
throw new Error(`trusted Pi version ${
|
|
272
|
-
if (capabilities.
|
|
273
|
-
throw new Error("external Pi cannot use
|
|
270
|
+
if (capabilities.version !== exports.PINNED_PI_VERSION)
|
|
271
|
+
throw new Error(`trusted Pi version ${exports.PINNED_PI_VERSION} is required`);
|
|
272
|
+
if (capabilities.trustedProtocol === true) {
|
|
273
|
+
throw new Error("external Pi cannot use a trusted protocol bypass");
|
|
274
274
|
}
|
|
275
275
|
const contextWindow = capabilities.contextWindow ?? capabilities.model?.contextWindow;
|
|
276
276
|
const expected = (() => {
|
|
@@ -286,8 +286,6 @@ function verifyPiCapabilities(capabilities) {
|
|
|
286
286
|
}
|
|
287
287
|
if (capabilities.compactRpc !== true)
|
|
288
288
|
throw new Error("Pi compact RPC capability is missing");
|
|
289
|
-
if (capabilities.distribution === "builtin" && capabilities.trustedProtocol === true)
|
|
290
|
-
return;
|
|
291
289
|
if (capabilities.reserveTokens !== expected.reserveTokens
|
|
292
290
|
|| capabilities.keepRecentTokens !== expected.keepRecentTokens) {
|
|
293
291
|
throw new Error(`Pi external effective compaction reserve/keep settings are unproven for context window ${contextWindow}`);
|
|
@@ -2,7 +2,7 @@ import crypto from "node:crypto";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
|
|
5
|
+
export const PINNED_PI_VERSION = "0.84.2";
|
|
6
6
|
/** Fallback values used while importing a Pi profile, before Pi reports its model. */
|
|
7
7
|
export const PI_CONTEXT_WINDOW = 272_000;
|
|
8
8
|
export const COMPACTION_RESERVE_TOKENS = 40_800;
|
|
@@ -238,17 +238,17 @@ export function parsePiExecutableVersion(output) {
|
|
|
238
238
|
throw new Error("Pi executable version output must contain exactly one version line");
|
|
239
239
|
const line = lines[0];
|
|
240
240
|
// Pi's bare version is canonical; permit only the fixed official display prefix.
|
|
241
|
-
const escapedVersion =
|
|
241
|
+
const escapedVersion = PINNED_PI_VERSION.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
242
242
|
if (!new RegExp(`^(?:${escapedVersion}|pi(?:-coding-agent)?(?:\\s+version)?\\s+v?${escapedVersion})$`, "i").test(line)) {
|
|
243
|
-
throw new Error(`Pi executable version must be exactly ${
|
|
243
|
+
throw new Error(`Pi executable version must be exactly ${PINNED_PI_VERSION}`);
|
|
244
244
|
}
|
|
245
|
-
return
|
|
245
|
+
return PINNED_PI_VERSION;
|
|
246
246
|
}
|
|
247
247
|
export function verifyPiCapabilities(capabilities) {
|
|
248
|
-
if (capabilities.version !==
|
|
249
|
-
throw new Error(`trusted Pi version ${
|
|
250
|
-
if (capabilities.
|
|
251
|
-
throw new Error("external Pi cannot use
|
|
248
|
+
if (capabilities.version !== PINNED_PI_VERSION)
|
|
249
|
+
throw new Error(`trusted Pi version ${PINNED_PI_VERSION} is required`);
|
|
250
|
+
if (capabilities.trustedProtocol === true) {
|
|
251
|
+
throw new Error("external Pi cannot use a trusted protocol bypass");
|
|
252
252
|
}
|
|
253
253
|
const contextWindow = capabilities.contextWindow ?? capabilities.model?.contextWindow;
|
|
254
254
|
const expected = (() => {
|
|
@@ -264,8 +264,6 @@ export function verifyPiCapabilities(capabilities) {
|
|
|
264
264
|
}
|
|
265
265
|
if (capabilities.compactRpc !== true)
|
|
266
266
|
throw new Error("Pi compact RPC capability is missing");
|
|
267
|
-
if (capabilities.distribution === "builtin" && capabilities.trustedProtocol === true)
|
|
268
|
-
return;
|
|
269
267
|
if (capabilities.reserveTokens !== expected.reserveTokens
|
|
270
268
|
|| capabilities.keepRecentTokens !== expected.keepRecentTokens) {
|
|
271
269
|
throw new Error(`Pi external effective compaction reserve/keep settings are unproven for context window ${contextWindow}`);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|