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.
Files changed (55) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +5 -9
  3. package/artifacts/release/THIRD_PARTY_NOTICES.txt +751 -4425
  4. package/dist/app/agent-cli.mjs +14 -2
  5. package/dist/app/agent-config.mjs +26 -84
  6. package/dist/app/binary-entry.mjs +0 -20
  7. package/dist/app/cli.mjs +8 -14
  8. package/dist/app/internal-command.cjs +0 -2
  9. package/dist/app/internal-command.mjs +0 -2
  10. package/dist/app/local-control.mjs +16 -3
  11. package/dist/app/runtime-model-directory.mjs +2 -16
  12. package/dist/app/runtime-process.mjs +42 -6
  13. package/dist/app/setup.mjs +52 -57
  14. package/dist/dashboard/dashboard-config-controller.mjs +83 -24
  15. package/dist/dashboard/dashboard-view-model.mjs +4 -7
  16. package/dist/dashboard/web/assets/dashboard.js +1 -1
  17. package/dist/feishu/host-shell.cjs +68 -17
  18. package/dist/feishu/host-shell.mjs +69 -18
  19. package/dist/platform/config.cjs +201 -138
  20. package/dist/platform/config.mjs +199 -138
  21. package/dist/runtime/pi-artifact-provenance.mjs +3 -3
  22. package/dist/runtime/pi-bash-timeout-extension.mjs +4 -5
  23. package/dist/runtime/pi-compaction-recovery.cjs +9 -11
  24. package/dist/runtime/pi-compaction-recovery.mjs +8 -10
  25. package/dist/runtime/pi-extension-api.mjs +1 -0
  26. package/dist/runtime/pi-model-catalog.cjs +7 -8
  27. package/dist/runtime/pi-model-catalog.mjs +7 -8
  28. package/dist/runtime/pi-subagent-injection.mjs +17 -4
  29. package/dist/runtime/runtime-adapters.mjs +41 -38
  30. package/dist/runtime/runtime-host.cjs +197 -20
  31. package/dist/runtime/runtime-host.mjs +198 -21
  32. package/dist/runtime/runtime-model-catalog.cjs +1 -1
  33. package/dist/runtime/runtime-model-catalog.mjs +1 -1
  34. package/dist/runtime/runtime-readiness.cjs +112 -49
  35. package/dist/runtime/runtime-readiness.mjs +103 -49
  36. package/dist/runtime/user-runtime.cjs +7 -24
  37. package/dist/runtime/user-runtime.mjs +7 -23
  38. package/dist/setup/bot-register.mjs +15 -123
  39. package/dist/setup/setup-agent-choice.mjs +48 -74
  40. package/dist/setup/setup-bind.mjs +51 -151
  41. package/dist/setup/setup-binding.mjs +6 -9
  42. package/package.json +3 -2
  43. package/dist/app/pi-auth-cli.mjs +0 -67
  44. package/dist/platform/process-boundary-trace.cjs +0 -108
  45. package/dist/runtime/builtin-pi-assets.cjs +0 -117
  46. package/dist/runtime/builtin-pi-assets.mjs +0 -105
  47. package/dist/runtime/pi-artifact-provenance.cjs +0 -147
  48. package/dist/runtime/pi-distribution-label.cjs +0 -6
  49. package/dist/runtime/pi-distribution-label.mjs +0 -3
  50. package/dist/runtime/pi-inline-extensions.mjs +0 -78
  51. package/dist/runtime/pi-official-auth.mjs +0 -491
  52. package/dist/runtime/pi-profile-migration.cjs +0 -764
  53. package/dist/runtime/pi-profile-migration.mjs +0 -719
  54. package/dist/runtime/pi-provider-config.cjs +0 -331
  55. package/dist/runtime/pi-provider-config.mjs +0 -314
@@ -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", "piDistribution", "effort", "mentionPolicy", "chatMentionPolicies", "createdAt"]);
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
- assertAllowedFields(agent, version === 3 ? AGENT_FIELDS_V3 : AGENT_FIELDS_V4, `Agent ${key}`);
166
- if (typeof agent.runtime !== "string" || !agent.runtime)
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)(agent.runtime))
169
- throw new Error(`Agent ${key}.runtime 未知:${agent.runtime}`);
170
- if (typeof agent.model !== "string" || !agent.model)
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(agent.runtime, agent.model);
173
- if (Object.hasOwn(agent, "piDistribution")) {
174
- if (agent.runtime !== "pi" || (agent.piDistribution !== "external" && agent.piDistribution !== "builtin")) {
175
- throw new Error(`Agent ${key}.piDistribution 只允许 Pi runtime 使用 external/builtin`);
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(agent.runtime, agent.effort))
184
- throw new Error(`Agent ${key}.effort=${agent.effort} 不在 ${agent.runtime} 安全档位中`);
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 { raw, bytes } = readConfigFile(layout.configFile, layout.root);
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
- runtime: agent.runtime, model: agent.model, piDistribution: agent.piDistribution ?? null, effort: agent.effort ?? null,
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-pi-distribution") {
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 withConfigLock(layout, action) {
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
- const current = readLockRecord(lock);
864
- if (current?.record?.nonce === owner.nonce && current.record.pid === owner.pid) {
865
- try {
866
- fs.unlinkSync(lock);
867
- (0, secure_metadata_js_1.fsyncDirectoryOf)(lock);
868
- }
869
- catch { /* best effort */ }
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, agent.piDistribution), model: agent.model, piDistribution: agent.piDistribution ?? (agent.runtime === "pi" ? "external" : null), effort: agent.effort ?? null,
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,