larkin 0.2.70

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 (144) hide show
  1. package/CONTRIBUTING.md +48 -0
  2. package/LICENSE +201 -0
  3. package/README.md +122 -0
  4. package/SECURITY.md +14 -0
  5. package/artifacts/release/THIRD_PARTY_NOTICES.txt +5488 -0
  6. package/dist/agent/agent-cli-capabilities.cjs +43 -0
  7. package/dist/agent/agent-cli-capabilities.mjs +35 -0
  8. package/dist/agent/agent-state-store.cjs +961 -0
  9. package/dist/agent/agent-state-store.mjs +923 -0
  10. package/dist/agent/agent-transport.cjs +201 -0
  11. package/dist/agent/agent-transport.mjs +194 -0
  12. package/dist/agent/config-cli-contract.cjs +19 -0
  13. package/dist/agent/config-cli-contract.mjs +16 -0
  14. package/dist/agent/context-prompt.cjs +159 -0
  15. package/dist/agent/context-prompt.mjs +155 -0
  16. package/dist/agent/freshness-gate.mjs +24 -0
  17. package/dist/agent/history-projection.cjs +72 -0
  18. package/dist/agent/history-projection.mjs +65 -0
  19. package/dist/agent/host-reminder-orchestrator.cjs +289 -0
  20. package/dist/agent/host-reminder-orchestrator.mjs +252 -0
  21. package/dist/agent/inbox-projection.cjs +92 -0
  22. package/dist/agent/inbox-projection.mjs +85 -0
  23. package/dist/agent/interaction-state-machine.cjs +710 -0
  24. package/dist/agent/interaction-state-machine.mjs +705 -0
  25. package/dist/agent/reminder-routes.cjs +192 -0
  26. package/dist/agent/reminder-routes.mjs +156 -0
  27. package/dist/agent/reminder-store.cjs +220 -0
  28. package/dist/agent/reminder-store.mjs +205 -0
  29. package/dist/agent/transport-business-context.cjs +486 -0
  30. package/dist/agent/transport-business-context.mjs +446 -0
  31. package/dist/agent/transport-shell.cjs +45 -0
  32. package/dist/agent/transport-shell.mjs +39 -0
  33. package/dist/app/agent-cli.mjs +596 -0
  34. package/dist/app/agent-config.mjs +633 -0
  35. package/dist/app/agent-lark-cli-workspace.cjs +95 -0
  36. package/dist/app/agent-lark-cli-workspace.mjs +85 -0
  37. package/dist/app/agent-readiness.cjs +42 -0
  38. package/dist/app/agent-readiness.mjs +38 -0
  39. package/dist/app/binary-entry.mjs +86 -0
  40. package/dist/app/cli.mjs +176 -0
  41. package/dist/app/dashboard.mjs +205 -0
  42. package/dist/app/internal-command.cjs +57 -0
  43. package/dist/app/internal-command.mjs +48 -0
  44. package/dist/app/lark-channel-secret.mjs +22 -0
  45. package/dist/app/lark-cli.mjs +887 -0
  46. package/dist/app/lark.mjs +45 -0
  47. package/dist/app/local-control.mjs +728 -0
  48. package/dist/app/official-lark-cli.cjs +138 -0
  49. package/dist/app/official-lark-cli.mjs +128 -0
  50. package/dist/app/pi-auth-cli.mjs +67 -0
  51. package/dist/app/run.mjs +364 -0
  52. package/dist/app/runtime-agent-config.mjs +416 -0
  53. package/dist/app/runtime-model-directory.mjs +78 -0
  54. package/dist/app/runtime-process.mjs +113 -0
  55. package/dist/app/session-cli.mjs +83 -0
  56. package/dist/app/setup-dashboard.mjs +26 -0
  57. package/dist/app/setup.mjs +166 -0
  58. package/dist/app/telemetry.mjs +83 -0
  59. package/dist/dashboard/dashboard-assets.mjs +21 -0
  60. package/dist/dashboard/dashboard-avatar.mjs +128 -0
  61. package/dist/dashboard/dashboard-config-controller.mjs +525 -0
  62. package/dist/dashboard/dashboard-lifecycle.cjs +97 -0
  63. package/dist/dashboard/dashboard-lifecycle.mjs +88 -0
  64. package/dist/dashboard/dashboard-template.mjs +29 -0
  65. package/dist/dashboard/dashboard-view-model.mjs +556 -0
  66. package/dist/dashboard/dashboard-workspace.mjs +104 -0
  67. package/dist/dashboard/web/assets/dashboard.css +3 -0
  68. package/dist/dashboard/web/assets/dashboard.js +49 -0
  69. package/dist/feishu/channel-transport.cjs +151 -0
  70. package/dist/feishu/channel-transport.mjs +148 -0
  71. package/dist/feishu/document-comment.cjs +120 -0
  72. package/dist/feishu/document-comment.mjs +109 -0
  73. package/dist/feishu/host-business-state.cjs +477 -0
  74. package/dist/feishu/host-business-state.mjs +462 -0
  75. package/dist/feishu/host-channel-business.cjs +161 -0
  76. package/dist/feishu/host-channel-business.mjs +157 -0
  77. package/dist/feishu/host-processing-eye.cjs +185 -0
  78. package/dist/feishu/host-processing-eye.mjs +181 -0
  79. package/dist/feishu/host-shell.cjs +1425 -0
  80. package/dist/feishu/host-shell.mjs +1385 -0
  81. package/dist/feishu/im-freshness-adapter.mjs +89 -0
  82. package/dist/feishu/interaction-orchestrator.cjs +279 -0
  83. package/dist/feishu/interaction-orchestrator.mjs +242 -0
  84. package/dist/feishu/lark-passthrough.mjs +70 -0
  85. package/dist/feishu/message-policy.cjs +111 -0
  86. package/dist/feishu/message-policy.mjs +98 -0
  87. package/dist/feishu/outbound-transport.cjs +221 -0
  88. package/dist/feishu/outbound-transport.mjs +215 -0
  89. package/dist/platform/build-info.cjs +25 -0
  90. package/dist/platform/build-info.mjs +18 -0
  91. package/dist/platform/callback-capability.cjs +301 -0
  92. package/dist/platform/callback-capability.mjs +252 -0
  93. package/dist/platform/check-bun-version.cjs +12 -0
  94. package/dist/platform/config.cjs +751 -0
  95. package/dist/platform/config.mjs +694 -0
  96. package/dist/platform/process-inspect.cjs +89 -0
  97. package/dist/platform/process-state.cjs +237 -0
  98. package/dist/platform/process-state.mjs +223 -0
  99. package/dist/platform/release-artifacts.mjs +61 -0
  100. package/dist/platform/root-layout.cjs +111 -0
  101. package/dist/platform/root-layout.mjs +72 -0
  102. package/dist/platform/telemetry-config.cjs +64 -0
  103. package/dist/platform/telemetry-config.mjs +56 -0
  104. package/dist/platform/telemetry-spool.cjs +661 -0
  105. package/dist/platform/telemetry-spool.mjs +653 -0
  106. package/dist/platform/telemetry-tracing.cjs +602 -0
  107. package/dist/platform/telemetry-tracing.mjs +596 -0
  108. package/dist/platform/telemetry-uploader.cjs +150 -0
  109. package/dist/platform/telemetry-uploader.mjs +146 -0
  110. package/dist/platform/workspace-service.cjs +469 -0
  111. package/dist/platform/workspace-service.mjs +430 -0
  112. package/dist/runtime/builtin-pi-assets.mjs +33 -0
  113. package/dist/runtime/claude-model-catalog.cjs +102 -0
  114. package/dist/runtime/claude-model-catalog.mjs +99 -0
  115. package/dist/runtime/codex-model-catalog.cjs +108 -0
  116. package/dist/runtime/codex-model-catalog.mjs +105 -0
  117. package/dist/runtime/pi-bash-timeout-extension.mjs +60 -0
  118. package/dist/runtime/pi-bash-timeout-injection.mjs +60 -0
  119. package/dist/runtime/pi-bash-timeout.bundle.js +43 -0
  120. package/dist/runtime/pi-model-catalog.cjs +116 -0
  121. package/dist/runtime/pi-model-catalog.mjs +75 -0
  122. package/dist/runtime/pi-official-auth.mjs +480 -0
  123. package/dist/runtime/pi-provider-config.cjs +252 -0
  124. package/dist/runtime/pi-provider-config.mjs +239 -0
  125. package/dist/runtime/pi-rpc-client.cjs +255 -0
  126. package/dist/runtime/pi-rpc-client.mjs +251 -0
  127. package/dist/runtime/pi-subagent-injection.mjs +129 -0
  128. package/dist/runtime/pi-subagents.bundle.js +8990 -0
  129. package/dist/runtime/runtime-adapters.mjs +1019 -0
  130. package/dist/runtime/runtime-contracts.cjs +2 -0
  131. package/dist/runtime/runtime-contracts.mjs +1 -0
  132. package/dist/runtime/runtime-host.cjs +930 -0
  133. package/dist/runtime/runtime-host.mjs +922 -0
  134. package/dist/runtime/runtime-model-catalog.cjs +43 -0
  135. package/dist/runtime/runtime-model-catalog.mjs +40 -0
  136. package/dist/runtime/runtime-readiness.cjs +177 -0
  137. package/dist/runtime/runtime-readiness.mjs +133 -0
  138. package/dist/setup/bot-register.mjs +655 -0
  139. package/dist/setup/grant-scopes.mjs +133 -0
  140. package/dist/setup/run-credential-preflight.mjs +89 -0
  141. package/dist/setup/setup-agent-choice.mjs +172 -0
  142. package/dist/setup/setup-bind.mjs +337 -0
  143. package/dist/setup/setup-binding.mjs +52 -0
  144. package/package.json +112 -0
