larkin 0.4.29 → 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 (57) 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 -86
  6. package/dist/app/binary-entry.mjs +0 -20
  7. package/dist/app/cli.mjs +6 -22
  8. package/dist/app/internal-command.cjs +0 -2
  9. package/dist/app/internal-command.mjs +0 -2
  10. package/dist/app/runtime-model-directory.mjs +2 -16
  11. package/dist/app/runtime-process.mjs +0 -1
  12. package/dist/app/setup.mjs +52 -57
  13. package/dist/dashboard/dashboard-config-controller.mjs +43 -109
  14. package/dist/dashboard/dashboard-view-model.mjs +4 -7
  15. package/dist/dashboard/web/assets/dashboard.css +1 -1
  16. package/dist/dashboard/web/assets/dashboard.js +15 -15
  17. package/dist/feishu/host-shell.cjs +1 -2
  18. package/dist/feishu/host-shell.mjs +1 -2
  19. package/dist/platform/config.cjs +168 -133
  20. package/dist/platform/config.mjs +168 -133
  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 +26 -43
  30. package/dist/runtime/runtime-host.cjs +21 -43
  31. package/dist/runtime/runtime-host.mjs +22 -44
  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 +42 -99
  35. package/dist/runtime/runtime-readiness.mjs +40 -98
  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 -277
  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.cjs +0 -594
  52. package/dist/runtime/pi-official-auth.mjs +0 -544
  53. package/dist/runtime/pi-profile-migration.cjs +0 -764
  54. package/dist/runtime/pi-profile-migration.mjs +0 -719
  55. package/dist/runtime/pi-provider-config.cjs +0 -354
  56. package/dist/runtime/pi-provider-config.mjs +0 -334
  57. package/dist/runtime/pi-provider-login.mjs +0 -240
