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,525 @@
1
+ import { execFile } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { configApplyState, loadConfig, loadRuntimeModels, markConfigApplied, mutateConfig, resolveMentionPolicy, runtimeConfigSignature, safeConfigView } from "../platform/config.mjs";
5
+ import { requestAgentUpsert } from "../app/local-control.mjs";
6
+ import { discoverClaudeModelCatalog } from "../runtime/claude-model-catalog.mjs";
7
+ import { discoverCodexModelCatalog } from "../runtime/codex-model-catalog.mjs";
8
+ import { discoverPiModelCatalog } from "../runtime/pi-model-catalog.mjs";
9
+ import { managedOfficialLarkCli } from "../app/agent-lark-cli-workspace.mjs";
10
+ function pruneCache(cache, expired, maxEntries) {
11
+ for (const [key, value] of cache)
12
+ if (expired(value))
13
+ cache.delete(key);
14
+ while (cache.size >= maxEntries) {
15
+ const oldest = cache.keys().next().value;
16
+ if (!oldest)
17
+ break;
18
+ cache.delete(oldest);
19
+ }
20
+ }
21
+ function knownChatAliases(map, configuredChatIds) {
22
+ const aliases = new Map();
23
+ for (const [alias, value] of Object.entries(map)) {
24
+ if (typeof value !== "string" || !/^oc_[A-Za-z0-9_-]+$/.test(value))
25
+ continue;
26
+ const current = aliases.get(value) || [];
27
+ current.push(alias);
28
+ aliases.set(value, current);
29
+ }
30
+ for (const chatId of configuredChatIds)
31
+ if (!aliases.has(chatId))
32
+ aliases.set(chatId, []);
33
+ return Object.fromEntries([...aliases.entries()].map(([chatId, values]) => {
34
+ const groupAliases = values.filter((value) => value.startsWith("#") && !value.includes(":"));
35
+ const readable = groupAliases
36
+ .map((value) => value.slice(1).trim())
37
+ .filter((value) => value.length > 0 && !/^[a-f0-9]{11}$/i.test(value))
38
+ .sort((left, right) => left.length - right.length || left.localeCompare(right, "zh-CN"))[0] || null;
39
+ const directOnly = values.some((value) => value.startsWith("dm:")) && groupAliases.length === 0;
40
+ return [chatId, { chatId, displayName: readable, kind: directOnly ? "direct" : "group" }];
41
+ }));
42
+ }
43
+ function cleanChatName(value) {
44
+ if (typeof value !== "string")
45
+ return null;
46
+ const cleaned = value
47
+ .replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]+/g, "")
48
+ .replace(/[\t\n\r]+/g, " ")
49
+ .replace(/\s+/g, " ")
50
+ .trim()
51
+ .slice(0, 120);
52
+ return cleaned || null;
53
+ }
54
+ function chatList(value) {
55
+ const rows = value?.data?.chats;
56
+ if (!Array.isArray(rows))
57
+ return [];
58
+ return rows.flatMap((item) => {
59
+ const row = item;
60
+ const name = cleanChatName(row.name);
61
+ return typeof row.chat_id === "string" && name ? [{ chatId: row.chat_id, name }] : [];
62
+ });
63
+ }
64
+ function chatName(value) {
65
+ const data = value?.data;
66
+ return cleanChatName(data?.name ?? data?.chat?.name);
67
+ }
68
+ function assertLarkSuccess(value) {
69
+ if (value?.ok === false)
70
+ throw new Error("directory unavailable");
71
+ }
72
+ async function runLarkJson(call) {
73
+ return await new Promise((resolve, reject) => {
74
+ execFile(call.command, call.args, { env: call.env, maxBuffer: call.maxBuffer, timeout: call.timeout }, (error, stdout) => {
75
+ if (error)
76
+ return reject(new Error("directory unavailable"));
77
+ try {
78
+ resolve(JSON.parse(stdout));
79
+ }
80
+ catch {
81
+ reject(new Error("directory unavailable"));
82
+ }
83
+ });
84
+ });
85
+ }
86
+ export function createChatDirectoryResolver(options = {}) {
87
+ const now = options.now ?? Date.now;
88
+ const ttlMs = options.ttlMs ?? 5 * 60_000;
89
+ const maxStaleMs = options.maxStaleMs ?? 60_000;
90
+ const execute = options.runLarkJson ?? runLarkJson;
91
+ const resolveManagedCli = options.managedCli ?? managedOfficialLarkCli;
92
+ const cache = new Map();
93
+ const inFlight = new Map();
94
+ return {
95
+ async resolve(input) {
96
+ const chatIds = [...new Set(input.chatIds)].sort();
97
+ if (chatIds.length === 0)
98
+ return {};
99
+ const key = [input.agentId, input.profile, input.configDir, ...chatIds].join("\u0000");
100
+ pruneCache(cache, (entry) => now() > entry.staleUntil, 128);
101
+ const cached = cache.get(key);
102
+ if (cached && now() < cached.expiresAt)
103
+ return cached.value;
104
+ let pending = inFlight.get(key);
105
+ if (!pending) {
106
+ pending = (async () => {
107
+ const managed = resolveManagedCli({ agentId: input.agentId, feishuAppId: input.agentId,
108
+ stateDir: path.dirname(input.configDir), larkConfigDir: input.configDir }, process.env);
109
+ const common = {
110
+ command: managed.command.command,
111
+ env: managed.env,
112
+ maxBuffer: 1024 * 1024,
113
+ timeout: 15_000,
114
+ };
115
+ const names = {};
116
+ const requested = new Set(chatIds);
117
+ let listSucceeded = false;
118
+ try {
119
+ const listed = await execute({ ...common, args: [...managed.command.argsPrefix, "im", "+chat-list", "--as", "bot", "--page-size", "100", "--json"] });
120
+ assertLarkSuccess(listed);
121
+ listSucceeded = true;
122
+ for (const chat of chatList(listed))
123
+ if (requested.has(chat.chatId))
124
+ names[chat.chatId] = chat.name;
125
+ }
126
+ catch { /* fall back to isolated per-chat reads */ }
127
+ let detailSucceeded = false;
128
+ for (const chatId of chatIds) {
129
+ if (names[chatId])
130
+ continue;
131
+ try {
132
+ const detail = await execute({ ...common, args: [...managed.command.argsPrefix, "im", "chats", "get", "--chat-id", chatId, "--as", "bot", "--json"] });
133
+ assertLarkSuccess(detail);
134
+ detailSucceeded = true;
135
+ const name = chatName(detail);
136
+ if (name)
137
+ names[chatId] = name;
138
+ }
139
+ catch { /* one deleted or inaccessible group must not erase other resolved names */ }
140
+ }
141
+ if (!listSucceeded && !detailSucceeded)
142
+ throw new Error("directory unavailable");
143
+ const expiresAt = now() + ttlMs;
144
+ cache.set(key, { expiresAt, staleUntil: expiresAt + maxStaleMs, value: names });
145
+ return names;
146
+ })();
147
+ inFlight.set(key, pending);
148
+ void pending.finally(() => inFlight.delete(key)).catch(() => undefined);
149
+ }
150
+ try {
151
+ return await pending;
152
+ }
153
+ catch {
154
+ if (cached && now() <= cached.staleUntil)
155
+ return cached.value;
156
+ throw new Error("directory unavailable");
157
+ }
158
+ },
159
+ };
160
+ }
161
+ export function createPiModelDirectoryResolver(options = {}) {
162
+ const discover = options.discoverPiModelCatalog ?? discoverPiModelCatalog;
163
+ const now = options.now ?? Date.now;
164
+ const ttlMs = options.ttlMs ?? 5 * 60_000;
165
+ // Dashboard polls status every 3s. A short negative cache avoids repeating expensive local
166
+ // catalog/auth discovery on every poll while still recovering promptly after login/config fixes.
167
+ const negativeTtlMs = options.negativeTtlMs ?? 30_000;
168
+ const cache = new Map();
169
+ const failures = new Map();
170
+ const inFlight = new Map();
171
+ return {
172
+ async resolve(input) {
173
+ const key = [input.agentId, input.cwd, input.agentDir ?? ""].join("\u0000");
174
+ pruneCache(cache, (entry) => now() >= entry.expiresAt, 64);
175
+ pruneCache(failures, (entry) => now() >= entry.expiresAt, 64);
176
+ const cached = cache.get(key);
177
+ if (cached && now() < cached.expiresAt)
178
+ return cached.value;
179
+ if (failures.has(key))
180
+ throw new Error("Pi model catalog unavailable");
181
+ let pending = inFlight.get(key);
182
+ if (!pending) {
183
+ pending = (async () => {
184
+ try {
185
+ const catalog = await discover({ cwd: input.cwd, ...(input.agentDir ? { agentDir: input.agentDir } : {}) });
186
+ const models = [
187
+ { id: "default", label: `default: ${catalog.effectiveModel}` },
188
+ ...catalog.models.map(({ id, label, contextWindow, supportedReasoningEfforts, defaultReasoningEffort }) => ({
189
+ id, label, ...(contextWindow ? { contextWindow } : {}), supportedReasoningEfforts, ...(defaultReasoningEffort ? { defaultReasoningEffort } : {}),
190
+ })),
191
+ ];
192
+ failures.delete(key);
193
+ cache.set(key, { expiresAt: now() + ttlMs, value: models });
194
+ return models;
195
+ }
196
+ catch (error) {
197
+ failures.set(key, { expiresAt: now() + negativeTtlMs });
198
+ throw error;
199
+ }
200
+ })();
201
+ inFlight.set(key, pending);
202
+ void pending.finally(() => inFlight.delete(key)).catch(() => undefined);
203
+ }
204
+ return await pending;
205
+ },
206
+ };
207
+ }
208
+ export function createCodexModelDirectoryResolver(options = {}) {
209
+ const discover = options.discoverCodexModelCatalog ?? discoverCodexModelCatalog;
210
+ const now = options.now ?? Date.now;
211
+ const ttlMs = options.ttlMs ?? 5 * 60_000;
212
+ const cache = new Map();
213
+ const inFlight = new Map();
214
+ return {
215
+ async resolve(input) {
216
+ const key = [input.agentId, input.cwd, input.env.CODEX_HOME ?? ""].join("\u0000");
217
+ pruneCache(cache, (entry) => now() >= entry.expiresAt, 64);
218
+ const cached = cache.get(key);
219
+ if (cached && now() < cached.expiresAt)
220
+ return cached.value;
221
+ let pending = inFlight.get(key);
222
+ if (!pending) {
223
+ pending = (async () => {
224
+ const catalog = await discover({ cwd: input.cwd, env: input.env });
225
+ const models = [
226
+ { id: "default", label: `default: ${catalog.effectiveModel}` },
227
+ ...catalog.models.map(({ id, label, supportedReasoningEfforts, defaultReasoningEffort }) => ({
228
+ id, label, supportedReasoningEfforts, ...(defaultReasoningEffort ? { defaultReasoningEffort } : {}),
229
+ })),
230
+ ];
231
+ cache.set(key, { expiresAt: now() + ttlMs, value: models });
232
+ return models;
233
+ })();
234
+ inFlight.set(key, pending);
235
+ void pending.finally(() => inFlight.delete(key)).catch(() => undefined);
236
+ }
237
+ return await pending;
238
+ },
239
+ };
240
+ }
241
+ export function createClaudeModelDirectoryResolver(options = {}) {
242
+ const discover = options.discoverClaudeModelCatalog ?? discoverClaudeModelCatalog;
243
+ const now = options.now ?? Date.now;
244
+ const ttlMs = options.ttlMs ?? 5 * 60_000;
245
+ const cache = new Map();
246
+ const inFlight = new Map();
247
+ return {
248
+ async resolve(input) {
249
+ const key = [input.agentId, input.cwd, input.env.CLAUDE_CONFIG_DIR ?? ""].join("\u0000");
250
+ pruneCache(cache, (entry) => now() >= entry.expiresAt, 64);
251
+ const cached = cache.get(key);
252
+ if (cached && now() < cached.expiresAt)
253
+ return cached.value;
254
+ let pending = inFlight.get(key);
255
+ if (!pending) {
256
+ pending = (async () => {
257
+ const catalog = await discover({ cwd: input.cwd, env: input.env });
258
+ const models = [
259
+ { id: "default", label: `default: ${catalog.effectiveModel}` },
260
+ ...catalog.models.map(({ id, label, supportedReasoningEfforts }) => ({ id, label, supportedReasoningEfforts })),
261
+ ];
262
+ cache.set(key, { expiresAt: now() + ttlMs, value: models });
263
+ return models;
264
+ })();
265
+ inFlight.set(key, pending);
266
+ void pending.finally(() => inFlight.delete(key)).catch(() => undefined);
267
+ }
268
+ return await pending;
269
+ },
270
+ };
271
+ }
272
+ async function sanitizedView(env, chatDirectoryResolver, onlyAgentId, chatId) {
273
+ const { config } = loadConfig(env);
274
+ const view = safeConfigView(config, onlyAgentId, chatId, configApplyState(env, config));
275
+ for (const item of view.agents) {
276
+ const agentId = String(item.agentId);
277
+ const agent = config.agents[agentId];
278
+ let map = {};
279
+ try {
280
+ const file = path.join(agent.stateDir, "feishu-map.json");
281
+ if (fs.statSync(file).size <= 256 * 1024) {
282
+ const value = JSON.parse(fs.readFileSync(file, "utf8"));
283
+ if (value && typeof value === "object" && !Array.isArray(value))
284
+ map = value;
285
+ }
286
+ }
287
+ catch { /* no known chat map yet */ }
288
+ const configuredChatIds = Object.keys(agent?.chatMentionPolicies || {});
289
+ const aliases = knownChatAliases(map, configuredChatIds);
290
+ let directory = {};
291
+ try {
292
+ directory = await chatDirectoryResolver.resolve({
293
+ agentId,
294
+ chatIds: configuredChatIds,
295
+ configDir: agent.larkConfigDir,
296
+ profile: agent.feishuProfile,
297
+ });
298
+ }
299
+ catch { /* explicit configuration remains visible without a resolved name */ }
300
+ item.knownChats = configuredChatIds.sort((left, right) => left.localeCompare(right, "en")).map((configuredChatId) => ({
301
+ ...(aliases[configuredChatId] ?? { chatId: configuredChatId, displayName: null, kind: "group" }),
302
+ displayName: directory[configuredChatId] ?? aliases[configuredChatId]?.displayName ?? null,
303
+ })).map((knownChat) => ({
304
+ ...knownChat,
305
+ override: agent.chatMentionPolicies?.[knownChat.chatId] ?? "inherit",
306
+ ...resolveMentionPolicy(config, agentId, knownChat.chatId),
307
+ }));
308
+ }
309
+ return view;
310
+ }
311
+ function json(res, status, value) {
312
+ res.writeHead(status, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store" });
313
+ res.end(JSON.stringify(value));
314
+ }
315
+ function assertWriteRequest(req, csrfCapability) {
316
+ const host = String(req.headers.host || "");
317
+ if (!/^(?:localhost|127\.0\.0\.1):\d+$/.test(host))
318
+ throw new Error("invalid host");
319
+ const expectedOrigin = `http://${host}`;
320
+ const origin = String(req.headers.origin || "");
321
+ const referer = String(req.headers.referer || "");
322
+ if (origin ? origin !== expectedOrigin : !referer.startsWith(`${expectedOrigin}/`))
323
+ throw new Error("same-origin required");
324
+ if (req.headers["x-larkin-csrf"] !== csrfCapability)
325
+ throw new Error("csrf rejected");
326
+ if (!String(req.headers["content-type"] || "").toLowerCase().startsWith("application/json"))
327
+ throw new Error("application/json required");
328
+ }
329
+ function assertPrivateReadRequest(req, csrfCapability) {
330
+ const host = String(req.headers.host || "");
331
+ if (!/^(?:localhost|127\.0\.0\.1):\d+$/.test(host))
332
+ throw new Error("invalid host");
333
+ if (req.headers["x-larkin-csrf"] !== csrfCapability)
334
+ throw new Error("capability rejected");
335
+ }
336
+ async function boundedJson(req) {
337
+ const declaredLength = Number(req.headers["content-length"] || 0);
338
+ if (Number.isFinite(declaredLength) && declaredLength > 16 * 1024)
339
+ throw new Error("request too large");
340
+ const chunks = [];
341
+ let size = 0;
342
+ for await (const chunk of req) {
343
+ const bytes = Buffer.from(chunk);
344
+ size += bytes.length;
345
+ if (size > 16 * 1024)
346
+ throw new Error("request too large");
347
+ chunks.push(bytes);
348
+ }
349
+ const body = Buffer.concat(chunks);
350
+ if (body.byteLength > 16 * 1024)
351
+ throw new Error("request too large");
352
+ const value = JSON.parse(body.toString("utf8"));
353
+ if (!value || typeof value !== "object" || Array.isArray(value))
354
+ throw new Error("JSON object required");
355
+ return value;
356
+ }
357
+ function dashboardMutation(value) {
358
+ const operation = String(value.operation || "");
359
+ const allowed = {
360
+ "set-global-mention": ["operation", "value"], "set-agent-mention": ["operation", "agentId", "value"],
361
+ "set-chat-mention": ["operation", "agentId", "chatId", "value"], "set-agent-runtime": ["operation", "agentId", "runtime", "model"],
362
+ "set-agent-model": ["operation", "agentId", "model"], "set-agent-effort": ["operation", "agentId", "effort"],
363
+ };
364
+ if (!allowed[operation] || Object.keys(value).some((key) => !allowed[operation].includes(key)))
365
+ throw new Error("unsupported operation");
366
+ if (operation === "set-global-mention")
367
+ return { kind: operation, value: String(value.value) };
368
+ const agentId = String(value.agentId || "");
369
+ if (operation === "set-agent-mention")
370
+ return { kind: operation, agentId, value: String(value.value) };
371
+ if (operation === "set-chat-mention")
372
+ return { kind: operation, agentId, chatId: String(value.chatId || ""), value: String(value.value) };
373
+ if (operation === "set-agent-runtime")
374
+ return { kind: operation, agentId, runtime: String(value.runtime || ""), ...(value.model ? { model: String(value.model) } : {}) };
375
+ if (operation === "set-agent-model")
376
+ return { kind: operation, agentId, model: String(value.model || "") };
377
+ return { kind: "set-agent-effort", agentId, effort: value.effort === null || value.effort === "default" ? null : String(value.effort || "") };
378
+ }
379
+ export function createDashboardConfigController({ csrfCapability, env = process.env, requestUpsert = requestAgentUpsert, chatDirectoryResolver = createChatDirectoryResolver(), claudeModelDirectoryResolver = createClaudeModelDirectoryResolver(), codexModelDirectoryResolver = createCodexModelDirectoryResolver(), piModelDirectoryResolver = createPiModelDirectoryResolver(), }) {
380
+ const resolveModelDirectory = async (runtime, agentId) => {
381
+ const { config } = loadConfig(env);
382
+ const agent = config.agents[agentId];
383
+ if (!agent)
384
+ throw new Error("unknown agent");
385
+ if (runtime === "pi")
386
+ return await piModelDirectoryResolver.resolve({
387
+ agentId, cwd: agent.workspaceDir, ...(env.PI_CODING_AGENT_DIR ? { agentDir: env.PI_CODING_AGENT_DIR } : {}),
388
+ });
389
+ if (runtime === "codex")
390
+ return await codexModelDirectoryResolver.resolve({ agentId, cwd: agent.workspaceDir, env });
391
+ if (runtime === "claude")
392
+ return await claudeModelDirectoryResolver.resolve({ agentId, cwd: agent.workspaceDir, env });
393
+ const authored = loadRuntimeModels()[runtime];
394
+ if (!authored)
395
+ throw new Error("unknown runtime");
396
+ return authored;
397
+ };
398
+ const assertDirectoryMutation = async (mutation) => {
399
+ if (mutation.kind !== "set-agent-runtime" && mutation.kind !== "set-agent-model" && mutation.kind !== "set-agent-effort")
400
+ return;
401
+ const { config } = loadConfig(env);
402
+ const agent = config.agents[mutation.agentId];
403
+ if (!agent)
404
+ throw new Error("unknown agent");
405
+ const runtime = mutation.kind === "set-agent-runtime" ? mutation.runtime : agent.runtime;
406
+ const model = mutation.kind === "set-agent-runtime" ? mutation.model || "default"
407
+ : mutation.kind === "set-agent-model" ? mutation.model : agent.model;
408
+ if (mutation.kind === "set-agent-effort" && mutation.effort === null)
409
+ return;
410
+ const directory = await resolveModelDirectory(runtime, mutation.agentId);
411
+ const selected = directory.find((candidate) => candidate.id === model);
412
+ if (!selected)
413
+ throw new Error("model is not in current runtime directory");
414
+ if (mutation.kind === "set-agent-effort") {
415
+ const efforts = Array.isArray(selected.supportedReasoningEfforts) ? selected.supportedReasoningEfforts : [];
416
+ if (!efforts.includes(mutation.effort))
417
+ throw new Error("effort is not in current model directory");
418
+ }
419
+ };
420
+ return {
421
+ async handle(req, res, requestUrl) {
422
+ if (requestUrl.pathname === "/api/config" && req.method === "GET") {
423
+ try {
424
+ assertPrivateReadRequest(req, csrfCapability);
425
+ json(res, 200, { ...await sanitizedView(env, chatDirectoryResolver, requestUrl.searchParams.get("agent") || undefined, requestUrl.searchParams.get("chat") || undefined), runtimeModels: loadRuntimeModels() });
426
+ }
427
+ catch (error) {
428
+ json(res, error instanceof Error && /host|capability/.test(error.message) ? 403 : 500, { error: "configuration unavailable" });
429
+ }
430
+ return true;
431
+ }
432
+ if (requestUrl.pathname === "/api/models/pi" && req.method === "GET") {
433
+ try {
434
+ assertPrivateReadRequest(req, csrfCapability);
435
+ const agentId = requestUrl.searchParams.get("agent") || "";
436
+ const { config } = loadConfig(env);
437
+ const agent = config.agents[agentId];
438
+ if (!agent)
439
+ throw new Error("unknown agent");
440
+ const models = await piModelDirectoryResolver.resolve({
441
+ agentId,
442
+ cwd: agent.workspaceDir,
443
+ ...(env.PI_CODING_AGENT_DIR ? { agentDir: env.PI_CODING_AGENT_DIR } : {}),
444
+ });
445
+ json(res, 200, { models });
446
+ }
447
+ catch (error) {
448
+ json(res, error instanceof Error && /host|capability/.test(error.message) ? 403 : 500, { error: "Pi model directory unavailable" });
449
+ }
450
+ return true;
451
+ }
452
+ if (requestUrl.pathname === "/api/models/codex" && req.method === "GET") {
453
+ try {
454
+ assertPrivateReadRequest(req, csrfCapability);
455
+ const agentId = requestUrl.searchParams.get("agent") || "";
456
+ const { config } = loadConfig(env);
457
+ const agent = config.agents[agentId];
458
+ if (!agent)
459
+ throw new Error("unknown agent");
460
+ const models = await codexModelDirectoryResolver.resolve({ agentId, cwd: agent.workspaceDir, env });
461
+ json(res, 200, { models });
462
+ }
463
+ catch (error) {
464
+ json(res, error instanceof Error && /host|capability/.test(error.message) ? 403 : 500, { error: "Codex model directory unavailable" });
465
+ }
466
+ return true;
467
+ }
468
+ if (requestUrl.pathname === "/api/models/claude" && req.method === "GET") {
469
+ try {
470
+ assertPrivateReadRequest(req, csrfCapability);
471
+ const agentId = requestUrl.searchParams.get("agent") || "";
472
+ const { config } = loadConfig(env);
473
+ const agent = config.agents[agentId];
474
+ if (!agent)
475
+ throw new Error("unknown agent");
476
+ const models = await claudeModelDirectoryResolver.resolve({ agentId, cwd: agent.workspaceDir, env });
477
+ json(res, 200, { models });
478
+ }
479
+ catch (error) {
480
+ json(res, error instanceof Error && /host|capability/.test(error.message) ? 403 : 500, { error: "Claude model directory unavailable" });
481
+ }
482
+ return true;
483
+ }
484
+ if (requestUrl.pathname === "/api/config" && req.method === "PATCH") {
485
+ try {
486
+ assertWriteRequest(req, csrfCapability);
487
+ const mutation = dashboardMutation(await boundedJson(req));
488
+ await assertDirectoryMutation(mutation);
489
+ const result = mutateConfig(env, mutation, { kind: "user" });
490
+ json(res, 200, { ok: true, revision: result.revision, persisted: true, applyState: result.applyState, changedScope: result.changedScope });
491
+ }
492
+ catch {
493
+ json(res, 400, { error: "configuration update rejected" });
494
+ }
495
+ return true;
496
+ }
497
+ if (requestUrl.pathname === "/api/config/apply" && req.method === "POST") {
498
+ try {
499
+ assertWriteRequest(req, csrfCapability);
500
+ const body = await boundedJson(req);
501
+ if (Object.keys(body).sort().join(",") !== "agentId" || !/^cli_[A-Za-z0-9]+$/.test(String(body.agentId || "")))
502
+ throw new Error("invalid apply request");
503
+ const { config } = loadConfig(env);
504
+ const agentId = String(body.agentId);
505
+ if (!config.agents[agentId])
506
+ throw new Error("unknown agent");
507
+ const expectedSignature = runtimeConfigSignature(config, agentId);
508
+ const result = await requestUpsert({ larkinHome: config.larkinHome, agentId });
509
+ if (!result.ok) {
510
+ json(res, 409, { error: result.error || "configuration saved but apply failed",
511
+ ...(result.readiness ? { readiness: result.readiness } : {}) });
512
+ return true;
513
+ }
514
+ markConfigApplied(env, agentId, expectedSignature);
515
+ json(res, 200, { ok: true, agentId, applyState: "applied" });
516
+ }
517
+ catch {
518
+ json(res, 409, { error: "configuration saved but apply failed" });
519
+ }
520
+ return true;
521
+ }
522
+ return false;
523
+ },
524
+ };
525
+ }
@@ -0,0 +1,97 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.dashboardBuildFingerprint = dashboardBuildFingerprint;
7
+ exports.dashboardReuseDecision = dashboardReuseDecision;
8
+ exports.reconcileDashboardRecord = reconcileDashboardRecord;
9
+ exports.prepareDashboardLaunch = prepareDashboardLaunch;
10
+ const node_crypto_1 = __importDefault(require("node:crypto"));
11
+ const node_path_1 = __importDefault(require("node:path"));
12
+ const node_fs_1 = __importDefault(require("node:fs"));
13
+ const process_state_js_1 = require("../platform/process-state.cjs");
14
+ const internal_command_js_1 = require("../app/internal-command.cjs");
15
+ const PUBLISHED_RUNTIME_DIRS = ["dist", "assets"];
16
+ const RUNTIME_EXTENSIONS = new Set([".cjs", ".mjs", ".js", ".css", ".json", ".node", ".svg", ".wasm"]);
17
+ function publishedRuntimeFiles(root) {
18
+ const files = [];
19
+ const visit = (directory) => {
20
+ let entries;
21
+ try {
22
+ entries = node_fs_1.default.readdirSync(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name, "en"));
23
+ }
24
+ catch {
25
+ return;
26
+ }
27
+ for (const entry of entries) {
28
+ if (entry.name.startsWith(".") || entry.name.endsWith(".tmp") || entry.name.endsWith(".tgz"))
29
+ continue;
30
+ const absolute = node_path_1.default.join(directory, entry.name);
31
+ if (entry.isDirectory())
32
+ visit(absolute);
33
+ else if (entry.isFile() && RUNTIME_EXTENSIONS.has(node_path_1.default.extname(entry.name)))
34
+ files.push(node_path_1.default.relative(root, absolute));
35
+ }
36
+ };
37
+ for (const directory of PUBLISHED_RUNTIME_DIRS)
38
+ visit(node_path_1.default.join(root, directory));
39
+ return files.sort((a, b) => a.localeCompare(b, "en"));
40
+ }
41
+ /** Same semver can contain a different local build, so hash every published shell runtime byte. */
42
+ function dashboardBuildFingerprint(root) {
43
+ const hash = node_crypto_1.default.createHash("sha256");
44
+ for (const relative of publishedRuntimeFiles(root)) {
45
+ hash.update(relative);
46
+ hash.update("\0");
47
+ hash.update(node_fs_1.default.readFileSync(node_path_1.default.join(root, relative)));
48
+ hash.update("\0");
49
+ }
50
+ return `sha256:${hash.digest("hex")}`;
51
+ }
52
+ function dashboardReuseDecision(record, currentFingerprint) {
53
+ if (record.state === "dead")
54
+ return { action: "start", reason: "dashboard not running" };
55
+ if (record.state === "unknown")
56
+ return { action: "refuse", reason: "dashboard process ownership unknown" };
57
+ if (record.state === "mismatch")
58
+ return { action: "refuse", reason: "dashboard process ownership mismatch" };
59
+ if (record.buildFingerprint === currentFingerprint)
60
+ return { action: "reuse", reason: "owned dashboard build matches" };
61
+ return {
62
+ action: "replace",
63
+ reason: typeof record.buildFingerprint === "string"
64
+ ? "owned dashboard build fingerprint differs"
65
+ : "owned dashboard has no build fingerprint",
66
+ };
67
+ }
68
+ async function reconcileDashboardRecord(record, currentFingerprint, dependencies = {}) {
69
+ const decision = dashboardReuseDecision(record, currentFingerprint);
70
+ if (decision.action === "refuse") {
71
+ throw new Error(`dashboard PID ${record.pid ?? "?"} ${decision.reason}(${record.reason || "no detail"});拒绝复用、替换或并行启动`);
72
+ }
73
+ if (decision.action !== "replace")
74
+ return decision;
75
+ const terminate = dependencies.terminate || ((owned) => (0, process_state_js_1.terminateOwnedProcess)(owned, "SIGTERM"));
76
+ const wait = dependencies.wait || ((owned) => (0, process_state_js_1.waitForProcessExit)(owned));
77
+ terminate(record);
78
+ if (!await wait(record))
79
+ throw new Error(`旧 dashboard(pid ${record.pid})10 秒内未退出;拒绝并行启动`);
80
+ return { action: "start", reason: decision.reason };
81
+ }
82
+ /**
83
+ * Serialize inspect/replace/start decisions. The caller keeps the lock through listen(),
84
+ * preventing two internal dashboard children from both passing the first-run gap.
85
+ */
86
+ async function prepareDashboardLaunch(larkinHome, currentFingerprint, lockCommandToken = (0, internal_command_js_1.processCommandToken)("dashboard", "app/dashboard.mjs")) {
87
+ const lock = (0, process_state_js_1.acquireProcessLock)(node_path_1.default.join(larkinHome, "dashboard-launch.lock.json"), lockCommandToken);
88
+ try {
89
+ const record = (0, process_state_js_1.readProcessState)(larkinHome).dashboard;
90
+ const result = await reconcileDashboardRecord(record, currentFingerprint);
91
+ return { action: result.action, reason: result.reason, record, release: lock.release };
92
+ }
93
+ catch (error) {
94
+ lock.release();
95
+ throw error;
96
+ }
97
+ }