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
@@ -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", "piDistribution", "effort", "mentionPolicy", "chatMentionPolicies", "createdAt"]);
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
- assertAllowedFields(agent, version === 3 ? AGENT_FIELDS_V3 : AGENT_FIELDS_V4, `Agent ${key}`);
100
- if (typeof agent.runtime !== "string" || !agent.runtime)
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(agent.runtime))
103
- throw new Error(`Agent ${key}.runtime 未知:${agent.runtime}`);
104
- if (typeof agent.model !== "string" || !agent.model)
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(agent.runtime, agent.model);
107
- if (Object.hasOwn(agent, "piDistribution")) {
108
- if (agent.runtime !== "pi" || (agent.piDistribution !== "external" && agent.piDistribution !== "builtin")) {
109
- throw new Error(`Agent ${key}.piDistribution 只允许 Pi runtime 使用 external/builtin`);
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(agent.runtime, agent.effort))
118
- throw new Error(`Agent ${key}.effort=${agent.effort} 不在 ${agent.runtime} 安全档位中`);
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 { raw, bytes } = readConfigFile(layout.configFile, layout.root);
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
- runtime: agent.runtime, model: agent.model, piDistribution: agent.piDistribution ?? null, effort: agent.effort ?? null,
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-pi-distribution") {
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 withConfigLock(layout, action) {
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
- const current = readLockRecord(lock);
798
- if (current?.record?.nonce === owner.nonce && current.record.pid === owner.pid) {
799
- try {
800
- fs.unlinkSync(lock);
801
- fsyncDirectoryOf(lock);
802
- }
803
- catch { /* best effort */ }
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, agent.piDistribution), model: agent.model, piDistribution: agent.piDistribution ?? (agent.runtime === "pi" ? "external" : null), effort: agent.effort ?? null,
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 = [".larkin-official-pi-package", "models-store.json", "npm"];
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-builtin-pi", directory: { device: String(target.dev), inode: String(target.ino) },
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
- * 由 Larkin 向 builtin Pi 传入 inline factory,或通过 external Pi 的
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 仍为 external Pi
14
- * 单独产出注入 bundle。
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
- // Built-in bash tool definition (keeps renderCall/renderResult/system-prompt metadata).
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
- const pi_provider_config_js_1 = require("./pi-provider-config.cjs");
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 = pi_provider_config_js_1.BUNDLED_PI_VERSION.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
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 ${pi_provider_config_js_1.BUNDLED_PI_VERSION}`);
265
+ throw new Error(`Pi executable version must be exactly ${exports.PINNED_PI_VERSION}`);
266
266
  }
267
- return pi_provider_config_js_1.BUNDLED_PI_VERSION;
267
+ return exports.PINNED_PI_VERSION;
268
268
  }
269
269
  function verifyPiCapabilities(capabilities) {
270
- if (capabilities.version !== pi_provider_config_js_1.BUNDLED_PI_VERSION)
271
- throw new Error(`trusted Pi version ${pi_provider_config_js_1.BUNDLED_PI_VERSION} is required`);
272
- if (capabilities.distribution === "external" && capabilities.trustedProtocol === true) {
273
- throw new Error("external Pi cannot use the bundled trusted protocol bypass");
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
- import { BUNDLED_PI_VERSION } from "./pi-provider-config.mjs";
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 = BUNDLED_PI_VERSION.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
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 ${BUNDLED_PI_VERSION}`);
243
+ throw new Error(`Pi executable version must be exactly ${PINNED_PI_VERSION}`);
244
244
  }
245
- return BUNDLED_PI_VERSION;
245
+ return PINNED_PI_VERSION;
246
246
  }
247
247
  export function verifyPiCapabilities(capabilities) {
248
- if (capabilities.version !== BUNDLED_PI_VERSION)
249
- throw new Error(`trusted Pi version ${BUNDLED_PI_VERSION} is required`);
250
- if (capabilities.distribution === "external" && capabilities.trustedProtocol === true) {
251
- throw new Error("external Pi cannot use the bundled trusted protocol bypass");
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 {};