@@ -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,38 +661,16 @@ 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)}。请使用 Dashboard Provider Credentials 或 larkin pi-auth login。`);
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") {
602
- if (mutation.requireBuiltinPi && (agent.runtime !== "pi" || agent.piDistribution !== "builtin")) {
603
- throw new Error(`Agent ${mutation.agentId} 不是内置 Pi`);
604
- }
605
674
  if (!mutation.model.trim())
606
675
  throw new Error("model 不能为空");
607
676
  assertModel(agent.runtime, mutation.model);
@@ -807,7 +876,6 @@ function acquireConfigLock(layout) {
807
876
  function withConfigLock(layout, action) {
808
877
  const held = acquireConfigLock(layout);
809
878
  try {
810
- recoverMigrationJournal(layout.root);
811
879
  recoverRollbackJournal(layout.root);
812
880
  return action();
813
881
  }
@@ -822,7 +890,6 @@ export async function withConfigMutationLockAsync(env, action) {
822
890
  const layout = TargetRootLayout.fromConfigDir(resolveConfigDir(env));
823
891
  const held = acquireConfigLock(layout);
824
892
  try {
825
- recoverMigrationJournal(layout.root);
826
893
  recoverRollbackJournal(layout.root);
827
894
  return await action();
828
895
  }
@@ -922,10 +989,6 @@ function readConfigSnapshot(file, root) {
922
989
  if (JSON.stringify(beforeRaw) !== JSON.stringify(parsed.beforeConfig))
923
990
  throw new Error("config snapshot before config does not match bytes");
924
991
  validateApplyState(parsed.beforeApplyState);
925
- if (parsed.migration !== undefined) {
926
- validatePiProfileMigrationState(parsed.migration);
927
- assertMigrationTarget(root, parsed.targetAgentId, parsed.migration);
928
- }
929
992
  return parsed;
930
993
  }
931
994
  function atomicWriteBytes(file, bytes, limit = CONFIG_LIMIT_BYTES) {
@@ -969,12 +1032,6 @@ export function mutateConfig(env, mutation, authority, options = {}) {
969
1032
  throw new Error(`没找到配置 ${layout.configFile},先跑 larkin setup`);
970
1033
  const config = normalizeConfig(current.raw, layout.root);
971
1034
  const priorSignatures = Object.fromEntries(Object.keys(config.agents).map((agentId) => [agentId, agentSignature(config, agentId)]));
972
- if (options.importExternalProfile && (mutation.kind !== "set-agent-pi-distribution" || mutation.distribution !== "builtin")) {
973
- throw new Error("external Pi profile import is only valid for builtin Pi distribution");
974
- }
975
- const migrationPlan = options.importExternalProfile && mutation.kind === "set-agent-pi-distribution"
976
- ? preparePiProfileMigration(env, layout.root, mutation.agentId, "builtin")
977
- : undefined;
978
1035
  const changed = applyMutation(config, mutation);
979
1036
  const stored = toStored(config);
980
1037
  normalizeConfig(stored, layout.root);
@@ -995,16 +1052,8 @@ export function mutateConfig(env, mutation, authority, options = {}) {
995
1052
  version: 2, targetAgentId: changed.agentId, beforeRevision: revision(current.bytes), afterRevision: nextRevision,
996
1053
  afterSignature: agentSignature(config, changed.agentId), beforeConfig: current.raw,
997
1054
  beforeConfigBytes: current.bytes.toString("base64"), beforeApplyState,
998
- ...(migrationPlan ? { migration: migrationPlan.state } : {}),
999
1055
  });
1000
1056
  }
1001
- if (migrationPlan) {
1002
- atomicWriteConfig(migrationJournalFile(layout.root), {
1003
- version: 1, phase: "forward", targetAgentId: changed.agentId, beforeRevision: revision(current.bytes),
1004
- expectedAfterRevision: nextRevision, migration: migrationPlan.state,
1005
- }, PROFILE_MIGRATION_ROLLBACK_LIMIT_BYTES);
1006
- applyPiProfileMigration(migrationPlan);
1007
- }
1008
1057
  atomicWriteConfig(layout.configFile, stored);
1009
1058
  let fullyApplied = changed.affectedAgentIds.length === 0;
1010
1059
  try {
@@ -1028,17 +1077,6 @@ export function mutateConfig(env, mutation, authority, options = {}) {
1028
1077
  catch {
1029
1078
  fullyApplied = false; /* Config persistence remains authoritative. */
1030
1079
  }
1031
- if (migrationPlan) {
1032
- releasePiProfileMigrationLock(migrationPlan.state);
1033
- try {
1034
- fs.unlinkSync(migrationJournalFile(layout.root));
1035
- fsyncDirectoryOf(migrationJournalFile(layout.root));
1036
- }
1037
- catch (error) {
1038
- if (error.code !== "ENOENT")
1039
- throw error;
1040
- }
1041
- }
1042
1080
  return {
1043
1081
  revision: nextRevision, previousRevision: revision(current.bytes), changedScope: changed.scope,
1044
1082
  ...(changed.agentId ? { agentId: changed.agentId } : {}), persisted: true,
@@ -1062,11 +1100,8 @@ export function rollbackConfig(env, snapshotFile) {
1062
1100
  const journal = {
1063
1101
  version: 1, targetAgentId: snapshot.targetAgentId, expectedAfterRevision: snapshot.afterRevision,
1064
1102
  beforeRevision: snapshot.beforeRevision, beforeConfigBytes: snapshot.beforeConfigBytes, beforeApplyState: snapshot.beforeApplyState,
1065
- ...(snapshot.migration ? { migration: snapshot.migration } : {}),
1066
1103
  };
1067
1104
  atomicWriteConfig(rollbackJournalFile(layout.root), journal, PROFILE_MIGRATION_ROLLBACK_LIMIT_BYTES);
1068
- if (snapshot.migration)
1069
- rollbackPiProfileMigration(snapshot.migration);
1070
1105
  const bytes = atomicWriteBytes(layout.configFile, beforeBytes);
1071
1106
  restoreTargetApplyState(layout.root, snapshot.targetAgentId, snapshot.beforeRevision, snapshot.beforeApplyState);
1072
1107
  fs.unlinkSync(rollbackJournalFile(layout.root));
@@ -1088,7 +1123,7 @@ export function commitSetupConfig(env, expectedRevision, nextStored) {
1088
1123
  export function safeConfigView(config, onlyAgentId, chatId, applyState) {
1089
1124
  const entries = onlyAgentId ? [[onlyAgentId, config.agents[onlyAgentId]]] : Object.entries(config.agents);
1090
1125
  const agents = entries.filter((entry) => Boolean(entry[1])).map(([agentId, agent]) => ({
1091
- 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,
1092
1127
  mention: {
1093
1128
  override: agent.mentionPolicy ?? "inherit",
1094
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 {};