@@ -0,0 +1,694 @@
1
+ import * as crypto from "node:crypto";
2
+ import * as fs from "node:fs";
3
+ import * as os from "node:os";
4
+ import * as path from "node:path";
5
+ import { TargetRootLayout, resolveConfigDir as resolveRootConfigDir } from "./root-layout.mjs";
6
+ import { CURRENT_RUNTIME_MODELS } from "../runtime/runtime-model-catalog.mjs";
7
+ import processInspect from "./process-inspect.cjs";
8
+ const TOP_FIELDS_V3 = new Set(["version", "serverId", "activeAgent", "agents"]);
9
+ const TOP_FIELDS_V4 = new Set(["version", "serverId", "mentionPolicy", "activeAgent", "agents"]);
10
+ const AGENT_FIELDS_V3 = new Set(["runtime", "model", "effort", "noMentionChats", "createdAt"]);
11
+ const AGENT_FIELDS_V4 = new Set(["runtime", "model", "piDistribution", "effort", "mentionPolicy", "chatMentionPolicies", "createdAt"]);
12
+ const APP_ID = /^cli_[A-Za-z0-9]+$/;
13
+ const CHAT_ID = /^oc_[A-Za-z0-9_-]+$/;
14
+ const PI_EFFORTS = new Set(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
15
+ const CODEX_EFFORTS = new Set(["minimal", "low", "medium", "high", "xhigh", "max", "ultra"]);
16
+ const CLAUDE_EFFORTS = new Set(["low", "medium", "high", "xhigh", "max"]);
17
+ const PI_MODEL = /^[A-Za-z0-9._-]+\/[A-Za-z0-9._:@+-]+$/;
18
+ const CODEX_MODEL = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
19
+ const CLAUDE_MODEL = /^[A-Za-z0-9][A-Za-z0-9._\[\]-]{0,127}$/;
20
+ const CONFIG_LIMIT_BYTES = 1024 * 1024;
21
+ const LOCK_RETRIES = 200;
22
+ const MALFORMED_LOCK_GRACE_MS = 5_000;
23
+ let runtimeModels;
24
+ function isPlainObject(value) {
25
+ return value !== null && typeof value === "object" && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype;
26
+ }
27
+ function validateRuntimeModels(catalog) {
28
+ if (!isPlainObject(catalog) || Object.keys(catalog).length === 0)
29
+ throw new Error("runtime 模型目录必须是非空 object");
30
+ for (const [runtime, models] of Object.entries(catalog)) {
31
+ if (!runtime || !Array.isArray(models) || models.length === 0)
32
+ throw new Error(`runtime 模型目录 ${runtime || "<empty>"} 必须是非空数组`);
33
+ for (const [index, model] of models.entries()) {
34
+ if (!isPlainObject(model) || typeof model.id !== "string" || !model.id)
35
+ throw new Error(`runtime 模型目录 ${runtime}[${index}].id 必须是非空字符串`);
36
+ }
37
+ }
38
+ return catalog;
39
+ }
40
+ export function loadRuntimeModels() {
41
+ if (runtimeModels !== undefined)
42
+ return runtimeModels;
43
+ runtimeModels = validateRuntimeModels(structuredClone(CURRENT_RUNTIME_MODELS));
44
+ return runtimeModels;
45
+ }
46
+ export function defaultModelFor(runtime) {
47
+ const models = loadRuntimeModels()[runtime];
48
+ if (!models)
49
+ throw new Error(`runtime 模型目录不存在 runtime: ${runtime}`);
50
+ return models[0].id;
51
+ }
52
+ export function resolveConfigDir(env = process.env) {
53
+ return resolveRootConfigDir(env, os.homedir());
54
+ }
55
+ export function resolveLarkConfigDir(env = process.env, configDir = resolveConfigDir(env)) {
56
+ return env.LARKSUITE_CLI_CONFIG_DIR || path.join(configDir, "lark-cli-config");
57
+ }
58
+ export function resolveRuntimeAuthority(env = process.env) {
59
+ const primary = env.LARKIN_AGENT_ID || null;
60
+ if (primary && !APP_ID.test(primary))
61
+ throw new Error("Runtime Agent 身份标记格式无效");
62
+ return primary;
63
+ }
64
+ function assertAllowedFields(object, allowed, label) {
65
+ for (const key of Object.keys(object))
66
+ if (!allowed.has(key))
67
+ throw new Error(`${label} 含不支持字段 ${key}`);
68
+ }
69
+ function assertPolicy(value, label) {
70
+ if (value !== "require" && value !== "free")
71
+ throw new Error(`${label} 只允许 require/free`);
72
+ }
73
+ function assertModel(runtime, model) {
74
+ if (model === "default")
75
+ return;
76
+ const safe = runtime === "pi" ? PI_MODEL.test(model)
77
+ : runtime === "codex" ? CODEX_MODEL.test(model)
78
+ : runtime === "claude" ? CLAUDE_MODEL.test(model) : false;
79
+ if (!safe)
80
+ throw new Error(`${runtime} model 格式不安全:${model}`);
81
+ }
82
+ function runtimeSupportsEffort(runtime, effort) {
83
+ return runtime === "pi" ? PI_EFFORTS.has(effort)
84
+ : runtime === "codex" ? CODEX_EFFORTS.has(effort)
85
+ : runtime === "claude" ? CLAUDE_EFFORTS.has(effort) : false;
86
+ }
87
+ function validateStoredAgent(key, agent, version) {
88
+ if (!APP_ID.test(key))
89
+ throw new Error(`Agent key 必须是安全的飞书 App ID(cli_ + ASCII 字母数字):${key}`);
90
+ if (!isPlainObject(agent))
91
+ throw new Error(`Agent ${key} 必须是普通 object`);
92
+ assertAllowedFields(agent, version === 3 ? AGENT_FIELDS_V3 : AGENT_FIELDS_V4, `Agent ${key}`);
93
+ if (typeof agent.runtime !== "string" || !agent.runtime)
94
+ throw new Error(`Agent ${key}.runtime 必须是非空字符串`);
95
+ if (!loadRuntimeModels()[agent.runtime])
96
+ throw new Error(`Agent ${key}.runtime 未知:${agent.runtime}`);
97
+ if (typeof agent.model !== "string" || !agent.model)
98
+ throw new Error(`Agent ${key}.model 必须是非空字符串`);
99
+ assertModel(agent.runtime, agent.model);
100
+ if (Object.hasOwn(agent, "piDistribution")) {
101
+ if (agent.runtime !== "pi" || (agent.piDistribution !== "external" && agent.piDistribution !== "builtin")) {
102
+ throw new Error(`Agent ${key}.piDistribution 只允许 Pi runtime 使用 external/builtin`);
103
+ }
104
+ }
105
+ if (Object.hasOwn(agent, "effort") && (typeof agent.effort !== "string" || !agent.effort))
106
+ throw new Error(`Agent ${key}.effort 必须是非空字符串`);
107
+ if (agent.model === "default" && Object.hasOwn(agent, "effort"))
108
+ throw new Error(`Agent ${key}.model=default 时不能保存 effort`);
109
+ if (typeof agent.effort === "string") {
110
+ if (!runtimeSupportsEffort(agent.runtime, agent.effort))
111
+ throw new Error(`Agent ${key}.effort=${agent.effort} 不在 ${agent.runtime} 安全档位中`);
112
+ }
113
+ if (version === 3 && Object.hasOwn(agent, "noMentionChats")) {
114
+ if (!Array.isArray(agent.noMentionChats) || agent.noMentionChats.some((chat) => typeof chat !== "string" || !CHAT_ID.test(chat))) {
115
+ throw new Error(`Agent ${key}.noMentionChats 必须是安全的群 ID 字符串数组`);
116
+ }
117
+ }
118
+ if (version === 4 && Object.hasOwn(agent, "mentionPolicy"))
119
+ assertPolicy(agent.mentionPolicy, `Agent ${key}.mentionPolicy`);
120
+ if (version === 4 && Object.hasOwn(agent, "chatMentionPolicies")) {
121
+ if (!isPlainObject(agent.chatMentionPolicies))
122
+ throw new Error(`Agent ${key}.chatMentionPolicies 必须是普通 object`);
123
+ for (const [chatId, policy] of Object.entries(agent.chatMentionPolicies)) {
124
+ if (!CHAT_ID.test(chatId))
125
+ throw new Error(`Agent ${key}.chatMentionPolicies 群 ID 不安全:${chatId}`);
126
+ assertPolicy(policy, `Agent ${key}.chatMentionPolicies.${chatId}`);
127
+ }
128
+ }
129
+ if (Object.hasOwn(agent, "createdAt") && (typeof agent.createdAt !== "string" || !agent.createdAt || !Number.isFinite(Date.parse(agent.createdAt)))) {
130
+ throw new Error(`Agent ${key}.createdAt 格式无效`);
131
+ }
132
+ }
133
+ function hydratedStoredAgent(key, agent, version) {
134
+ const chatMentionPolicies = {};
135
+ if (version === 3) {
136
+ for (const chatId of agent.noMentionChats || [])
137
+ chatMentionPolicies[chatId] = "free";
138
+ }
139
+ else if (isPlainObject(agent.chatMentionPolicies)) {
140
+ for (const [chatId, policy] of Object.entries(agent.chatMentionPolicies))
141
+ chatMentionPolicies[chatId] = policy;
142
+ }
143
+ return {
144
+ runtime: agent.runtime,
145
+ model: agent.model,
146
+ ...(agent.piDistribution === "external" || agent.piDistribution === "builtin" ? { piDistribution: agent.piDistribution } : {}),
147
+ ...(typeof agent.effort === "string" ? { effort: agent.effort } : {}),
148
+ ...(version === 4 && (agent.mentionPolicy === "require" || agent.mentionPolicy === "free") ? { mentionPolicy: agent.mentionPolicy } : {}),
149
+ ...(Object.keys(chatMentionPolicies).length ? { chatMentionPolicies, noMentionChats: Object.entries(chatMentionPolicies).filter(([, policy]) => policy === "free").map(([chatId]) => chatId) } : {}),
150
+ ...(typeof agent.createdAt === "string" ? { createdAt: agent.createdAt } : {}),
151
+ };
152
+ }
153
+ export function hydrateAgent(key, agent, configDir) {
154
+ const layout = TargetRootLayout.fromConfigDir(configDir);
155
+ return {
156
+ name: key, agentId: key, feishuAppId: key, feishuProfile: key,
157
+ runtime: agent.runtime, model: agent.model,
158
+ ...(agent.piDistribution ? { piDistribution: agent.piDistribution } : {}),
159
+ workspaceDir: layout.workspaceDir(key), stateDir: layout.agentStateDir(key),
160
+ larkConfigDir: path.join(layout.agentStateDir(key), "lark-cli-config"),
161
+ ...(agent.effort ? { effort: agent.effort } : {}),
162
+ ...(agent.mentionPolicy ? { mentionPolicy: agent.mentionPolicy } : {}),
163
+ ...(agent.chatMentionPolicies ? { chatMentionPolicies: { ...agent.chatMentionPolicies } } : {}),
164
+ ...(agent.noMentionChats ? { noMentionChats: [...agent.noMentionChats] } : {}),
165
+ ...(agent.createdAt ? { createdAt: agent.createdAt } : {}),
166
+ };
167
+ }
168
+ export function normalizeConfig(raw, configDir, { mint } = {}) {
169
+ const layout = TargetRootLayout.fromConfigDir(configDir);
170
+ if (raw === null)
171
+ return {
172
+ version: 4, serverId: typeof mint === "function" ? mint() : null, mentionPolicy: "require",
173
+ configDir: layout.configDir, larkinHome: layout.larkinHome, larkConfigDir: resolveLarkConfigDir({}, layout.root), activeAgent: null, agents: {},
174
+ };
175
+ if (!isPlainObject(raw))
176
+ throw new Error("config 必须是普通 object");
177
+ const version = raw.version;
178
+ if (version !== 3 && version !== 4)
179
+ throw new Error("不支持该配置格式:larkin 只接受 version=3/4");
180
+ const topFields = version === 3 ? TOP_FIELDS_V3 : TOP_FIELDS_V4;
181
+ assertAllowedFields(raw, topFields, "config");
182
+ for (const field of topFields)
183
+ if (!Object.hasOwn(raw, field))
184
+ throw new Error(`config 缺少必需字段 ${field}`);
185
+ if (typeof raw.serverId !== "string" || !raw.serverId)
186
+ throw new Error("config.serverId 必须是非空字符串");
187
+ if (version === 4)
188
+ assertPolicy(raw.mentionPolicy, "config.mentionPolicy");
189
+ if (raw.activeAgent !== null && typeof raw.activeAgent !== "string")
190
+ throw new Error("config.activeAgent 必须是字符串或 null");
191
+ if (!isPlainObject(raw.agents))
192
+ throw new Error("config.agents 必须是普通 object");
193
+ for (const [key, agent] of Object.entries(raw.agents))
194
+ validateStoredAgent(key, agent, version);
195
+ if (raw.activeAgent !== null && !Object.hasOwn(raw.agents, raw.activeAgent))
196
+ throw new Error(`config.activeAgent 指向不存在的 Agent:${raw.activeAgent}`);
197
+ const agents = {};
198
+ for (const [key, agent] of Object.entries(raw.agents))
199
+ agents[key] = hydrateAgent(key, hydratedStoredAgent(key, agent, version), layout.root);
200
+ return {
201
+ version: 4, serverId: raw.serverId, mentionPolicy: version === 4 ? raw.mentionPolicy : "require",
202
+ configDir: layout.configDir, larkinHome: layout.larkinHome, larkConfigDir: resolveLarkConfigDir({}, layout.root), activeAgent: raw.activeAgent, agents,
203
+ };
204
+ }
205
+ export function assertPrivateConfigMetadata(metadata, label = "配置文件") {
206
+ if (!metadata.regularFile)
207
+ throw new Error(`${label} 必须是普通文件`);
208
+ if (typeof process.getuid === "function" && metadata.uid !== process.getuid())
209
+ throw new Error(`${label} owner 不是当前用户`);
210
+ if ((metadata.mode & 0o777) !== 0o600)
211
+ throw new Error(`${label} 权限必须是 0600`);
212
+ }
213
+ function readPrivateFile(file, root, limit, label) {
214
+ let fd = null;
215
+ try {
216
+ const rootReal = fs.realpathSync(root);
217
+ const parentReal = fs.realpathSync(path.dirname(file));
218
+ if (parentReal !== rootReal)
219
+ throw new Error(`${label} 必须位于 canonical config root 内`);
220
+ fd = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0));
221
+ const stat = fs.fstatSync(fd);
222
+ assertPrivateConfigMetadata({ regularFile: stat.isFile(), uid: stat.uid, mode: stat.mode }, label);
223
+ if (stat.size > limit)
224
+ throw new Error(`${label} 超过 ${limit} bytes`);
225
+ return fs.readFileSync(fd);
226
+ }
227
+ catch (error) {
228
+ if (error.code === "ENOENT")
229
+ return null;
230
+ if (error.code === "ELOOP")
231
+ throw new Error(`${label} 不允许 symlink`);
232
+ throw error;
233
+ }
234
+ finally {
235
+ if (fd !== null)
236
+ fs.closeSync(fd);
237
+ }
238
+ }
239
+ function readConfigFile(file, root = path.dirname(file)) {
240
+ try {
241
+ const bytes = readPrivateFile(file, root, CONFIG_LIMIT_BYTES, "配置文件");
242
+ if (bytes === null)
243
+ return { raw: null, bytes: Buffer.alloc(0) };
244
+ return { raw: JSON.parse(bytes.toString("utf8")), bytes };
245
+ }
246
+ catch (error) {
247
+ if (error instanceof SyntaxError)
248
+ throw new Error(`配置文件无法解析 ${file}: ${error.message}`);
249
+ throw error;
250
+ }
251
+ }
252
+ export function loadConfig(env = process.env, opts = {}) {
253
+ const layout = TargetRootLayout.fromConfigDir(resolveConfigDir(env));
254
+ const { raw, bytes } = readConfigFile(layout.configFile, layout.root);
255
+ return { configDir: layout.configDir, file: layout.configFile, revision: revision(bytes), config: normalizeConfig(raw, layout.root, opts) };
256
+ }
257
+ export function selectAgent(config, env = process.env) {
258
+ const agents = config.agents || {};
259
+ const runtimeAgentId = resolveRuntimeAuthority(env);
260
+ if (runtimeAgentId) {
261
+ if (!agents[runtimeAgentId])
262
+ throw new Error(`Agent 不存在或未配置:${runtimeAgentId}`);
263
+ return agents[runtimeAgentId];
264
+ }
265
+ if (!config.activeAgent)
266
+ throw new Error("未配置 activeAgent,无法选择 Agent");
267
+ if (!agents[config.activeAgent])
268
+ throw new Error(`activeAgent 指向不存在的 Agent:${config.activeAgent}`);
269
+ return agents[config.activeAgent];
270
+ }
271
+ export function toStored(config) {
272
+ const out = { version: 4, serverId: config.serverId, mentionPolicy: config.mentionPolicy, activeAgent: config.activeAgent, agents: {} };
273
+ for (const [key, agent] of Object.entries(config.agents || {})) {
274
+ const stored = { runtime: agent.runtime, model: agent.model };
275
+ if (agent.piDistribution)
276
+ stored.piDistribution = agent.piDistribution;
277
+ if (typeof agent.effort === "string" && agent.effort)
278
+ stored.effort = agent.effort;
279
+ if (agent.mentionPolicy)
280
+ stored.mentionPolicy = agent.mentionPolicy;
281
+ if (agent.chatMentionPolicies && Object.keys(agent.chatMentionPolicies).length)
282
+ stored.chatMentionPolicies = { ...agent.chatMentionPolicies };
283
+ if (typeof agent.createdAt === "string" && agent.createdAt)
284
+ stored.createdAt = agent.createdAt;
285
+ out.agents[key] = stored;
286
+ }
287
+ return out;
288
+ }
289
+ export function resolveMentionPolicy(config, agentId, chatId) {
290
+ const agent = config.agents[agentId];
291
+ if (!agent)
292
+ throw new Error(`Agent 不存在:${agentId}`);
293
+ const chat = agent.chatMentionPolicies?.[chatId];
294
+ if (chat)
295
+ return { effective: chat, source: "chat" };
296
+ if (agent.mentionPolicy)
297
+ return { effective: agent.mentionPolicy, source: "agent" };
298
+ return { effective: config.mentionPolicy, source: "global" };
299
+ }
300
+ /** Resolve non-chat event policy. Deliberately has no chat-id input and never reads per-chat overrides. */
301
+ export function resolveAgentGlobalMentionPolicy(config, agentId) {
302
+ const agent = config.agents[agentId];
303
+ if (!agent)
304
+ throw new Error(`Agent 不存在:${agentId}`);
305
+ if (agent.mentionPolicy)
306
+ return { effective: agent.mentionPolicy, source: "agent" };
307
+ return { effective: config.mentionPolicy, source: "global" };
308
+ }
309
+ function revision(bytes) {
310
+ return `sha256:${crypto.createHash("sha256").update(bytes).digest("hex")}`;
311
+ }
312
+ function agentSignature(config, agentId) {
313
+ const agent = config.agents[agentId];
314
+ if (!agent)
315
+ throw new Error(`Agent 不存在:${agentId}`);
316
+ const chats = Object.fromEntries(Object.entries(agent.chatMentionPolicies || {}).sort(([left], [right]) => left.localeCompare(right)));
317
+ return revision(JSON.stringify({
318
+ runtime: agent.runtime, model: agent.model, piDistribution: agent.piDistribution ?? null, effort: agent.effort ?? null,
319
+ globalMentionPolicy: config.mentionPolicy, agentMentionPolicy: agent.mentionPolicy ?? null, chatMentionPolicies: chats,
320
+ }));
321
+ }
322
+ export function runtimeConfigSignature(config, agentId) {
323
+ return agentSignature(config, agentId);
324
+ }
325
+ function applyStateFile(configDir) { return path.join(configDir, "config-apply-state.json"); }
326
+ function readApplyFile(configDir) {
327
+ const file = applyStateFile(configDir);
328
+ try {
329
+ const bytes = readPrivateFile(file, configDir, 256 * 1024, "config apply state");
330
+ if (bytes === null)
331
+ return { version: 1, persistedRevision: "unknown", agents: {} };
332
+ const value = JSON.parse(bytes.toString("utf8"));
333
+ if (value.version !== 1 || typeof value.persistedRevision !== "string" || !isPlainObject(value.agents))
334
+ throw new Error("config apply state 无效");
335
+ const agents = {};
336
+ for (const [agentId, record] of Object.entries(value.agents)) {
337
+ if (!APP_ID.test(agentId) || !isPlainObject(record) || typeof record.persistedSignature !== "string"
338
+ || (record.appliedSignature !== null && typeof record.appliedSignature !== "string"))
339
+ throw new Error("config apply state Agent 记录无效");
340
+ agents[agentId] = { persistedSignature: record.persistedSignature, appliedSignature: record.appliedSignature };
341
+ }
342
+ return { version: 1, persistedRevision: value.persistedRevision, agents };
343
+ }
344
+ catch (error) {
345
+ throw error;
346
+ }
347
+ }
348
+ function writeApplyFile(configDir, value) {
349
+ atomicWriteConfig(applyStateFile(configDir), value);
350
+ }
351
+ export function configApplyState(env, config) {
352
+ let state;
353
+ try {
354
+ state = readApplyFile(config.configDir);
355
+ }
356
+ catch {
357
+ state = { version: 1, persistedRevision: "unknown", agents: {} };
358
+ }
359
+ return {
360
+ persistedRevision: state.persistedRevision,
361
+ agents: Object.fromEntries(Object.entries(config.agents).map(([agentId]) => {
362
+ const record = state.agents[agentId];
363
+ const current = agentSignature(config, agentId);
364
+ const applyState = !record ? "unknown" : record.persistedSignature === current && record.appliedSignature === current ? "applied" : "pending";
365
+ return [agentId, { applyState, persistedSignature: current, appliedSignature: record?.appliedSignature ?? null }];
366
+ })),
367
+ };
368
+ }
369
+ export function markConfigApplied(env, agentId, expectedSignature) {
370
+ const layout = TargetRootLayout.fromConfigDir(resolveConfigDir(env));
371
+ withConfigLock(layout, () => {
372
+ const current = readConfigFile(layout.configFile, layout.root);
373
+ if (current.raw === null)
374
+ throw new Error(`没找到配置 ${layout.configFile}`);
375
+ const config = normalizeConfig(current.raw, layout.root);
376
+ const signature = runtimeConfigSignature(config, agentId);
377
+ if (signature !== expectedSignature)
378
+ throw new Error("配置在 apply 期间发生变化;未把新配置误标为已应用");
379
+ const state = readApplyFile(layout.root);
380
+ state.persistedRevision = revision(current.bytes);
381
+ state.agents[agentId] = { persistedSignature: signature, appliedSignature: signature };
382
+ writeApplyFile(layout.root, state);
383
+ });
384
+ }
385
+ function assertAuthority(authority, mutation) {
386
+ if (authority.kind !== "user" && authority.kind !== "agent")
387
+ throw new Error("配置 authority 无效");
388
+ if (authority.kind === "agent" && !APP_ID.test(authority.agentId))
389
+ throw new Error("Runtime Agent authority 格式无效");
390
+ void mutation;
391
+ }
392
+ function applyMutation(config, mutation) {
393
+ if (mutation.kind === "set-global-mention") {
394
+ assertPolicy(mutation.value, "全局 mention policy");
395
+ config.mentionPolicy = mutation.value;
396
+ return { scope: "global", runtimeChange: false, affectedAgentIds: Object.keys(config.agents) };
397
+ }
398
+ if (!APP_ID.test(mutation.agentId) || !config.agents[mutation.agentId])
399
+ throw new Error(`Agent 不存在:${mutation.agentId}`);
400
+ const agent = config.agents[mutation.agentId];
401
+ if (mutation.kind === "set-agent-mention") {
402
+ if (mutation.value === "inherit")
403
+ delete agent.mentionPolicy;
404
+ else {
405
+ assertPolicy(mutation.value, "Agent mention policy");
406
+ agent.mentionPolicy = mutation.value;
407
+ }
408
+ return { scope: "agent", agentId: mutation.agentId, runtimeChange: false, affectedAgentIds: [mutation.agentId] };
409
+ }
410
+ if (mutation.kind === "set-chat-mention") {
411
+ if (!CHAT_ID.test(mutation.chatId))
412
+ throw new Error(`群 ID 格式无效:${mutation.chatId}`);
413
+ const policies = { ...(agent.chatMentionPolicies || {}) };
414
+ if (mutation.value === "inherit")
415
+ delete policies[mutation.chatId];
416
+ else {
417
+ assertPolicy(mutation.value, "群 mention policy");
418
+ policies[mutation.chatId] = mutation.value;
419
+ }
420
+ if (Object.keys(policies).length)
421
+ agent.chatMentionPolicies = policies;
422
+ else
423
+ delete agent.chatMentionPolicies;
424
+ agent.noMentionChats = Object.entries(policies).filter(([, value]) => value === "free").map(([chatId]) => chatId);
425
+ return { scope: "chat", agentId: mutation.agentId, runtimeChange: false, affectedAgentIds: [mutation.agentId] };
426
+ }
427
+ if (mutation.kind === "set-agent-runtime") {
428
+ if (!loadRuntimeModels()[mutation.runtime])
429
+ throw new Error(`未知 runtime:${mutation.runtime}`);
430
+ assertModel(mutation.runtime, mutation.model || "default");
431
+ agent.runtime = mutation.runtime;
432
+ agent.model = mutation.model || "default";
433
+ delete agent.effort;
434
+ }
435
+ else if (mutation.kind === "set-agent-model") {
436
+ if (!mutation.model.trim())
437
+ throw new Error("model 不能为空");
438
+ assertModel(agent.runtime, mutation.model);
439
+ const changedModel = mutation.model !== agent.model;
440
+ agent.model = mutation.model;
441
+ if (changedModel || mutation.model === "default")
442
+ delete agent.effort;
443
+ }
444
+ else {
445
+ if (mutation.effort === null)
446
+ delete agent.effort;
447
+ else {
448
+ if (agent.model === "default")
449
+ throw new Error("model=default 时不能设置 effort");
450
+ if (!runtimeSupportsEffort(agent.runtime, mutation.effort))
451
+ throw new Error(`effort=${mutation.effort} 不在 ${agent.runtime} 安全档位中`);
452
+ agent.effort = mutation.effort;
453
+ }
454
+ }
455
+ return { scope: "agent", agentId: mutation.agentId, runtimeChange: true, affectedAgentIds: [mutation.agentId] };
456
+ }
457
+ function sleep(milliseconds) {
458
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, milliseconds);
459
+ }
460
+ function daemonHasConfiguredAgent(root, agentId) {
461
+ try {
462
+ const record = JSON.parse(fs.readFileSync(path.join(root, "daemon-status.json"), "utf8"));
463
+ const inspected = processInspect.inspectProcess(record.pid);
464
+ return inspected.ok === true && typeof inspected.command === "string" && inspected.command.includes(record.commandToken || "app/runtime-process.mjs")
465
+ && inspected.startToken === record.processStartToken && Array.isArray(record.agents) && record.agents.includes(agentId);
466
+ }
467
+ catch {
468
+ return false;
469
+ }
470
+ }
471
+ function currentLockRecord() {
472
+ const inspected = processInspect.inspectProcess(process.pid);
473
+ if (!inspected.ok || !inspected.startToken)
474
+ throw new Error("无法取得 config lock 进程身份");
475
+ return { pid: process.pid, processStartToken: inspected.startToken, nonce: crypto.randomUUID(), createdAt: new Date().toISOString() };
476
+ }
477
+ function readLockRecord(file) {
478
+ try {
479
+ const stat = fs.lstatSync(file);
480
+ const bytes = fs.readFileSync(file);
481
+ let record = null;
482
+ try {
483
+ const parsed = JSON.parse(bytes.toString("utf8"));
484
+ if (Number.isInteger(parsed.pid) && typeof parsed.processStartToken === "string" && typeof parsed.nonce === "string" && typeof parsed.createdAt === "string") {
485
+ record = parsed;
486
+ }
487
+ }
488
+ catch { /* malformed lock is reclaimed only after the grace window */ }
489
+ return { record, ageMs: Date.now() - stat.mtimeMs, identity: `${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeMs}:${record?.nonce || "malformed"}` };
490
+ }
491
+ catch (error) {
492
+ if (error.code === "ENOENT")
493
+ return null;
494
+ throw error;
495
+ }
496
+ }
497
+ function lockIsStale(snapshot) {
498
+ if (!snapshot.record)
499
+ return snapshot.ageMs >= MALFORMED_LOCK_GRACE_MS;
500
+ const inspected = processInspect.inspectProcess(snapshot.record.pid);
501
+ return inspected.dead === true || (inspected.ok === true && inspected.startToken !== snapshot.record.processStartToken);
502
+ }
503
+ function reclaimStaleLock(file, snapshot, owner) {
504
+ if (!lockIsStale(snapshot))
505
+ return false;
506
+ const guard = `${file}.reclaim`;
507
+ let guardFd = null;
508
+ for (let attempt = 0; attempt < 2 && guardFd === null; attempt += 1) {
509
+ try {
510
+ guardFd = fs.openSync(guard, "wx", 0o600);
511
+ }
512
+ catch (error) {
513
+ if (error.code !== "EEXIST")
514
+ throw error;
515
+ const orphan = readLockRecord(guard);
516
+ if (!orphan || !lockIsStale(orphan))
517
+ return false;
518
+ const latest = readLockRecord(guard);
519
+ if (!latest || latest.identity !== orphan.identity || !lockIsStale(latest))
520
+ return false;
521
+ try {
522
+ fs.unlinkSync(guard);
523
+ }
524
+ catch (unlinkError) {
525
+ if (unlinkError.code !== "ENOENT")
526
+ throw unlinkError;
527
+ }
528
+ }
529
+ }
530
+ if (guardFd === null)
531
+ return false;
532
+ try {
533
+ fs.writeFileSync(guardFd, `${JSON.stringify(owner)}\n`);
534
+ fs.fsyncSync(guardFd);
535
+ const latest = readLockRecord(file);
536
+ if (latest && latest.identity === snapshot.identity && lockIsStale(latest))
537
+ fs.unlinkSync(file);
538
+ return true;
539
+ }
540
+ finally {
541
+ fs.closeSync(guardFd);
542
+ const current = readLockRecord(guard);
543
+ if (current?.record?.nonce === owner.nonce && current.record.pid === owner.pid) {
544
+ try {
545
+ fs.unlinkSync(guard);
546
+ }
547
+ catch { /* best effort owner-only release */ }
548
+ }
549
+ }
550
+ }
551
+ function withConfigLock(layout, action) {
552
+ fs.mkdirSync(layout.root, { recursive: true, mode: 0o700 });
553
+ const lock = `${layout.configFile}.lock`;
554
+ const owner = currentLockRecord();
555
+ let acquired = false;
556
+ for (let attempt = 0; attempt < LOCK_RETRIES; attempt += 1) {
557
+ try {
558
+ const fd = fs.openSync(lock, "wx", 0o600);
559
+ try {
560
+ fs.writeFileSync(fd, `${JSON.stringify(owner)}\n`);
561
+ fs.fsyncSync(fd);
562
+ }
563
+ finally {
564
+ fs.closeSync(fd);
565
+ }
566
+ acquired = true;
567
+ break;
568
+ }
569
+ catch (error) {
570
+ if (error.code !== "EEXIST")
571
+ throw error;
572
+ const snapshot = readLockRecord(lock);
573
+ if (snapshot)
574
+ reclaimStaleLock(lock, snapshot, owner);
575
+ sleep(10);
576
+ }
577
+ }
578
+ if (!acquired)
579
+ throw new Error("配置正被其他进程修改,请稍后重试");
580
+ try {
581
+ return action();
582
+ }
583
+ finally {
584
+ const current = readLockRecord(lock);
585
+ if (current?.record?.nonce === owner.nonce && current.record.pid === owner.pid) {
586
+ try {
587
+ fs.unlinkSync(lock);
588
+ }
589
+ catch { /* best effort */ }
590
+ }
591
+ }
592
+ }
593
+ function atomicWriteConfig(file, value) {
594
+ const bytes = Buffer.from(`${JSON.stringify(value, null, 2)}\n`, "utf8");
595
+ if (bytes.length > CONFIG_LIMIT_BYTES)
596
+ throw new Error(`配置文件超过 ${CONFIG_LIMIT_BYTES} bytes`);
597
+ const temporary = `${file}.${process.pid}.${crypto.randomUUID()}.tmp`;
598
+ const fd = fs.openSync(temporary, "wx", 0o600);
599
+ try {
600
+ fs.writeFileSync(fd, bytes);
601
+ fs.fsyncSync(fd);
602
+ }
603
+ finally {
604
+ fs.closeSync(fd);
605
+ }
606
+ try {
607
+ fs.renameSync(temporary, file);
608
+ fs.chmodSync(file, 0o600);
609
+ const dirFd = fs.openSync(path.dirname(file), "r");
610
+ try {
611
+ fs.fsyncSync(dirFd);
612
+ }
613
+ finally {
614
+ fs.closeSync(dirFd);
615
+ }
616
+ }
617
+ catch (error) {
618
+ try {
619
+ fs.unlinkSync(temporary);
620
+ }
621
+ catch { /* best effort */ }
622
+ throw error;
623
+ }
624
+ return bytes;
625
+ }
626
+ export function mutateConfig(env, mutation, authority) {
627
+ assertAuthority(authority, mutation);
628
+ const layout = TargetRootLayout.fromConfigDir(resolveConfigDir(env));
629
+ return withConfigLock(layout, () => {
630
+ const current = readConfigFile(layout.configFile, layout.root);
631
+ if (current.raw === null)
632
+ throw new Error(`没找到配置 ${layout.configFile},先跑 larkin setup`);
633
+ const config = normalizeConfig(current.raw, layout.root);
634
+ const priorSignatures = Object.fromEntries(Object.keys(config.agents).map((agentId) => [agentId, agentSignature(config, agentId)]));
635
+ const changed = applyMutation(config, mutation);
636
+ const stored = toStored(config);
637
+ normalizeConfig(stored, layout.root);
638
+ const bytes = atomicWriteConfig(layout.configFile, stored);
639
+ const nextRevision = revision(bytes);
640
+ let fullyApplied = changed.affectedAgentIds.length === 0;
641
+ try {
642
+ const applyState = readApplyFile(layout.root);
643
+ applyState.persistedRevision = nextRevision;
644
+ fullyApplied = true;
645
+ for (const agentId of changed.affectedAgentIds) {
646
+ const nextSignature = agentSignature(config, agentId);
647
+ const prior = applyState.agents[agentId];
648
+ const daemonHasAgent = daemonHasConfiguredAgent(layout.root, agentId);
649
+ const observedPrior = prior?.appliedSignature ?? (daemonHasAgent ? priorSignatures[agentId] : null);
650
+ const appliedSignature = !changed.runtimeChange && daemonHasAgent && observedPrior === priorSignatures[agentId]
651
+ ? nextSignature
652
+ : observedPrior;
653
+ applyState.agents[agentId] = { persistedSignature: nextSignature, appliedSignature };
654
+ if (appliedSignature !== nextSignature)
655
+ fullyApplied = false;
656
+ }
657
+ writeApplyFile(layout.root, applyState);
658
+ }
659
+ catch {
660
+ fullyApplied = false; /* Config persistence remains authoritative. */
661
+ }
662
+ return {
663
+ revision: nextRevision, previousRevision: revision(current.bytes), changedScope: changed.scope,
664
+ ...(changed.agentId ? { agentId: changed.agentId } : {}), persisted: true,
665
+ applyState: fullyApplied ? "applied" : "saved_not_applied", config,
666
+ };
667
+ });
668
+ }
669
+ export function commitSetupConfig(env, expectedRevision, nextStored) {
670
+ const layout = TargetRootLayout.fromConfigDir(resolveConfigDir(env));
671
+ return withConfigLock(layout, () => {
672
+ const current = readConfigFile(layout.configFile, layout.root);
673
+ if (revision(current.bytes) !== expectedRevision)
674
+ throw new Error("setup 期间配置已被其他命令修改;未覆盖并发更新,请重试 setup");
675
+ const config = normalizeConfig(nextStored, layout.root);
676
+ const bytes = atomicWriteConfig(layout.configFile, toStored(config));
677
+ return { revision: revision(bytes), config };
678
+ });
679
+ }
680
+ export function safeConfigView(config, onlyAgentId, chatId, applyState) {
681
+ const entries = onlyAgentId ? [[onlyAgentId, config.agents[onlyAgentId]]] : Object.entries(config.agents);
682
+ const agents = entries.filter((entry) => Boolean(entry[1])).map(([agentId, agent]) => ({
683
+ agentId, runtime: agent.runtime, model: agent.model, effort: agent.effort ?? null,
684
+ mention: {
685
+ override: agent.mentionPolicy ?? "inherit",
686
+ effective: agent.mentionPolicy ?? config.mentionPolicy,
687
+ source: agent.mentionPolicy ? "agent" : "global",
688
+ ...(chatId ? { chat: { chatId, override: agent.chatMentionPolicies?.[chatId] ?? "inherit", ...resolveMentionPolicy(config, agentId, chatId) } } : {}),
689
+ },
690
+ chatMentionPolicies: { ...(agent.chatMentionPolicies || {}) },
691
+ apply: applyState?.agents?.[agentId] ?? { applyState: "unknown" },
692
+ }));
693
+ return { version: 4, mentionPolicy: config.mentionPolicy, persistedRevision: applyState?.persistedRevision ?? "unknown", agents };
694
+ }