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,416 @@
1
+ import { spawn, spawnSync } from "node:child_process";
2
+ import crypto from "node:crypto";
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { acquireProcessLock } from "../platform/process-state.mjs";
7
+ import { assertSecureBotsDirectory, readSecureBotCredential, validCredentialRecord, } from "../setup/run-credential-preflight.mjs";
8
+ import { resolveOfficialLarkCli } from "./official-lark-cli.mjs";
9
+ import { internalCommandSpec } from "./internal-command.mjs";
10
+ import { assertAgentWorkspaceBound, larkChannelSourceConfigPath, larkChannelWorkspaceConfigPath, managedLarkCliEnv, } from "./agent-lark-cli-workspace.mjs";
11
+ function assertSecureProfileDirectory(directory) {
12
+ const stat = fs.lstatSync(directory);
13
+ if (!stat.isDirectory() || stat.isSymbolicLink()
14
+ || (typeof process.getuid === "function" && stat.uid !== process.getuid())
15
+ || (stat.mode & 0o777) !== 0o700)
16
+ throw new Error("lark-cli profile 目录不安全");
17
+ }
18
+ function captureProfileSnapshot(file) {
19
+ let fd = null;
20
+ try {
21
+ fd = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0));
22
+ const stat = fs.fstatSync(fd);
23
+ if (!stat.isFile()
24
+ || (typeof process.getuid === "function" && stat.uid !== process.getuid())
25
+ || (stat.mode & 0o077) !== 0)
26
+ throw new Error("lark-cli config.json 不安全");
27
+ const raw = fs.readFileSync(fd);
28
+ const value = JSON.parse(raw.toString("utf8"));
29
+ if (!value || typeof value !== "object" || (value.apps !== undefined && !Array.isArray(value.apps))) {
30
+ throw new Error("lark-cli config.json schema 无效");
31
+ }
32
+ return { value, raw, mode: stat.mode & 0o777 };
33
+ }
34
+ catch (error) {
35
+ if (error.code === "ENOENT")
36
+ return null;
37
+ throw error;
38
+ }
39
+ finally {
40
+ if (fd !== null)
41
+ fs.closeSync(fd);
42
+ }
43
+ }
44
+ function atomicPublishProfile(file, bytes, mode = 0o600) {
45
+ const directory = path.dirname(file);
46
+ assertSecureProfileDirectory(directory);
47
+ const canonical = path.join(path.resolve(directory), "config.json");
48
+ if (path.resolve(file) !== canonical || path.basename(file) !== "config.json") {
49
+ throw new Error("lark-cli config 发布路径无效");
50
+ }
51
+ const temporary = path.join(directory, `.config.${process.pid}.${crypto.randomUUID()}.publish`);
52
+ const fd = fs.openSync(temporary, "wx", 0o600);
53
+ try {
54
+ fs.writeFileSync(fd, bytes);
55
+ fs.fchmodSync(fd, mode);
56
+ fs.fsyncSync(fd);
57
+ }
58
+ finally {
59
+ fs.closeSync(fd);
60
+ }
61
+ try {
62
+ let current = null;
63
+ try {
64
+ current = fs.lstatSync(file);
65
+ }
66
+ catch (error) {
67
+ if (error.code !== "ENOENT")
68
+ throw error;
69
+ }
70
+ if (current) {
71
+ if (typeof process.getuid === "function" && current.uid !== process.getuid()) {
72
+ throw new Error("config.json 不属于当前用户");
73
+ }
74
+ if (current.isDirectory()) {
75
+ try {
76
+ fs.rmdirSync(file);
77
+ }
78
+ catch (error) {
79
+ if (["ENOTEMPTY", "EEXIST"].includes(String(error.code || ""))) {
80
+ throw new Error("config.json 已被替换为非空目录");
81
+ }
82
+ throw error;
83
+ }
84
+ }
85
+ else if (!current.isFile() && !current.isSymbolicLink()) {
86
+ throw new Error("config.json 已被替换为不安全文件类型");
87
+ }
88
+ }
89
+ fs.renameSync(temporary, file);
90
+ }
91
+ catch (error) {
92
+ throw new Error(`${error instanceof Error ? error.message : String(error)};待发布内容保留于 ${temporary}`);
93
+ }
94
+ }
95
+ function containsUserToken(value) {
96
+ if (Array.isArray(value))
97
+ return value.some(containsUserToken);
98
+ if (!value || typeof value !== "object")
99
+ return false;
100
+ return Object.entries(value).some(([key, nested]) => (/token/i.test(key) && nested !== null && nested !== "" && nested !== undefined) || containsUserToken(nested));
101
+ }
102
+ function validStoredAppSecret(value, appId) {
103
+ if (!value || typeof value !== "object" || Array.isArray(value))
104
+ return false;
105
+ const reference = value;
106
+ return reference.source === "keychain" && reference.id === `appsecret:${appId}`;
107
+ }
108
+ function validateExclusiveBotProfile(snapshot, agent) {
109
+ const apps = snapshot.value.apps;
110
+ if (!Array.isArray(apps) || apps.length !== 1)
111
+ throw new Error("lark-cli profile 必须只包含当前 Agent App");
112
+ const app = apps[0];
113
+ if (app.appId !== agent.feishuAppId || (app.name !== undefined && app.name !== agent.feishuAppId)) {
114
+ throw new Error("lark-cli profile App identity 与当前 Agent 不一致");
115
+ }
116
+ if (!validStoredAppSecret(app.appSecret, agent.feishuAppId))
117
+ throw new Error("lark-cli workspace App Secret 必须是当前 App 的 keychain 引用");
118
+ if (app.defaultAs !== "bot" || app.strictMode !== "bot")
119
+ throw new Error("lark-channel workspace 未锁定 bot-only identity policy");
120
+ if (Array.isArray(app.users) ? app.users.length !== 0
121
+ : app.users && typeof app.users === "object" ? Object.keys(app.users).length !== 0 : app.users !== undefined && app.users !== null) {
122
+ throw new Error("lark-cli profile 不得保留 user identity");
123
+ }
124
+ if (containsUserToken(snapshot.value))
125
+ throw new Error("lark-cli profile 不得保留 user token");
126
+ }
127
+ export function hydrateRuntimeAgent(configDir, agent) {
128
+ const botsDir = path.join(configDir, "bots");
129
+ assertSecureBotsDirectory(botsDir);
130
+ const record = readSecureBotCredential(botsDir, agent.feishuAppId);
131
+ if (!validCredentialRecord(record, agent.feishuAppId)) {
132
+ throw new Error(`Agent ${agent.agentId} 的 bot 凭证无效或与 App ID 不一致`);
133
+ }
134
+ const credential = record;
135
+ // updatedAt is the authoritative secret-rotation revision. Other credential metadata
136
+ // (owner/callback capability) must never cause a keychain write on the next startup.
137
+ // Legacy records are rebound once by setup and remain on a stable non-secret sentinel.
138
+ const credentialRevision = credential.updatedAt ? `updated:${credential.updatedAt}` : "legacy-unversioned";
139
+ return {
140
+ ...agent,
141
+ feishuAppSecret: credential.appSecret,
142
+ feishuDomain: credential.tenant === "lark" ? "https://open.larksuite.com" : "https://open.feishu.cn",
143
+ credentialRevision,
144
+ };
145
+ }
146
+ function shellQuote(value) {
147
+ return `'${value.replaceAll("'", `'"'"'`)}'`;
148
+ }
149
+ function assertSecureRuntimeCommandDirectory(commandDir) {
150
+ fs.mkdirSync(commandDir, { recursive: true, mode: 0o700 });
151
+ const stat = fs.lstatSync(commandDir);
152
+ if (!stat.isDirectory() || stat.isSymbolicLink()
153
+ || (typeof process.getuid === "function" && stat.uid !== process.getuid())) {
154
+ throw new Error("Runtime command shim 目录不安全");
155
+ }
156
+ fs.chmodSync(commandDir, 0o700);
157
+ }
158
+ function runOfficialLarkCli(command, args, options, dependencies) {
159
+ return dependencies.runOfficialCli
160
+ ? dependencies.runOfficialCli(command, args, options)
161
+ : spawnSync(command.command, [...command.argsPrefix, ...args], options);
162
+ }
163
+ function officialFailure(label, result, secret) {
164
+ const stderr = String(result.stderr || "").replaceAll(secret, "<redacted>").trim().slice(0, 400);
165
+ const detail = [result.error?.message, stderr].filter(Boolean).join(": ");
166
+ return new Error(`${label} failed (exit=${result.status ?? "none"})${detail ? `: ${detail}` : ""}`);
167
+ }
168
+ function runOfficialLarkCliAsync(command, args, env, dependencies) {
169
+ const timeoutMs = dependencies.timeoutMs ?? 60_000;
170
+ const maxOutputBytes = dependencies.maxOutputBytes ?? 64 * 1024;
171
+ return new Promise((resolve, reject) => {
172
+ const child = spawn(command.command, [...command.argsPrefix, ...args], { env, stdio: ["ignore", "pipe", "pipe"] });
173
+ dependencies.onChild?.(child);
174
+ const stdout = [];
175
+ const stderr = [];
176
+ let outputBytes = 0;
177
+ let failure = null;
178
+ let settled = false;
179
+ let killTimer = null;
180
+ const terminate = (error) => {
181
+ if (failure)
182
+ return;
183
+ failure = error;
184
+ child.kill("SIGTERM");
185
+ killTimer = setTimeout(() => { if (child.exitCode === null && child.signalCode === null)
186
+ child.kill("SIGKILL"); }, 1_000);
187
+ killTimer.unref?.();
188
+ };
189
+ const collect = (target, chunk) => {
190
+ outputBytes += chunk.length;
191
+ if (outputBytes > maxOutputBytes) {
192
+ terminate(new Error("official lark-cli output exceeded the bounded limit"));
193
+ return;
194
+ }
195
+ target.push(Buffer.from(chunk));
196
+ };
197
+ child.stdout?.on("data", (chunk) => collect(stdout, chunk));
198
+ child.stderr?.on("data", (chunk) => collect(stderr, chunk));
199
+ const timeout = setTimeout(() => terminate(new Error("official lark-cli config bind timed out")), timeoutMs);
200
+ timeout.unref?.();
201
+ const abort = () => terminate(new Error("official lark-cli config bind cancelled"));
202
+ if (dependencies.signal?.aborted)
203
+ abort();
204
+ else
205
+ dependencies.signal?.addEventListener("abort", abort, { once: true });
206
+ child.once("error", (error) => {
207
+ if (settled)
208
+ return;
209
+ settled = true;
210
+ clearTimeout(timeout);
211
+ if (killTimer)
212
+ clearTimeout(killTimer);
213
+ dependencies.signal?.removeEventListener("abort", abort);
214
+ dependencies.onChild?.(null);
215
+ reject(error);
216
+ });
217
+ child.once("exit", (status) => {
218
+ if (settled)
219
+ return;
220
+ settled = true;
221
+ clearTimeout(timeout);
222
+ if (killTimer)
223
+ clearTimeout(killTimer);
224
+ dependencies.signal?.removeEventListener("abort", abort);
225
+ dependencies.onChild?.(null);
226
+ if (failure) {
227
+ reject(failure);
228
+ return;
229
+ }
230
+ resolve({ status, stdout: Buffer.concat(stdout).toString("utf8"), stderr: Buffer.concat(stderr).toString("utf8") });
231
+ });
232
+ });
233
+ }
234
+ export function installRuntimeCommandShims(agent) {
235
+ const stateDir = path.resolve(agent.stateDir);
236
+ const commandDir = path.join(stateDir, "runtime-bin");
237
+ assertSecureRuntimeCommandDirectory(commandDir);
238
+ const standalone = process.env.LARKIN_STANDALONE === "1";
239
+ const binaryEntry = fileURLToPath(new URL("./binary-entry.mjs", import.meta.url));
240
+ for (const [name, argumentsPrefix] of [["larkin", standalone ? [] : [binaryEntry]]]) {
241
+ const file = path.join(commandDir, name);
242
+ const temporary = path.join(commandDir, `.${name}.${process.pid}.${crypto.randomUUID()}.tmp`);
243
+ const command = [process.execPath, ...argumentsPrefix].map(shellQuote).join(" ");
244
+ fs.writeFileSync(temporary, `#!/bin/sh\nexec ${command} "$@"\n`, { mode: 0o700, flag: "wx" });
245
+ fs.renameSync(temporary, file);
246
+ fs.chmodSync(file, 0o700);
247
+ }
248
+ return commandDir;
249
+ }
250
+ function sourceProjection(agent, env) {
251
+ const configDir = path.resolve(env.LARKIN_CONFIG_DIR || "");
252
+ if (!configDir)
253
+ throw new Error("LARKIN_CONFIG_DIR required for lark-channel source projection");
254
+ const provider = internalCommandSpec("lark-channel-secret", [], {
255
+ ...env,
256
+ LARKIN_BINARY_ENTRY_PATH: fileURLToPath(new URL("./binary-entry.mjs", import.meta.url)),
257
+ });
258
+ return {
259
+ credentialRevision: agent.credentialRevision,
260
+ accounts: { app: {
261
+ id: agent.feishuAppId,
262
+ secret: { source: "exec", provider: "larkin-bot-credential", id: agent.feishuAppId },
263
+ tenant: agent.feishuDomain === "https://open.larksuite.com" ? "lark" : "feishu",
264
+ } },
265
+ secrets: { providers: { "larkin-bot-credential": {
266
+ source: "exec", command: provider.command, args: provider.args,
267
+ noOutputTimeoutMs: 5_000, maxOutputBytes: 16 * 1024, jsonOnly: true,
268
+ env: {
269
+ LARKIN_CONFIG_DIR: configDir,
270
+ LARKIN_AGENT_ID: agent.agentId,
271
+ LARKIN_SECRET_PROVIDER_CONTEXT: "bind",
272
+ },
273
+ } } },
274
+ };
275
+ }
276
+ function validateSourceProjection(file, agent) {
277
+ const snapshot = captureProfileSnapshot(file);
278
+ if (!snapshot)
279
+ throw new Error(`Agent ${agent.agentId} lark-channel source projection missing`);
280
+ const root = snapshot.value;
281
+ const app = root.accounts?.app;
282
+ const provider = root.secrets?.providers?.["larkin-bot-credential"];
283
+ if (app?.id !== agent.feishuAppId || app?.secret?.source !== "exec"
284
+ || app?.secret?.provider !== "larkin-bot-credential" || app?.secret?.id !== agent.feishuAppId) {
285
+ throw new Error(`Agent ${agent.agentId} lark-channel source projection identity mismatch`);
286
+ }
287
+ if (provider?.source !== "exec" || typeof provider.command !== "string" || !path.isAbsolute(provider.command)
288
+ || !Array.isArray(provider.args) || provider.env?.LARKIN_AGENT_ID !== agent.agentId
289
+ || provider.env?.LARKIN_SECRET_PROVIDER_CONTEXT !== "bind") {
290
+ throw new Error(`Agent ${agent.agentId} lark-channel source projection provider invalid`);
291
+ }
292
+ if (JSON.stringify(snapshot.value).includes("appSecret"))
293
+ throw new Error("lark-channel source projection 不得包含 plaintext secret 字段");
294
+ if (root.credentialRevision !== agent.credentialRevision)
295
+ throw new Error(`Agent ${agent.agentId} lark-channel credential revision mismatch`);
296
+ }
297
+ export function syncAgentProfile(agent, env, dependencies = {}) {
298
+ const expected = path.join(path.resolve(env.LARKIN_CONFIG_DIR || ""), "state", "agents", agent.agentId, "lark-cli-config");
299
+ if (path.resolve(agent.larkConfigDir) !== expected)
300
+ throw new Error("lark-cli profile 路径不是 canonical contained 路径");
301
+ fs.mkdirSync(expected, { recursive: true, mode: 0o700 });
302
+ assertSecureProfileDirectory(expected);
303
+ const sourceFile = larkChannelSourceConfigPath(agent);
304
+ const sourceDir = path.dirname(sourceFile);
305
+ fs.mkdirSync(sourceDir, { recursive: true, mode: 0o700 });
306
+ assertSecureProfileDirectory(sourceDir);
307
+ const lock = acquireProcessLock(path.join(expected, ".larkin-profile-sync.lock.json"), path.basename(process.argv[1] || "node"));
308
+ const workspaceFile = larkChannelWorkspaceConfigPath(agent);
309
+ const stagedSource = path.join(sourceDir, `.config.${process.pid}.${crypto.randomUUID()}.bind-source`);
310
+ try {
311
+ // Parse/security failures are pre-bind failures; a merely stale valid state may be rebound.
312
+ captureProfileSnapshot(sourceFile);
313
+ captureProfileSnapshot(workspaceFile);
314
+ fs.writeFileSync(stagedSource, `${JSON.stringify(sourceProjection(agent, env), null, 2)}\n`, { mode: 0o600, flag: "wx" });
315
+ validateSourceProjection(stagedSource, agent);
316
+ installRuntimeCommandShims(agent);
317
+ if (!dependencies.forceRebind) {
318
+ try {
319
+ validateSourceProjection(sourceFile, agent);
320
+ const existingWorkspace = captureProfileSnapshot(workspaceFile);
321
+ if (!existingWorkspace)
322
+ throw new Error("workspace missing");
323
+ validateExclusiveBotProfile(existingWorkspace, agent);
324
+ assertAgentWorkspaceBound(agent);
325
+ return;
326
+ }
327
+ catch { /* stale, absent, or mismatched state requires exactly one bind */ }
328
+ }
329
+ const profileEnv = { ...managedLarkCliEnv(agent, env), LARK_CHANNEL_CONFIG: stagedSource };
330
+ try {
331
+ const official = dependencies.resolveOfficialCli?.(profileEnv) ?? resolveOfficialLarkCli({ env: profileEnv });
332
+ const sync = runOfficialLarkCli(official, ["config", "bind", "--source", "lark-channel", "--identity", "bot-only"], {
333
+ encoding: "utf8", env: profileEnv,
334
+ }, dependencies);
335
+ if (sync.status !== 0 || sync.error)
336
+ throw officialFailure(`Agent ${agent.agentId} lark-channel bind`, sync, agent.feishuAppSecret);
337
+ const workspace = captureProfileSnapshot(workspaceFile);
338
+ if (!workspace)
339
+ throw new Error(`Agent ${agent.agentId} lark-channel workspace config missing`);
340
+ validateExclusiveBotProfile(workspace, agent);
341
+ atomicPublishProfile(sourceFile, fs.readFileSync(stagedSource), 0o600);
342
+ assertAgentWorkspaceBound(agent);
343
+ }
344
+ catch (error) {
345
+ const message = error instanceof Error ? error.message : `Agent ${agent.agentId} lark-channel bind failed`;
346
+ throw new Error(`${message};官方 bind/keychain 结果未被证明可回滚,当前 Agent 保持 fail-closed,请重跑 larkin setup`);
347
+ }
348
+ }
349
+ finally {
350
+ try {
351
+ fs.unlinkSync(stagedSource);
352
+ }
353
+ catch { /* renamed/absent */ }
354
+ lock.release();
355
+ }
356
+ }
357
+ /** Async setup-only profile sync. Existing runtime callers retain the synchronous contract above. */
358
+ export async function syncAgentProfileAsync(agent, env, dependencies = {}) {
359
+ const expected = path.join(path.resolve(env.LARKIN_CONFIG_DIR || ""), "state", "agents", agent.agentId, "lark-cli-config");
360
+ if (path.resolve(agent.larkConfigDir) !== expected)
361
+ throw new Error("lark-cli profile 路径不是 canonical contained 路径");
362
+ fs.mkdirSync(expected, { recursive: true, mode: 0o700 });
363
+ assertSecureProfileDirectory(expected);
364
+ const sourceFile = larkChannelSourceConfigPath(agent);
365
+ const sourceDir = path.dirname(sourceFile);
366
+ fs.mkdirSync(sourceDir, { recursive: true, mode: 0o700 });
367
+ assertSecureProfileDirectory(sourceDir);
368
+ const lock = acquireProcessLock(path.join(expected, ".larkin-profile-sync.lock.json"), path.basename(process.argv[1] || "node"));
369
+ const workspaceFile = larkChannelWorkspaceConfigPath(agent);
370
+ const stagedSource = path.join(sourceDir, `.config.${process.pid}.${crypto.randomUUID()}.bind-source`);
371
+ try {
372
+ captureProfileSnapshot(sourceFile);
373
+ captureProfileSnapshot(workspaceFile);
374
+ fs.writeFileSync(stagedSource, `${JSON.stringify(sourceProjection(agent, env), null, 2)}\n`, { mode: 0o600, flag: "wx" });
375
+ validateSourceProjection(stagedSource, agent);
376
+ installRuntimeCommandShims(agent);
377
+ if (!dependencies.forceRebind) {
378
+ try {
379
+ validateSourceProjection(sourceFile, agent);
380
+ const existingWorkspace = captureProfileSnapshot(workspaceFile);
381
+ if (!existingWorkspace)
382
+ throw new Error("workspace missing");
383
+ validateExclusiveBotProfile(existingWorkspace, agent);
384
+ assertAgentWorkspaceBound(agent);
385
+ return;
386
+ }
387
+ catch { /* stale, absent, or mismatched state requires exactly one bind */ }
388
+ }
389
+ const profileEnv = { ...managedLarkCliEnv(agent, env), LARK_CHANNEL_CONFIG: stagedSource };
390
+ try {
391
+ const official = dependencies.resolveOfficialCli?.(profileEnv) ?? resolveOfficialLarkCli({ env: profileEnv });
392
+ const result = await runOfficialLarkCliAsync(official, ["config", "bind", "--source", "lark-channel", "--identity", "bot-only"], profileEnv, dependencies);
393
+ if (result.status !== 0) {
394
+ const stderr = result.stderr.replaceAll(agent.feishuAppSecret, "<redacted>").trim().slice(0, 400);
395
+ throw new Error(`Agent ${agent.agentId} lark-channel bind failed (exit=${result.status ?? "none"})${stderr ? `: ${stderr}` : ""}`);
396
+ }
397
+ const workspace = captureProfileSnapshot(workspaceFile);
398
+ if (!workspace)
399
+ throw new Error(`Agent ${agent.agentId} lark-channel workspace config missing`);
400
+ validateExclusiveBotProfile(workspace, agent);
401
+ atomicPublishProfile(sourceFile, fs.readFileSync(stagedSource), 0o600);
402
+ assertAgentWorkspaceBound(agent);
403
+ }
404
+ catch (error) {
405
+ const message = error instanceof Error ? error.message : `Agent ${agent.agentId} lark-channel bind failed`;
406
+ throw new Error(`${message};官方 bind/keychain 结果未被证明可回滚,当前 Agent 保持 fail-closed,请重跑 larkin setup`);
407
+ }
408
+ }
409
+ finally {
410
+ try {
411
+ fs.unlinkSync(stagedSource);
412
+ }
413
+ catch { /* renamed/absent */ }
414
+ lock.release();
415
+ }
416
+ }
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env bun
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { discoverClaudeModelCatalog } from "../runtime/claude-model-catalog.mjs";
6
+ import { discoverCodexModelCatalog } from "../runtime/codex-model-catalog.mjs";
7
+ import { discoverPiModelCatalog } from "../runtime/pi-model-catalog.mjs";
8
+ function readTestModelDirectory(env) {
9
+ const fixture = env?.LARKIN_TEST_RUNTIME_MODEL_DIRECTORY_FILE;
10
+ if (!fixture)
11
+ return undefined;
12
+ const parsed = JSON.parse(fs.readFileSync(fixture, "utf8"));
13
+ if (!Array.isArray(parsed.models))
14
+ throw new Error("runtime model directory test fixture is invalid");
15
+ return parsed.models;
16
+ }
17
+ export async function discoverRuntimeModelDirectory(input) {
18
+ const testDirectory = readTestModelDirectory(input.env);
19
+ if (testDirectory)
20
+ return testDirectory;
21
+ if (input.runtime === "codex") {
22
+ const catalog = await discoverCodexModelCatalog({ cwd: input.cwd, env: input.env });
23
+ return [
24
+ { id: "default", label: `default: ${catalog.effectiveModel}` },
25
+ ...catalog.models.map(({ id, label, supportedReasoningEfforts, defaultReasoningEffort }) => ({
26
+ id, label, supportedReasoningEfforts, ...(defaultReasoningEffort ? { defaultReasoningEffort } : {}),
27
+ })),
28
+ ];
29
+ }
30
+ if (input.runtime === "claude") {
31
+ const catalog = await discoverClaudeModelCatalog({ cwd: input.cwd, env: input.env });
32
+ return [
33
+ { id: "default", label: `default: ${catalog.effectiveModel}` },
34
+ ...catalog.models.map(({ id, label, supportedReasoningEfforts }) => ({ id, label, supportedReasoningEfforts })),
35
+ ];
36
+ }
37
+ if (input.runtime === "pi") {
38
+ const catalog = await discoverPiModelCatalog({ cwd: input.cwd, ...(input.agentDir ? { agentDir: input.agentDir } : {}) });
39
+ if (!catalog.effectiveModel)
40
+ throw new Error("Pi 模型目录未解析出默认模型");
41
+ return [
42
+ { id: "default", label: `default: ${catalog.effectiveModel}` },
43
+ ...catalog.models.map(({ id, label, supportedReasoningEfforts, defaultReasoningEffort }) => ({
44
+ id, label, supportedReasoningEfforts, ...(defaultReasoningEffort ? { defaultReasoningEffort } : {}),
45
+ })),
46
+ ];
47
+ }
48
+ throw new Error(`未知 runtime:${input.runtime}`);
49
+ }
50
+ function isMainEntry(argvPath = process.argv[1]) {
51
+ if (!argvPath)
52
+ return false;
53
+ try {
54
+ return fs.realpathSync(argvPath) === fs.realpathSync(fileURLToPath(import.meta.url));
55
+ }
56
+ catch {
57
+ return path.resolve(argvPath) === path.resolve(fileURLToPath(import.meta.url));
58
+ }
59
+ }
60
+ export async function main() {
61
+ const runtime = process.argv[2] || "";
62
+ const cwd = process.argv[3] || "";
63
+ if (!runtime || !cwd)
64
+ throw new Error("runtime model directory requires runtime and cwd");
65
+ const models = await discoverRuntimeModelDirectory({
66
+ runtime,
67
+ cwd,
68
+ env: process.env,
69
+ ...(process.env.PI_CODING_AGENT_DIR ? { agentDir: process.env.PI_CODING_AGENT_DIR } : {}),
70
+ });
71
+ process.stdout.write(`${JSON.stringify({ models })}\n`);
72
+ }
73
+ if (isMainEntry()) {
74
+ main().catch((error) => {
75
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
76
+ process.exitCode = 1;
77
+ });
78
+ }
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env bun
2
+ import path from "node:path";
3
+ import { fileURLToPath, pathToFileURL } from "node:url";
4
+ import { createHostShell } from "../feishu/host-shell.mjs";
5
+ import { ContextPromptBuilder } from "../agent/context-prompt.mjs";
6
+ import { createNativeRuntimeAdapter } from "../runtime/runtime-adapters.mjs";
7
+ import { createRuntimeHost } from "../runtime/runtime-host.mjs";
8
+ import { createAgentStateStore } from "../agent/agent-state-store.mjs";
9
+ import { loadConfig, markConfigApplied, runtimeConfigSignature } from "../platform/config.mjs";
10
+ import { createAgentControlServer } from "./local-control.mjs";
11
+ import { hydrateRuntimeAgent, syncAgentProfile } from "./runtime-agent-config.mjs";
12
+ import { loadTelemetryConfig } from "../platform/telemetry-config.mjs";
13
+ import { telemetrySingleton } from "../platform/telemetry-tracing.mjs";
14
+ import { packageVersion } from "../platform/build-info.mjs";
15
+ export function loadAndSyncRuntimeAgent(env, agentId, dependencies = {}) {
16
+ const loaded = loadConfig(env);
17
+ const stored = loaded.config.agents[agentId];
18
+ if (!stored)
19
+ throw new Error(`Agent ${agentId} 不存在于 canonical config`);
20
+ const agent = hydrateRuntimeAgent(loaded.configDir, stored);
21
+ syncAgentProfile(agent, { ...env, LARKIN_CONFIG_DIR: loaded.configDir }, dependencies);
22
+ return agent;
23
+ }
24
+ export async function markConfigAppliedAfterRuntimeReady(env, runningAgents, runtimeReady) {
25
+ await runtimeReady;
26
+ try {
27
+ const loaded = loadConfig(env);
28
+ for (const running of runningAgents) {
29
+ const current = loaded.config.agents[running.agentId];
30
+ if (!current || current.runtime !== running.runtime || current.model !== running.model
31
+ || (current.piDistribution || "external") !== (running.piDistribution || "external")
32
+ || (current.effort || null) !== (running.effort || null))
33
+ continue;
34
+ markConfigApplied(env, running.agentId, runtimeConfigSignature(loaded.config, running.agentId));
35
+ }
36
+ }
37
+ catch { /* Runtime is ready; apply projection stays pending on a concurrent config change. */ }
38
+ }
39
+ export async function main(env = process.env, overrides = {}) {
40
+ let telemetry = telemetrySingleton();
41
+ try {
42
+ const configured = JSON.parse(env.LARKIN_AGENTS_CONFIG || "[]");
43
+ const stateDirs = new Map(configured.flatMap((agent) => agent.agentId && agent.stateDir ? [[agent.agentId, agent.stateDir]] : []));
44
+ const sourceRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
45
+ telemetry = telemetrySingleton(loadTelemetryConfig(env), { stateDirFor: (agentId) => stateDirs.get(agentId),
46
+ stateDirs: [...stateDirs.values()], serviceVersion: packageVersion(sourceRoot) });
47
+ }
48
+ catch (error) {
49
+ process.stderr.write(`[telemetry] disabled after initialization failure: ${error instanceof Error ? error.name : "unknown"}\n`);
50
+ }
51
+ let channelPackage = overrides.channelPackage;
52
+ if (!channelPackage && env.LARKIN_FEISHU_DRYRUN === "1" && env.LARKIN_TEST_CHANNEL_MODULE) {
53
+ channelPackage = await import(pathToFileURL(path.resolve(env.LARKIN_TEST_CHANNEL_MODULE)).href);
54
+ }
55
+ const adapters = new Map();
56
+ const runtimeHost = overrides.runtimeHost ?? createRuntimeHost({
57
+ promptBuilder: new ContextPromptBuilder(),
58
+ stateStoreFor(agentId) {
59
+ if (!env.LARKIN_HOME)
60
+ throw new Error("LARKIN_HOME is required for Runtime delivery state");
61
+ return createAgentStateStore(env.LARKIN_HOME, agentId);
62
+ },
63
+ adapterFor(runtime) {
64
+ let adapter = adapters.get(runtime);
65
+ if (!adapter) {
66
+ adapter = createNativeRuntimeAdapter(runtime, { env,
67
+ ...(runtime === "codex" && env.LARKIN_CODEX_COMMAND ? { codexCommand: env.LARKIN_CODEX_COMMAND } : {}),
68
+ ...(runtime === "codex" && env.LARKIN_CODEX_MODEL ? { codexModelOverride: env.LARKIN_CODEX_MODEL } : {}) });
69
+ adapters.set(runtime, adapter);
70
+ }
71
+ return adapter;
72
+ },
73
+ log: (...parts) => process.stderr.write(`[runtime] ${parts.join(" ")}\n`),
74
+ telemetry,
75
+ });
76
+ let controlServer = null;
77
+ const hostShell = createHostShell({
78
+ env,
79
+ runtimeHost,
80
+ ...(channelPackage ? { channelPackage } : {}),
81
+ ...(overrides.eventSourceStartDelayMs !== undefined ? { eventSourceStartDelayMs: overrides.eventSourceStartDelayMs } : {}),
82
+ ...(overrides.channelDisconnectTimeoutMs !== undefined ? { channelDisconnectTimeoutMs: overrides.channelDisconnectTimeoutMs } : {}),
83
+ onOrderedShutdownComplete: (exitCode) => {
84
+ void Promise.allSettled([controlServer?.close(), telemetry.shutdown()]).finally(() => {
85
+ (overrides.exitProcess ?? ((code) => process.exit(code)))(exitCode);
86
+ });
87
+ },
88
+ telemetry,
89
+ });
90
+ if (!env.LARKIN_HOME || !env.LARKIN_CONFIG_DIR)
91
+ throw new Error("LARKIN_HOME/LARKIN_CONFIG_DIR required");
92
+ if (!env.LARKIN_CONTROL_AUTHORIZATION)
93
+ throw new Error("LARKIN_CONTROL_AUTHORIZATION required");
94
+ controlServer = createAgentControlServer({
95
+ larkinHome: env.LARKIN_HOME,
96
+ authorityToken: env.LARKIN_CONTROL_AUTHORIZATION,
97
+ async upsert(request) {
98
+ const agent = loadAndSyncRuntimeAgent(env, request.agentId);
99
+ // Only the selected profile is synchronized; active profiles and their directory
100
+ // are never quarantined or rebuilt during hot attach.
101
+ await hostShell.upsertAgent(agent);
102
+ },
103
+ async resetSession(request) {
104
+ const result = await hostShell.resetSession(request.agentId, request.waitReadyMs);
105
+ return { ok: result.readyForFreshScenario, agentId: request.agentId, ...result };
106
+ },
107
+ });
108
+ await controlServer.start();
109
+ await markConfigAppliedAfterRuntimeReady(env, hostShell.agents, hostShell.start());
110
+ }
111
+ if (path.resolve(process.argv[1] || "") === path.resolve(fileURLToPath(import.meta.url))) {
112
+ main().catch((error) => { console.error(`✗ ${error instanceof Error ? error.message : String(error)}`); process.exit(1); });
113
+ }