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.cjs
CHANGED
|
@@ -53,6 +53,8 @@ exports.resolveAgentGlobalMentionPolicy = resolveAgentGlobalMentionPolicy;
|
|
|
53
53
|
exports.runtimeConfigSignature = runtimeConfigSignature;
|
|
54
54
|
exports.configApplyState = configApplyState;
|
|
55
55
|
exports.markConfigApplied = markConfigApplied;
|
|
56
|
+
exports.withConfigMutationLock = withConfigMutationLock;
|
|
57
|
+
exports.withConfigMutationLockAsync = withConfigMutationLockAsync;
|
|
56
58
|
exports.mutateConfig = mutateConfig;
|
|
57
59
|
exports.rollbackConfig = rollbackConfig;
|
|
58
60
|
exports.commitSetupConfig = commitSetupConfig;
|
|
@@ -64,10 +66,8 @@ const path = __importStar(require("node:path"));
|
|
|
64
66
|
const root_layout_js_1 = require("./root-layout.cjs");
|
|
65
67
|
const secure_metadata_js_1 = require("./secure-metadata.cjs");
|
|
66
68
|
const runtime_model_catalog_js_1 = require("../runtime/runtime-model-catalog.cjs");
|
|
67
|
-
const pi_provider_config_js_1 = require("../runtime/pi-provider-config.cjs");
|
|
68
69
|
const user_runtime_js_1 = require("../runtime/user-runtime.cjs");
|
|
69
70
|
const process_inspect_cjs_1 = __importDefault(require("./process-inspect.cjs"));
|
|
70
|
-
const pi_profile_migration_js_1 = require("../runtime/pi-profile-migration.cjs");
|
|
71
71
|
var user_runtime_js_2 = require("../runtime/user-runtime.cjs");
|
|
72
72
|
Object.defineProperty(exports, "RUNTIME_OPTIONS", { enumerable: true, get: function () { return user_runtime_js_2.RUNTIME_OPTIONS; } });
|
|
73
73
|
Object.defineProperty(exports, "fromUserRuntime", { enumerable: true, get: function () { return user_runtime_js_2.fromUserRuntime; } });
|
|
@@ -79,7 +79,8 @@ Object.defineProperty(exports, "toUserRuntime", { enumerable: true, get: functio
|
|
|
79
79
|
const TOP_FIELDS_V3 = new Set(["version", "serverId", "activeAgent", "agents"]);
|
|
80
80
|
const TOP_FIELDS_V4 = new Set(["version", "serverId", "mentionPolicy", "activeAgent", "agents"]);
|
|
81
81
|
const AGENT_FIELDS_V3 = new Set(["runtime", "model", "effort", "noMentionChats", "createdAt"]);
|
|
82
|
-
const AGENT_FIELDS_V4 = new Set(["runtime", "model", "
|
|
82
|
+
const AGENT_FIELDS_V4 = new Set(["runtime", "model", "effort", "mentionPolicy", "chatMentionPolicies", "createdAt"]);
|
|
83
|
+
const LEGACY_AGENT_FIELDS = new Set(["piDistribution", "runtimeOption"]);
|
|
83
84
|
const APP_ID = /^cli_[A-Za-z0-9]+$/;
|
|
84
85
|
const CHAT_ID = /^oc_[A-Za-z0-9_-]+$/;
|
|
85
86
|
const PI_EFFORTS = new Set(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
|
|
@@ -157,31 +158,51 @@ function runtimeSupportsEffort(runtime, effort) {
|
|
|
157
158
|
: runtime === "codex" ? CODEX_EFFORTS.has(effort)
|
|
158
159
|
: runtime === "claude" ? CLAUDE_EFFORTS.has(effort) : false;
|
|
159
160
|
}
|
|
161
|
+
function takeLegacyAgentFields(agent) {
|
|
162
|
+
const cleaned = {};
|
|
163
|
+
for (const [field, value] of Object.entries(agent)) {
|
|
164
|
+
if (!LEGACY_AGENT_FIELDS.has(field))
|
|
165
|
+
cleaned[field] = value;
|
|
166
|
+
}
|
|
167
|
+
return cleaned;
|
|
168
|
+
}
|
|
169
|
+
function isLegacyBuiltinAgentRecord(agent) {
|
|
170
|
+
if (!isPlainObject(agent))
|
|
171
|
+
return false;
|
|
172
|
+
return agent.piDistribution === "builtin" || agent.runtime === "builtin-pi" || agent.runtimeOption === "builtin-pi";
|
|
173
|
+
}
|
|
174
|
+
function canonicalizeLegacyStoredAgent(key, agent) {
|
|
175
|
+
const cleaned = takeLegacyAgentFields(agent);
|
|
176
|
+
if (cleaned.runtime === "builtin-pi" || isLegacyBuiltinAgentRecord(agent))
|
|
177
|
+
cleaned.runtime = "pi";
|
|
178
|
+
if (typeof cleaned.runtime !== "string" || !cleaned.runtime)
|
|
179
|
+
throw new Error(`Agent ${key}.runtime 必须是非空字符串`);
|
|
180
|
+
return cleaned;
|
|
181
|
+
}
|
|
160
182
|
function validateStoredAgent(key, agent, version) {
|
|
161
183
|
if (!APP_ID.test(key))
|
|
162
184
|
throw new Error(`Agent key 必须是安全的飞书 App ID(cli_ + ASCII 字母数字):${key}`);
|
|
163
185
|
if (!isPlainObject(agent))
|
|
164
186
|
throw new Error(`Agent ${key} 必须是普通 object`);
|
|
165
|
-
|
|
166
|
-
|
|
187
|
+
const canonical = canonicalizeLegacyStoredAgent(key, agent);
|
|
188
|
+
assertAllowedFields(canonical, version === 3 ? AGENT_FIELDS_V3 : AGENT_FIELDS_V4, `Agent ${key}`);
|
|
189
|
+
if (typeof canonical.runtime !== "string" || !canonical.runtime)
|
|
167
190
|
throw new Error(`Agent ${key}.runtime 必须是非空字符串`);
|
|
168
|
-
if (!(0, user_runtime_js_1.isAdapterRuntime)(
|
|
169
|
-
throw new Error(`Agent ${key}.runtime 未知:${
|
|
170
|
-
if (typeof
|
|
191
|
+
if (!(0, user_runtime_js_1.isAdapterRuntime)(canonical.runtime))
|
|
192
|
+
throw new Error(`Agent ${key}.runtime 未知:${canonical.runtime}`);
|
|
193
|
+
if (typeof canonical.model !== "string" || !canonical.model)
|
|
171
194
|
throw new Error(`Agent ${key}.model 必须是非空字符串`);
|
|
172
|
-
assertModel(
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
}
|
|
177
|
-
}
|
|
195
|
+
assertModel(canonical.runtime, canonical.model);
|
|
196
|
+
Object.assign(agent, canonical);
|
|
197
|
+
for (const field of LEGACY_AGENT_FIELDS)
|
|
198
|
+
delete agent[field];
|
|
178
199
|
if (Object.hasOwn(agent, "effort") && (typeof agent.effort !== "string" || !agent.effort))
|
|
179
200
|
throw new Error(`Agent ${key}.effort 必须是非空字符串`);
|
|
180
201
|
if (agent.model === "default" && Object.hasOwn(agent, "effort"))
|
|
181
202
|
throw new Error(`Agent ${key}.model=default 时不能保存 effort`);
|
|
182
203
|
if (typeof agent.effort === "string") {
|
|
183
|
-
if (!runtimeSupportsEffort(
|
|
184
|
-
throw new Error(`Agent ${key}.effort=${agent.effort} 不在 ${
|
|
204
|
+
if (!runtimeSupportsEffort(canonical.runtime, agent.effort))
|
|
205
|
+
throw new Error(`Agent ${key}.effort=${agent.effort} 不在 ${canonical.runtime} 安全档位中`);
|
|
185
206
|
}
|
|
186
207
|
if (version === 3 && Object.hasOwn(agent, "noMentionChats")) {
|
|
187
208
|
if (!Array.isArray(agent.noMentionChats) || agent.noMentionChats.some((chat) => typeof chat !== "string" || !CHAT_ID.test(chat))) {
|
|
@@ -216,7 +237,6 @@ function hydratedStoredAgent(key, agent, version) {
|
|
|
216
237
|
return {
|
|
217
238
|
runtime: agent.runtime,
|
|
218
239
|
model: agent.model,
|
|
219
|
-
...(agent.piDistribution === "external" || agent.piDistribution === "builtin" ? { piDistribution: agent.piDistribution } : {}),
|
|
220
240
|
...(typeof agent.effort === "string" ? { effort: agent.effort } : {}),
|
|
221
241
|
...(version === 4 && (agent.mentionPolicy === "require" || agent.mentionPolicy === "free") ? { mentionPolicy: agent.mentionPolicy } : {}),
|
|
222
242
|
...(Object.keys(chatMentionPolicies).length ? { chatMentionPolicies, noMentionChats: Object.entries(chatMentionPolicies).filter(([, policy]) => policy === "free").map(([chatId]) => chatId) } : {}),
|
|
@@ -228,7 +248,6 @@ function hydrateAgent(key, agent, configDir) {
|
|
|
228
248
|
return {
|
|
229
249
|
name: key, agentId: key, feishuAppId: key, feishuProfile: key,
|
|
230
250
|
runtime: agent.runtime, model: agent.model,
|
|
231
|
-
...(agent.piDistribution ? { piDistribution: agent.piDistribution } : {}),
|
|
232
251
|
workspaceDir: layout.workspaceDir(key), stateDir: layout.agentStateDir(key),
|
|
233
252
|
larkConfigDir: path.join(layout.agentStateDir(key), "lark-cli-config"),
|
|
234
253
|
...(agent.effort ? { effort: agent.effort } : {}),
|
|
@@ -349,9 +368,137 @@ function readConfigFile(file, root = path.dirname(file)) {
|
|
|
349
368
|
throw error;
|
|
350
369
|
}
|
|
351
370
|
}
|
|
371
|
+
function collectLegacyBuiltinAgentIds(raw) {
|
|
372
|
+
if (!isPlainObject(raw) || !isPlainObject(raw.agents))
|
|
373
|
+
return [];
|
|
374
|
+
return Object.entries(raw.agents).filter(([, agent]) => isLegacyBuiltinAgentRecord(agent)).map(([agentId]) => agentId);
|
|
375
|
+
}
|
|
376
|
+
function ownedPiProviderDirectory(configDir, agentId) {
|
|
377
|
+
if (!APP_ID.test(agentId))
|
|
378
|
+
throw new Error(`Pi Agent ID 格式无效:${agentId}`);
|
|
379
|
+
return path.join(path.resolve(configDir), "providers", "pi", agentId);
|
|
380
|
+
}
|
|
381
|
+
function userPiHome(env) {
|
|
382
|
+
const home = typeof env.HOME === "string" && env.HOME.trim() ? env.HOME : os.homedir();
|
|
383
|
+
return path.resolve(home, ".pi");
|
|
384
|
+
}
|
|
385
|
+
function isInsideOrEqual(candidate, root) {
|
|
386
|
+
const relative = path.relative(root, candidate);
|
|
387
|
+
return relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative));
|
|
388
|
+
}
|
|
389
|
+
function pathExists(target) {
|
|
390
|
+
try {
|
|
391
|
+
fs.lstatSync(target);
|
|
392
|
+
return true;
|
|
393
|
+
}
|
|
394
|
+
catch (error) {
|
|
395
|
+
if (error.code === "ENOENT")
|
|
396
|
+
return false;
|
|
397
|
+
throw error;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
function shouldSkipOwnedPiDeletion(configDir, expected, env) {
|
|
401
|
+
let userPi;
|
|
402
|
+
try {
|
|
403
|
+
userPi = fs.realpathSync(userPiHome(env));
|
|
404
|
+
}
|
|
405
|
+
catch {
|
|
406
|
+
userPi = path.resolve(userPiHome(env));
|
|
407
|
+
}
|
|
408
|
+
let configRoot;
|
|
409
|
+
try {
|
|
410
|
+
configRoot = fs.realpathSync(path.resolve(configDir));
|
|
411
|
+
}
|
|
412
|
+
catch {
|
|
413
|
+
configRoot = path.resolve(configDir);
|
|
414
|
+
}
|
|
415
|
+
let resolvedExpected;
|
|
416
|
+
try {
|
|
417
|
+
resolvedExpected = fs.realpathSync(path.resolve(expected));
|
|
418
|
+
}
|
|
419
|
+
catch {
|
|
420
|
+
resolvedExpected = path.resolve(expected);
|
|
421
|
+
}
|
|
422
|
+
if (isInsideOrEqual(configRoot, userPi))
|
|
423
|
+
return "config root is inside the user ~/.pi directory";
|
|
424
|
+
if (isInsideOrEqual(resolvedExpected, userPi))
|
|
425
|
+
return "owned Pi directory is inside the user ~/.pi directory";
|
|
426
|
+
return null;
|
|
427
|
+
}
|
|
428
|
+
function deleteOwnedPiProviderDirectory(configDir, agentId, env = process.env) {
|
|
429
|
+
const expected = ownedPiProviderDirectory(configDir, agentId);
|
|
430
|
+
const skip = shouldSkipOwnedPiDeletion(configDir, expected, env);
|
|
431
|
+
if (skip) {
|
|
432
|
+
process.stderr.write(`[larkin] skipped leftover builtin Pi directory for ${agentId}: ${skip}\n`);
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
const parent = path.dirname(expected);
|
|
436
|
+
if (!pathExists(parent) || !pathExists(expected))
|
|
437
|
+
return;
|
|
438
|
+
assertNoSymlinkAncestors(expected);
|
|
439
|
+
const rootReal = fs.realpathSync(path.resolve(configDir));
|
|
440
|
+
const parentReal = fs.realpathSync(parent);
|
|
441
|
+
const relative = path.relative(rootReal, parentReal);
|
|
442
|
+
if (relative.startsWith(`..${path.sep}`) || relative === ".." || path.isAbsolute(relative)) {
|
|
443
|
+
throw new Error("owned Pi provider directory escaped the config root");
|
|
444
|
+
}
|
|
445
|
+
if (path.basename(parent) !== "pi" || path.basename(path.dirname(parent)) !== "providers") {
|
|
446
|
+
throw new Error("owned Pi provider directory is not providers/pi/<agentId>");
|
|
447
|
+
}
|
|
448
|
+
const stat = fs.lstatSync(expected);
|
|
449
|
+
if (stat.isSymbolicLink())
|
|
450
|
+
throw new Error("owned Pi provider directory is a symlink");
|
|
451
|
+
if (!stat.isDirectory())
|
|
452
|
+
throw new Error("owned Pi provider directory must be a directory");
|
|
453
|
+
fs.rmSync(expected, { recursive: true, force: false });
|
|
454
|
+
}
|
|
455
|
+
function rewriteLegacyBuiltinAgents(raw) {
|
|
456
|
+
if (!isPlainObject(raw) || !isPlainObject(raw.agents))
|
|
457
|
+
return raw;
|
|
458
|
+
const legacyIds = new Set(collectLegacyBuiltinAgentIds(raw));
|
|
459
|
+
if (legacyIds.size === 0)
|
|
460
|
+
return raw;
|
|
461
|
+
const agents = {};
|
|
462
|
+
for (const [agentId, agent] of Object.entries(raw.agents)) {
|
|
463
|
+
if (!legacyIds.has(agentId) || !isPlainObject(agent)) {
|
|
464
|
+
agents[agentId] = agent;
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
const next = takeLegacyAgentFields(agent);
|
|
468
|
+
if (next.runtime === "builtin-pi" || isLegacyBuiltinAgentRecord(agent))
|
|
469
|
+
next.runtime = "pi";
|
|
470
|
+
agents[agentId] = next;
|
|
471
|
+
}
|
|
472
|
+
return { ...raw, agents };
|
|
473
|
+
}
|
|
474
|
+
function persistLegacyBuiltinMigration(layout, opts, env) {
|
|
475
|
+
const current = readConfigFile(layout.configFile, layout.root);
|
|
476
|
+
const migratedIds = collectLegacyBuiltinAgentIds(current.raw);
|
|
477
|
+
if (migratedIds.length === 0)
|
|
478
|
+
return current;
|
|
479
|
+
const rewritten = rewriteLegacyBuiltinAgents(current.raw);
|
|
480
|
+
const config = normalizeConfig(structuredClone(rewritten), layout.root, opts);
|
|
481
|
+
const bytes = atomicWriteConfig(layout.configFile, rewritten);
|
|
482
|
+
for (const agentId of migratedIds) {
|
|
483
|
+
const agent = config.agents[agentId];
|
|
484
|
+
const model = typeof agent?.model === "string" ? agent.model : "unknown";
|
|
485
|
+
process.stderr.write(`[larkin] migrated Agent ${agentId} from builtin-pi to pi (model=${model})\n`);
|
|
486
|
+
try {
|
|
487
|
+
deleteOwnedPiProviderDirectory(layout.root, agentId, env);
|
|
488
|
+
}
|
|
489
|
+
catch (error) {
|
|
490
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
491
|
+
process.stderr.write(`[larkin] failed to delete leftover builtin Pi directory for ${agentId}: ${reason}\n`);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
return { raw: rewritten, bytes };
|
|
495
|
+
}
|
|
352
496
|
function loadConfig(env = process.env, opts = {}) {
|
|
353
497
|
const layout = root_layout_js_1.TargetRootLayout.fromConfigDir(resolveConfigDir(env));
|
|
354
|
-
const
|
|
498
|
+
const initial = readConfigFile(layout.configFile, layout.root);
|
|
499
|
+
const { raw, bytes } = collectLegacyBuiltinAgentIds(initial.raw).length
|
|
500
|
+
? withConfigLock(layout, () => persistLegacyBuiltinMigration(layout, opts, env))
|
|
501
|
+
: initial;
|
|
355
502
|
return { configDir: layout.configDir, file: layout.configFile, revision: revision(bytes), config: normalizeConfig(raw, layout.root, opts) };
|
|
356
503
|
}
|
|
357
504
|
function selectAgent(config, env = process.env) {
|
|
@@ -372,8 +519,6 @@ function toStored(config) {
|
|
|
372
519
|
const out = { version: 4, serverId: config.serverId, mentionPolicy: config.mentionPolicy, activeAgent: config.activeAgent, agents: {} };
|
|
373
520
|
for (const [key, agent] of Object.entries(config.agents || {})) {
|
|
374
521
|
const stored = { runtime: agent.runtime, model: agent.model };
|
|
375
|
-
if (agent.piDistribution)
|
|
376
|
-
stored.piDistribution = agent.piDistribution;
|
|
377
522
|
if (typeof agent.effort === "string" && agent.effort)
|
|
378
523
|
stored.effort = agent.effort;
|
|
379
524
|
if (agent.mentionPolicy)
|
|
@@ -415,7 +560,8 @@ function agentSignature(config, agentId) {
|
|
|
415
560
|
throw new Error(`Agent 不存在:${agentId}`);
|
|
416
561
|
const chats = Object.fromEntries(Object.entries(agent.chatMentionPolicies || {}).sort(([left], [right]) => left.localeCompare(right)));
|
|
417
562
|
return revision(JSON.stringify({
|
|
418
|
-
|
|
563
|
+
schema: 2,
|
|
564
|
+
runtime: agent.runtime, model: agent.model, effort: agent.effort ?? null,
|
|
419
565
|
globalMentionPolicy: config.mentionPolicy, agentMentionPolicy: agent.mentionPolicy ?? null, chatMentionPolicies: chats,
|
|
420
566
|
}));
|
|
421
567
|
}
|
|
@@ -452,12 +598,6 @@ function writeApplyFile(configDir, value) {
|
|
|
452
598
|
atomicWriteConfig(applyStateFile(configDir), value);
|
|
453
599
|
}
|
|
454
600
|
function rollbackJournalFile(root) { return path.join(root, CONFIG_ROLLBACK_JOURNAL_FILE); }
|
|
455
|
-
function migrationJournalFile(root) { return path.join(root, ".pi-profile-migration-journal.json"); }
|
|
456
|
-
function assertMigrationTarget(root, agentId, migration) {
|
|
457
|
-
if (migration.agentId !== agentId || path.resolve(migration.targetDir) !== path.join(path.resolve(root), "providers", "pi", agentId)) {
|
|
458
|
-
throw new Error("Pi profile migration target is invalid");
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
601
|
function restoreTargetApplyState(root, targetAgentId, beforeRevision, before) {
|
|
462
602
|
const current = readApplyFile(root);
|
|
463
603
|
const agents = { ...current.agents };
|
|
@@ -496,15 +636,9 @@ function recoverRollbackJournal(root) {
|
|
|
496
636
|
const beforeRaw = JSON.parse(beforeBytes.toString("utf8"));
|
|
497
637
|
normalizeConfig(beforeRaw, root);
|
|
498
638
|
validateApplyState(journal.beforeApplyState);
|
|
499
|
-
if (journal.migration !== undefined) {
|
|
500
|
-
(0, pi_profile_migration_js_1.validatePiProfileMigrationState)(journal.migration);
|
|
501
|
-
assertMigrationTarget(root, journal.targetAgentId, journal.migration);
|
|
502
|
-
}
|
|
503
639
|
const current = readConfigFile(path.join(root, "config.json"), root);
|
|
504
640
|
const currentRevision = revision(current.bytes);
|
|
505
641
|
if (currentRevision === journal.expectedAfterRevision) {
|
|
506
|
-
if (journal.migration)
|
|
507
|
-
(0, pi_profile_migration_js_1.rollbackPiProfileMigration)(journal.migration);
|
|
508
642
|
atomicWriteBytes(path.join(root, "config.json"), beforeBytes);
|
|
509
643
|
restoreTargetApplyState(root, journal.targetAgentId, journal.beforeRevision, journal.beforeApplyState);
|
|
510
644
|
}
|
|
@@ -519,47 +653,6 @@ function recoverRollbackJournal(root) {
|
|
|
519
653
|
fs.unlinkSync(file);
|
|
520
654
|
(0, secure_metadata_js_1.fsyncDirectoryOf)(file);
|
|
521
655
|
}
|
|
522
|
-
function recoverMigrationJournal(root) {
|
|
523
|
-
const file = migrationJournalFile(root);
|
|
524
|
-
const bytes = readPrivateFile(file, root, PROFILE_MIGRATION_ROLLBACK_LIMIT_BYTES, "Pi profile migration journal");
|
|
525
|
-
if (bytes === null)
|
|
526
|
-
return;
|
|
527
|
-
let journal;
|
|
528
|
-
try {
|
|
529
|
-
journal = JSON.parse(bytes.toString("utf8"));
|
|
530
|
-
}
|
|
531
|
-
catch {
|
|
532
|
-
throw new Error("Pi profile migration journal is invalid");
|
|
533
|
-
}
|
|
534
|
-
if (journal.version !== 1 || journal.phase !== "forward" || typeof journal.targetAgentId !== "string"
|
|
535
|
-
|| typeof journal.beforeRevision !== "string" || typeof journal.expectedAfterRevision !== "string" || !journal.migration) {
|
|
536
|
-
throw new Error("Pi profile migration journal is invalid");
|
|
537
|
-
}
|
|
538
|
-
(0, pi_profile_migration_js_1.validatePiProfileMigrationState)(journal.migration);
|
|
539
|
-
assertMigrationTarget(root, journal.targetAgentId, journal.migration);
|
|
540
|
-
const current = readConfigFile(path.join(root, "config.json"), root);
|
|
541
|
-
const currentRevision = revision(current.bytes);
|
|
542
|
-
if (currentRevision === journal.beforeRevision) {
|
|
543
|
-
// Forward import did not publish config. The migration helper restores only
|
|
544
|
-
// files still matching its expected post-import hashes.
|
|
545
|
-
try {
|
|
546
|
-
(0, pi_profile_migration_js_1.rollbackPiProfileMigration)(journal.migration);
|
|
547
|
-
}
|
|
548
|
-
catch (error) {
|
|
549
|
-
throw new Error(`Pi profile migration recovery refused: ${error instanceof Error ? error.message : String(error)}`);
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
else if (currentRevision === journal.expectedAfterRevision) {
|
|
553
|
-
// Config won the race; verify the provider target before finalizing.
|
|
554
|
-
const migration = journal.migration;
|
|
555
|
-
(0, pi_profile_migration_js_1.assertPiProfileMigrationAfterState)(migration);
|
|
556
|
-
}
|
|
557
|
-
else
|
|
558
|
-
throw new Error("Pi profile migration journal conflicts with the current configuration");
|
|
559
|
-
(0, pi_profile_migration_js_1.releasePiProfileMigrationLock)(journal.migration);
|
|
560
|
-
fs.unlinkSync(file);
|
|
561
|
-
(0, secure_metadata_js_1.fsyncDirectoryOf)(file);
|
|
562
|
-
}
|
|
563
656
|
function configApplyState(env, config) {
|
|
564
657
|
let state;
|
|
565
658
|
try {
|
|
@@ -636,32 +729,13 @@ function applyMutation(config, mutation) {
|
|
|
636
729
|
agent.noMentionChats = Object.entries(policies).filter(([, value]) => value === "free").map(([chatId]) => chatId);
|
|
637
730
|
return { scope: "chat", agentId: mutation.agentId, runtimeChange: false, affectedAgentIds: [mutation.agentId] };
|
|
638
731
|
}
|
|
639
|
-
if (mutation.kind === "set-agent-
|
|
640
|
-
if (agent.runtime !== "pi")
|
|
641
|
-
throw new Error(`Agent ${mutation.agentId} 不是 Pi runtime`);
|
|
642
|
-
if (mutation.distribution !== "builtin" && mutation.distribution !== "external")
|
|
643
|
-
throw new Error("Pi distribution 只允许 builtin/external");
|
|
644
|
-
agent.piDistribution = mutation.distribution;
|
|
645
|
-
}
|
|
646
|
-
else if (mutation.kind === "set-agent-runtime") {
|
|
732
|
+
if (mutation.kind === "set-agent-runtime") {
|
|
647
733
|
if (!(0, user_runtime_js_1.isUserRuntime)(mutation.runtime))
|
|
648
734
|
throw new Error(`未知 runtime:${mutation.runtime}`);
|
|
649
735
|
const stored = (0, user_runtime_js_1.fromUserRuntime)(mutation.runtime);
|
|
650
|
-
if (stored.piDistribution === "builtin") {
|
|
651
|
-
try {
|
|
652
|
-
(0, pi_provider_config_js_1.assertBuiltinPiAgentDirectory)((0, pi_provider_config_js_1.piAgentDirectory)(config.configDir, mutation.agentId));
|
|
653
|
-
}
|
|
654
|
-
catch (error) {
|
|
655
|
-
throw new Error(`无法切换到 builtin-pi:${error instanceof Error ? error.message : String(error)}。请先运行 larkin setup 并为该 Agent 选择 builtin-pi 完成 provider 配置`);
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
736
|
assertModel(stored.runtime, mutation.model || "default");
|
|
659
737
|
agent.runtime = stored.runtime;
|
|
660
738
|
agent.model = mutation.model || "default";
|
|
661
|
-
if (stored.piDistribution)
|
|
662
|
-
agent.piDistribution = stored.piDistribution;
|
|
663
|
-
else
|
|
664
|
-
delete agent.piDistribution;
|
|
665
739
|
delete agent.effort;
|
|
666
740
|
}
|
|
667
741
|
else if (mutation.kind === "set-agent-model") {
|
|
@@ -815,7 +889,7 @@ function assertConfigRoot(root) {
|
|
|
815
889
|
if (typeof process.getuid === "function" && stat.uid !== process.getuid())
|
|
816
890
|
throw new Error("config root owner is unsafe");
|
|
817
891
|
}
|
|
818
|
-
function
|
|
892
|
+
function acquireConfigLock(layout) {
|
|
819
893
|
assertNoSymlinkAncestors(layout.root);
|
|
820
894
|
fs.mkdirSync(layout.root, { recursive: true, mode: 0o700 });
|
|
821
895
|
const rootStat = fs.lstatSync(layout.root);
|
|
@@ -854,20 +928,41 @@ function withConfigLock(layout, action) {
|
|
|
854
928
|
}
|
|
855
929
|
if (!acquired)
|
|
856
930
|
throw new Error("配置正被其他进程修改,请稍后重试");
|
|
931
|
+
return {
|
|
932
|
+
release() {
|
|
933
|
+
const current = readLockRecord(lock);
|
|
934
|
+
if (current?.record?.nonce === owner.nonce && current.record.pid === owner.pid) {
|
|
935
|
+
try {
|
|
936
|
+
fs.unlinkSync(lock);
|
|
937
|
+
(0, secure_metadata_js_1.fsyncDirectoryOf)(lock);
|
|
938
|
+
}
|
|
939
|
+
catch { /* best effort */ }
|
|
940
|
+
}
|
|
941
|
+
},
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
function withConfigLock(layout, action) {
|
|
945
|
+
const held = acquireConfigLock(layout);
|
|
857
946
|
try {
|
|
858
|
-
recoverMigrationJournal(layout.root);
|
|
859
947
|
recoverRollbackJournal(layout.root);
|
|
860
948
|
return action();
|
|
861
949
|
}
|
|
862
950
|
finally {
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
951
|
+
held.release();
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
function withConfigMutationLock(env, action) {
|
|
955
|
+
return withConfigLock(root_layout_js_1.TargetRootLayout.fromConfigDir(resolveConfigDir(env)), action);
|
|
956
|
+
}
|
|
957
|
+
async function withConfigMutationLockAsync(env, action) {
|
|
958
|
+
const layout = root_layout_js_1.TargetRootLayout.fromConfigDir(resolveConfigDir(env));
|
|
959
|
+
const held = acquireConfigLock(layout);
|
|
960
|
+
try {
|
|
961
|
+
recoverRollbackJournal(layout.root);
|
|
962
|
+
return await action();
|
|
963
|
+
}
|
|
964
|
+
finally {
|
|
965
|
+
held.release();
|
|
871
966
|
}
|
|
872
967
|
}
|
|
873
968
|
const CONFIG_ROLLBACK_JOURNAL_FILE = ".config-rollback-journal.json";
|
|
@@ -962,10 +1057,6 @@ function readConfigSnapshot(file, root) {
|
|
|
962
1057
|
if (JSON.stringify(beforeRaw) !== JSON.stringify(parsed.beforeConfig))
|
|
963
1058
|
throw new Error("config snapshot before config does not match bytes");
|
|
964
1059
|
validateApplyState(parsed.beforeApplyState);
|
|
965
|
-
if (parsed.migration !== undefined) {
|
|
966
|
-
(0, pi_profile_migration_js_1.validatePiProfileMigrationState)(parsed.migration);
|
|
967
|
-
assertMigrationTarget(root, parsed.targetAgentId, parsed.migration);
|
|
968
|
-
}
|
|
969
1060
|
return parsed;
|
|
970
1061
|
}
|
|
971
1062
|
function atomicWriteBytes(file, bytes, limit = CONFIG_LIMIT_BYTES) {
|
|
@@ -1009,12 +1100,6 @@ function mutateConfig(env, mutation, authority, options = {}) {
|
|
|
1009
1100
|
throw new Error(`没找到配置 ${layout.configFile},先跑 larkin setup`);
|
|
1010
1101
|
const config = normalizeConfig(current.raw, layout.root);
|
|
1011
1102
|
const priorSignatures = Object.fromEntries(Object.keys(config.agents).map((agentId) => [agentId, agentSignature(config, agentId)]));
|
|
1012
|
-
if (options.importExternalProfile && (mutation.kind !== "set-agent-pi-distribution" || mutation.distribution !== "builtin")) {
|
|
1013
|
-
throw new Error("external Pi profile import is only valid for builtin Pi distribution");
|
|
1014
|
-
}
|
|
1015
|
-
const migrationPlan = options.importExternalProfile && mutation.kind === "set-agent-pi-distribution"
|
|
1016
|
-
? (0, pi_profile_migration_js_1.preparePiProfileMigration)(env, layout.root, mutation.agentId, "builtin")
|
|
1017
|
-
: undefined;
|
|
1018
1103
|
const changed = applyMutation(config, mutation);
|
|
1019
1104
|
const stored = toStored(config);
|
|
1020
1105
|
normalizeConfig(stored, layout.root);
|
|
@@ -1035,16 +1120,8 @@ function mutateConfig(env, mutation, authority, options = {}) {
|
|
|
1035
1120
|
version: 2, targetAgentId: changed.agentId, beforeRevision: revision(current.bytes), afterRevision: nextRevision,
|
|
1036
1121
|
afterSignature: agentSignature(config, changed.agentId), beforeConfig: current.raw,
|
|
1037
1122
|
beforeConfigBytes: current.bytes.toString("base64"), beforeApplyState,
|
|
1038
|
-
...(migrationPlan ? { migration: migrationPlan.state } : {}),
|
|
1039
1123
|
});
|
|
1040
1124
|
}
|
|
1041
|
-
if (migrationPlan) {
|
|
1042
|
-
atomicWriteConfig(migrationJournalFile(layout.root), {
|
|
1043
|
-
version: 1, phase: "forward", targetAgentId: changed.agentId, beforeRevision: revision(current.bytes),
|
|
1044
|
-
expectedAfterRevision: nextRevision, migration: migrationPlan.state,
|
|
1045
|
-
}, PROFILE_MIGRATION_ROLLBACK_LIMIT_BYTES);
|
|
1046
|
-
(0, pi_profile_migration_js_1.applyPiProfileMigration)(migrationPlan);
|
|
1047
|
-
}
|
|
1048
1125
|
atomicWriteConfig(layout.configFile, stored);
|
|
1049
1126
|
let fullyApplied = changed.affectedAgentIds.length === 0;
|
|
1050
1127
|
try {
|
|
@@ -1068,17 +1145,6 @@ function mutateConfig(env, mutation, authority, options = {}) {
|
|
|
1068
1145
|
catch {
|
|
1069
1146
|
fullyApplied = false; /* Config persistence remains authoritative. */
|
|
1070
1147
|
}
|
|
1071
|
-
if (migrationPlan) {
|
|
1072
|
-
(0, pi_profile_migration_js_1.releasePiProfileMigrationLock)(migrationPlan.state);
|
|
1073
|
-
try {
|
|
1074
|
-
fs.unlinkSync(migrationJournalFile(layout.root));
|
|
1075
|
-
(0, secure_metadata_js_1.fsyncDirectoryOf)(migrationJournalFile(layout.root));
|
|
1076
|
-
}
|
|
1077
|
-
catch (error) {
|
|
1078
|
-
if (error.code !== "ENOENT")
|
|
1079
|
-
throw error;
|
|
1080
|
-
}
|
|
1081
|
-
}
|
|
1082
1148
|
return {
|
|
1083
1149
|
revision: nextRevision, previousRevision: revision(current.bytes), changedScope: changed.scope,
|
|
1084
1150
|
...(changed.agentId ? { agentId: changed.agentId } : {}), persisted: true,
|
|
@@ -1102,11 +1168,8 @@ function rollbackConfig(env, snapshotFile) {
|
|
|
1102
1168
|
const journal = {
|
|
1103
1169
|
version: 1, targetAgentId: snapshot.targetAgentId, expectedAfterRevision: snapshot.afterRevision,
|
|
1104
1170
|
beforeRevision: snapshot.beforeRevision, beforeConfigBytes: snapshot.beforeConfigBytes, beforeApplyState: snapshot.beforeApplyState,
|
|
1105
|
-
...(snapshot.migration ? { migration: snapshot.migration } : {}),
|
|
1106
1171
|
};
|
|
1107
1172
|
atomicWriteConfig(rollbackJournalFile(layout.root), journal, PROFILE_MIGRATION_ROLLBACK_LIMIT_BYTES);
|
|
1108
|
-
if (snapshot.migration)
|
|
1109
|
-
(0, pi_profile_migration_js_1.rollbackPiProfileMigration)(snapshot.migration);
|
|
1110
1173
|
const bytes = atomicWriteBytes(layout.configFile, beforeBytes);
|
|
1111
1174
|
restoreTargetApplyState(layout.root, snapshot.targetAgentId, snapshot.beforeRevision, snapshot.beforeApplyState);
|
|
1112
1175
|
fs.unlinkSync(rollbackJournalFile(layout.root));
|
|
@@ -1128,7 +1191,7 @@ function commitSetupConfig(env, expectedRevision, nextStored) {
|
|
|
1128
1191
|
function safeConfigView(config, onlyAgentId, chatId, applyState) {
|
|
1129
1192
|
const entries = onlyAgentId ? [[onlyAgentId, config.agents[onlyAgentId]]] : Object.entries(config.agents);
|
|
1130
1193
|
const agents = entries.filter((entry) => Boolean(entry[1])).map(([agentId, agent]) => ({
|
|
1131
|
-
agentId, runtime: agent.runtime, runtimeOption: (0, user_runtime_js_1.toUserRuntime)(agent.runtime
|
|
1194
|
+
agentId, runtime: agent.runtime, runtimeOption: (0, user_runtime_js_1.toUserRuntime)(agent.runtime), model: agent.model, effort: agent.effort ?? null,
|
|
1132
1195
|
mention: {
|
|
1133
1196
|
override: agent.mentionPolicy ?? "inherit",
|
|
1134
1197
|
effective: agent.mentionPolicy ?? config.mentionPolicy,
|