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,89 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ const node_child_process_1 = require("node:child_process");
6
+ const node_path_1 = __importDefault(require("node:path"));
7
+ function validPid(pid) {
8
+ const value = Number(pid);
9
+ return Number.isInteger(value) && value > 0 ? value : null;
10
+ }
11
+ function pidAlive(pid, kill = process.kill) {
12
+ const value = validPid(pid);
13
+ if (!value)
14
+ return false;
15
+ try {
16
+ kill(value, 0);
17
+ return true;
18
+ }
19
+ catch (error) {
20
+ // POSIX reserves ESRCH for a missing process. EPERM means the process may
21
+ // be alive but is not inspectable by this user and therefore must fail closed.
22
+ return error.code !== "ESRCH";
23
+ }
24
+ }
25
+ function inspectUnix(pid) {
26
+ const env = { ...process.env, LC_ALL: "C", LANG: "C" };
27
+ const state = (0, node_child_process_1.spawnSync)("ps", ["-ww", "-p", String(pid), "-o", "stat="], { encoding: "utf8", env });
28
+ const started = (0, node_child_process_1.spawnSync)("ps", ["-ww", "-p", String(pid), "-o", "lstart="], { encoding: "utf8", env });
29
+ const command = (0, node_child_process_1.spawnSync)("ps", ["-ww", "-p", String(pid), "-o", "command="], { encoding: "utf8", env });
30
+ if (state.status === 0 && /^Z/.test(String(state.stdout || "").trim()))
31
+ return { ok: false, dead: true, reason: "process exited" };
32
+ // The process can disappear between the initial kill(0) probe and ps. Re-checking here
33
+ // distinguishes a normal exit race from an inspection failure that must remain fail-closed.
34
+ if ((state.status !== 0 || started.status !== 0 || command.status !== 0) && !pidAlive(pid)) {
35
+ return { ok: false, dead: true, reason: "process exited" };
36
+ }
37
+ if (started.status !== 0 || command.status !== 0)
38
+ return { ok: false, reason: "ps failed" };
39
+ const startedText = String(started.stdout || "").trim();
40
+ const commandText = String(command.stdout || "").trim();
41
+ if (!startedText || !commandText)
42
+ return { ok: false, reason: "ps metadata incomplete" };
43
+ // `ps lstart` omits a timezone. Parsing it into an ISO timestamp makes the
44
+ // same process look different when two Bun processes have different TZ
45
+ // state (notably a Bun test parent and a normal Bun child). The value is an
46
+ // opaque identity token, so preserve the stable kernel-provided text.
47
+ return { ok: true, command: commandText, startToken: startedText };
48
+ }
49
+ function inspectWindows(pid) {
50
+ const script = `$p=Get-CimInstance Win32_Process -Filter \"ProcessId=${pid}\"; if($null -eq $p){exit 3}; [pscustomobject]@{CommandLine=$p.CommandLine;CreationDate=$p.CreationDate.ToUniversalTime().ToString('o')} | ConvertTo-Json -Compress`;
51
+ const result = (0, node_child_process_1.spawnSync)("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], { encoding: "utf8" });
52
+ if (result.status !== 0)
53
+ return { ok: false, reason: "CIM failed" };
54
+ try {
55
+ const parsed = JSON.parse(String(result.stdout || ""));
56
+ if (!parsed.CommandLine || !parsed.CreationDate)
57
+ return { ok: false, reason: "CIM metadata incomplete" };
58
+ return { ok: true, command: parsed.CommandLine, startToken: new Date(parsed.CreationDate).toISOString() };
59
+ }
60
+ catch {
61
+ return { ok: false, reason: "CIM output invalid" };
62
+ }
63
+ }
64
+ function inspectProcess(pid) {
65
+ const value = validPid(pid);
66
+ if (!value)
67
+ return { ok: false, reason: "invalid pid" };
68
+ if (!pidAlive(value))
69
+ return { ok: false, dead: true, reason: "process not alive" };
70
+ return process.platform === "win32" ? inspectWindows(value) : inspectUnix(value);
71
+ }
72
+ function currentProcessMetadata(commandToken) {
73
+ const inspected = inspectProcess(process.pid);
74
+ if (!inspected.ok || !inspected.command || !inspected.startToken) {
75
+ throw new Error(`无法读取当前进程身份:${inspected.reason || "metadata incomplete"}`);
76
+ }
77
+ let effectiveToken = commandToken;
78
+ if (!inspected.command.includes(commandToken)) {
79
+ const bunToken = node_path_1.default.basename(process.execPath);
80
+ if (process.env.LARKIN_BUN_TEST_RUNNER === "1" && inspected.command.includes(bunToken))
81
+ effectiveToken = bunToken;
82
+ else if (process.env.LARKIN_BUN_TEST_RUNNER === "1" && /(^|[\s/])bun(?:\s|$)/.test(inspected.command))
83
+ effectiveToken = "bun";
84
+ else
85
+ throw new Error(`当前进程命令不含身份标记 ${commandToken}`);
86
+ }
87
+ return { pid: process.pid, processStartToken: inspected.startToken, commandToken: effectiveToken };
88
+ }
89
+ module.exports = { validPid, pidAlive, inspectProcess, currentProcessMetadata };
@@ -0,0 +1,237 @@
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.currentProcessMetadata = exports.inspectProcess = exports.pidAlive = void 0;
7
+ exports.readJson = readJson;
8
+ exports.readOwnedProcessRecord = readOwnedProcessRecord;
9
+ exports.readProcessState = readProcessState;
10
+ exports.daemonHasAgent = daemonHasAgent;
11
+ exports.assertProcessCanStart = assertProcessCanStart;
12
+ exports.terminateOwnedProcess = terminateOwnedProcess;
13
+ exports.waitForProcessExit = waitForProcessExit;
14
+ exports.acquireProcessLock = acquireProcessLock;
15
+ const node_crypto_1 = __importDefault(require("node:crypto"));
16
+ const node_fs_1 = __importDefault(require("node:fs"));
17
+ const node_path_1 = __importDefault(require("node:path"));
18
+ const process_inspect_cjs_1 = __importDefault(require("./process-inspect.cjs"));
19
+ const processInspect = process_inspect_cjs_1.default;
20
+ exports.pidAlive = processInspect.pidAlive;
21
+ exports.inspectProcess = processInspect.inspectProcess;
22
+ exports.currentProcessMetadata = processInspect.currentProcessMetadata;
23
+ function readJson(file, fallback = null) {
24
+ try {
25
+ return JSON.parse(node_fs_1.default.readFileSync(file, "utf8"));
26
+ }
27
+ catch {
28
+ return fallback;
29
+ }
30
+ }
31
+ function readOwnedProcessRecord(file, defaultCommandToken, { inspect = exports.inspectProcess } = {}) {
32
+ const record = readJson(file, null);
33
+ if (!record)
34
+ return { file, state: "dead", alive: false, running: false, reason: "status missing" };
35
+ const alive = (0, exports.pidAlive)(record.pid);
36
+ if (!alive)
37
+ return { ...record, file, state: "dead", alive: false, running: false, reason: "pid not alive" };
38
+ const inspected = inspect(Number(record.pid));
39
+ if (!inspected?.ok && inspected?.dead) {
40
+ return { ...record, file, state: "dead", alive: false, running: false, reason: inspected.reason || "process exited", inspected };
41
+ }
42
+ if (!inspected?.ok) {
43
+ return {
44
+ ...record,
45
+ file,
46
+ state: "unknown",
47
+ alive: true,
48
+ running: false,
49
+ reason: inspected?.reason || "process inspection failed",
50
+ };
51
+ }
52
+ if (typeof inspected.command !== "string" || !inspected.command ||
53
+ typeof inspected.startToken !== "string" || !inspected.startToken) {
54
+ return {
55
+ ...record,
56
+ file,
57
+ state: "unknown",
58
+ alive: true,
59
+ running: false,
60
+ reason: "process inspection metadata incomplete",
61
+ inspected,
62
+ };
63
+ }
64
+ const commandToken = record.commandToken || defaultCommandToken;
65
+ if (!commandToken || !inspected.command.includes(commandToken)) {
66
+ return { ...record, file, state: "mismatch", alive: true, running: false, reason: "command mismatch", inspected };
67
+ }
68
+ if (!record.processStartToken) {
69
+ return {
70
+ ...record,
71
+ file,
72
+ state: "unknown",
73
+ alive: true,
74
+ running: false,
75
+ reason: "status lacks processStartToken",
76
+ inspected,
77
+ };
78
+ }
79
+ if (record.processStartToken !== inspected.startToken) {
80
+ return { ...record, file, state: "mismatch", alive: true, running: false, reason: "process start mismatch", inspected };
81
+ }
82
+ return { ...record, file, state: "owned", alive: true, running: true, reason: null, inspected };
83
+ }
84
+ function readProcessState(larkinHome) {
85
+ return {
86
+ supervisor: readOwnedProcessRecord(node_path_1.default.join(larkinHome, "supervisor-status.json"), "app/run.mjs"),
87
+ daemon: readOwnedProcessRecord(node_path_1.default.join(larkinHome, "daemon-status.json"), "app/runtime-process.mjs"),
88
+ dashboard: readOwnedProcessRecord(node_path_1.default.join(larkinHome, "dashboard-status.json"), "app/dashboard.mjs"),
89
+ };
90
+ }
91
+ function daemonHasAgent(daemon, agentId) {
92
+ return !!(daemon?.state === "owned" && Array.isArray(daemon.agents) && daemon.agents.includes(agentId));
93
+ }
94
+ function assertProcessCanStart(record, label) {
95
+ if (record?.state === "unknown") {
96
+ throw new Error(`${label} PID ${record.pid} 存活但身份无法确认(${record.reason});为避免重复进程或误杀,已停止`);
97
+ }
98
+ }
99
+ function terminateOwnedProcess(record, signal = "SIGTERM", { inspect = exports.inspectProcess } = {}) {
100
+ const current = readOwnedProcessRecord(record.file, record.commandToken, { inspect });
101
+ if (current.state !== "owned") {
102
+ throw new Error(`拒绝向 PID ${record.pid} 发送 ${signal}:进程所有权=${current.state}(${current.reason})`);
103
+ }
104
+ process.kill(Number(current.pid), signal);
105
+ return current;
106
+ }
107
+ async function waitForProcessExit(record, timeoutMs = 10_000, { inspect = exports.inspectProcess } = {}) {
108
+ const deadline = Date.now() + timeoutMs;
109
+ while (Date.now() < deadline) {
110
+ const current = readOwnedProcessRecord(record.file, record.commandToken, { inspect });
111
+ if (current.state === "dead" || current.state === "mismatch")
112
+ return true;
113
+ if (current.state === "unknown")
114
+ throw new Error(`等待 PID ${record.pid} 退出时无法确认所有权:${current.reason}`);
115
+ await new Promise((resolve) => setTimeout(resolve, 100));
116
+ }
117
+ return false;
118
+ }
119
+ function atomicCreateJson(file, value) {
120
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(file), { recursive: true, mode: 0o700 });
121
+ const fd = node_fs_1.default.openSync(file, "wx", 0o600);
122
+ try {
123
+ node_fs_1.default.writeFileSync(fd, `${JSON.stringify(value, null, 2)}\n`);
124
+ }
125
+ finally {
126
+ node_fs_1.default.closeSync(fd);
127
+ }
128
+ }
129
+ function tryAcquireReclaimLock(file, commandToken) {
130
+ const reclaimFile = `${file}.reclaim`;
131
+ const metadata = (0, exports.currentProcessMetadata)(commandToken);
132
+ const record = { ...metadata, nonce: node_crypto_1.default.randomUUID(), startedAt: new Date().toISOString() };
133
+ try {
134
+ atomicCreateJson(reclaimFile, record);
135
+ }
136
+ catch (error) {
137
+ if (error.code !== "EEXIST")
138
+ throw error;
139
+ const existing = readOwnedProcessRecord(reclaimFile, commandToken);
140
+ if (existing.state === "owned" || existing.state === "unknown")
141
+ return null;
142
+ node_fs_1.default.rmSync(reclaimFile, { force: true });
143
+ try {
144
+ atomicCreateJson(reclaimFile, record);
145
+ }
146
+ catch (retryError) {
147
+ if (retryError.code === "EEXIST")
148
+ return null;
149
+ throw retryError;
150
+ }
151
+ }
152
+ return () => {
153
+ const current = readJson(reclaimFile, null);
154
+ if (current?.nonce === record.nonce && current?.pid === process.pid)
155
+ node_fs_1.default.rmSync(reclaimFile, { force: true });
156
+ };
157
+ }
158
+ function acquireProcessLock(file, commandToken, { malformedGraceMs = 5_000 } = {}) {
159
+ const nonce = node_crypto_1.default.randomUUID();
160
+ const metadata = (0, exports.currentProcessMetadata)(commandToken);
161
+ const record = { ...metadata, nonce, startedAt: new Date().toISOString() };
162
+ let created = false;
163
+ for (let attempt = 0; attempt < 8; attempt += 1) {
164
+ try {
165
+ atomicCreateJson(file, record);
166
+ created = true;
167
+ break;
168
+ }
169
+ catch (error) {
170
+ if (error.code !== "EEXIST")
171
+ throw error;
172
+ let existing = null;
173
+ let malformedAge = null;
174
+ try {
175
+ existing = JSON.parse(node_fs_1.default.readFileSync(file, "utf8"));
176
+ }
177
+ catch {
178
+ try {
179
+ malformedAge = Date.now() - node_fs_1.default.statSync(file).mtimeMs;
180
+ }
181
+ catch (statError) {
182
+ if (statError.code === "ENOENT")
183
+ continue;
184
+ throw statError;
185
+ }
186
+ }
187
+ if (malformedAge !== null && malformedAge < malformedGraceMs)
188
+ throw new Error(`lock 正在创建,暂不能接管:${file}`);
189
+ if (existing) {
190
+ const state = readOwnedProcessRecord(file, existing.commandToken || commandToken);
191
+ if (state.state === "owned" || state.state === "unknown") {
192
+ throw new Error(`lock 已被正在运行的进程占用:${file} pid=${existing.pid} state=${state.state}`);
193
+ }
194
+ }
195
+ const releaseReclaim = tryAcquireReclaimLock(file, commandToken);
196
+ if (!releaseReclaim)
197
+ continue;
198
+ try {
199
+ let latest = null;
200
+ try {
201
+ latest = JSON.parse(node_fs_1.default.readFileSync(file, "utf8"));
202
+ }
203
+ catch {
204
+ let age;
205
+ try {
206
+ age = Date.now() - node_fs_1.default.statSync(file).mtimeMs;
207
+ }
208
+ catch (statError) {
209
+ if (statError.code === "ENOENT")
210
+ continue;
211
+ throw statError;
212
+ }
213
+ if (age < malformedGraceMs)
214
+ throw new Error(`lock 正在创建,暂不能接管:${file}`);
215
+ }
216
+ if (latest) {
217
+ const latestState = readOwnedProcessRecord(file, latest.commandToken || commandToken);
218
+ if (latestState.state === "owned" || latestState.state === "unknown") {
219
+ throw new Error(`lock 已被正在运行的进程占用:${file} pid=${latest.pid} state=${latestState.state}`);
220
+ }
221
+ }
222
+ node_fs_1.default.rmSync(file, { force: true });
223
+ }
224
+ finally {
225
+ releaseReclaim();
226
+ }
227
+ }
228
+ }
229
+ if (!created)
230
+ throw new Error(`无法取得 lock(并发重试耗尽):${file}`);
231
+ const release = () => {
232
+ const current = readJson(file, null);
233
+ if (current?.nonce === nonce && current?.pid === process.pid)
234
+ node_fs_1.default.rmSync(file, { force: true });
235
+ };
236
+ return { file, record, release };
237
+ }
@@ -0,0 +1,223 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import processInspectImport from "./process-inspect.cjs";
5
+ const processInspect = processInspectImport;
6
+ export const pidAlive = processInspect.pidAlive;
7
+ export const inspectProcess = processInspect.inspectProcess;
8
+ export const currentProcessMetadata = processInspect.currentProcessMetadata;
9
+ export function readJson(file, fallback = null) {
10
+ try {
11
+ return JSON.parse(fs.readFileSync(file, "utf8"));
12
+ }
13
+ catch {
14
+ return fallback;
15
+ }
16
+ }
17
+ export function readOwnedProcessRecord(file, defaultCommandToken, { inspect = inspectProcess } = {}) {
18
+ const record = readJson(file, null);
19
+ if (!record)
20
+ return { file, state: "dead", alive: false, running: false, reason: "status missing" };
21
+ const alive = pidAlive(record.pid);
22
+ if (!alive)
23
+ return { ...record, file, state: "dead", alive: false, running: false, reason: "pid not alive" };
24
+ const inspected = inspect(Number(record.pid));
25
+ if (!inspected?.ok && inspected?.dead) {
26
+ return { ...record, file, state: "dead", alive: false, running: false, reason: inspected.reason || "process exited", inspected };
27
+ }
28
+ if (!inspected?.ok) {
29
+ return {
30
+ ...record,
31
+ file,
32
+ state: "unknown",
33
+ alive: true,
34
+ running: false,
35
+ reason: inspected?.reason || "process inspection failed",
36
+ };
37
+ }
38
+ if (typeof inspected.command !== "string" || !inspected.command ||
39
+ typeof inspected.startToken !== "string" || !inspected.startToken) {
40
+ return {
41
+ ...record,
42
+ file,
43
+ state: "unknown",
44
+ alive: true,
45
+ running: false,
46
+ reason: "process inspection metadata incomplete",
47
+ inspected,
48
+ };
49
+ }
50
+ const commandToken = record.commandToken || defaultCommandToken;
51
+ if (!commandToken || !inspected.command.includes(commandToken)) {
52
+ return { ...record, file, state: "mismatch", alive: true, running: false, reason: "command mismatch", inspected };
53
+ }
54
+ if (!record.processStartToken) {
55
+ return {
56
+ ...record,
57
+ file,
58
+ state: "unknown",
59
+ alive: true,
60
+ running: false,
61
+ reason: "status lacks processStartToken",
62
+ inspected,
63
+ };
64
+ }
65
+ if (record.processStartToken !== inspected.startToken) {
66
+ return { ...record, file, state: "mismatch", alive: true, running: false, reason: "process start mismatch", inspected };
67
+ }
68
+ return { ...record, file, state: "owned", alive: true, running: true, reason: null, inspected };
69
+ }
70
+ export function readProcessState(larkinHome) {
71
+ return {
72
+ supervisor: readOwnedProcessRecord(path.join(larkinHome, "supervisor-status.json"), "app/run.mjs"),
73
+ daemon: readOwnedProcessRecord(path.join(larkinHome, "daemon-status.json"), "app/runtime-process.mjs"),
74
+ dashboard: readOwnedProcessRecord(path.join(larkinHome, "dashboard-status.json"), "app/dashboard.mjs"),
75
+ };
76
+ }
77
+ export function daemonHasAgent(daemon, agentId) {
78
+ return !!(daemon?.state === "owned" && Array.isArray(daemon.agents) && daemon.agents.includes(agentId));
79
+ }
80
+ export function assertProcessCanStart(record, label) {
81
+ if (record?.state === "unknown") {
82
+ throw new Error(`${label} PID ${record.pid} 存活但身份无法确认(${record.reason});为避免重复进程或误杀,已停止`);
83
+ }
84
+ }
85
+ export function terminateOwnedProcess(record, signal = "SIGTERM", { inspect = inspectProcess } = {}) {
86
+ const current = readOwnedProcessRecord(record.file, record.commandToken, { inspect });
87
+ if (current.state !== "owned") {
88
+ throw new Error(`拒绝向 PID ${record.pid} 发送 ${signal}:进程所有权=${current.state}(${current.reason})`);
89
+ }
90
+ process.kill(Number(current.pid), signal);
91
+ return current;
92
+ }
93
+ export async function waitForProcessExit(record, timeoutMs = 10_000, { inspect = inspectProcess } = {}) {
94
+ const deadline = Date.now() + timeoutMs;
95
+ while (Date.now() < deadline) {
96
+ const current = readOwnedProcessRecord(record.file, record.commandToken, { inspect });
97
+ if (current.state === "dead" || current.state === "mismatch")
98
+ return true;
99
+ if (current.state === "unknown")
100
+ throw new Error(`等待 PID ${record.pid} 退出时无法确认所有权:${current.reason}`);
101
+ await new Promise((resolve) => setTimeout(resolve, 100));
102
+ }
103
+ return false;
104
+ }
105
+ function atomicCreateJson(file, value) {
106
+ fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
107
+ const fd = fs.openSync(file, "wx", 0o600);
108
+ try {
109
+ fs.writeFileSync(fd, `${JSON.stringify(value, null, 2)}\n`);
110
+ }
111
+ finally {
112
+ fs.closeSync(fd);
113
+ }
114
+ }
115
+ function tryAcquireReclaimLock(file, commandToken) {
116
+ const reclaimFile = `${file}.reclaim`;
117
+ const metadata = currentProcessMetadata(commandToken);
118
+ const record = { ...metadata, nonce: crypto.randomUUID(), startedAt: new Date().toISOString() };
119
+ try {
120
+ atomicCreateJson(reclaimFile, record);
121
+ }
122
+ catch (error) {
123
+ if (error.code !== "EEXIST")
124
+ throw error;
125
+ const existing = readOwnedProcessRecord(reclaimFile, commandToken);
126
+ if (existing.state === "owned" || existing.state === "unknown")
127
+ return null;
128
+ fs.rmSync(reclaimFile, { force: true });
129
+ try {
130
+ atomicCreateJson(reclaimFile, record);
131
+ }
132
+ catch (retryError) {
133
+ if (retryError.code === "EEXIST")
134
+ return null;
135
+ throw retryError;
136
+ }
137
+ }
138
+ return () => {
139
+ const current = readJson(reclaimFile, null);
140
+ if (current?.nonce === record.nonce && current?.pid === process.pid)
141
+ fs.rmSync(reclaimFile, { force: true });
142
+ };
143
+ }
144
+ export function acquireProcessLock(file, commandToken, { malformedGraceMs = 5_000 } = {}) {
145
+ const nonce = crypto.randomUUID();
146
+ const metadata = currentProcessMetadata(commandToken);
147
+ const record = { ...metadata, nonce, startedAt: new Date().toISOString() };
148
+ let created = false;
149
+ for (let attempt = 0; attempt < 8; attempt += 1) {
150
+ try {
151
+ atomicCreateJson(file, record);
152
+ created = true;
153
+ break;
154
+ }
155
+ catch (error) {
156
+ if (error.code !== "EEXIST")
157
+ throw error;
158
+ let existing = null;
159
+ let malformedAge = null;
160
+ try {
161
+ existing = JSON.parse(fs.readFileSync(file, "utf8"));
162
+ }
163
+ catch {
164
+ try {
165
+ malformedAge = Date.now() - fs.statSync(file).mtimeMs;
166
+ }
167
+ catch (statError) {
168
+ if (statError.code === "ENOENT")
169
+ continue;
170
+ throw statError;
171
+ }
172
+ }
173
+ if (malformedAge !== null && malformedAge < malformedGraceMs)
174
+ throw new Error(`lock 正在创建,暂不能接管:${file}`);
175
+ if (existing) {
176
+ const state = readOwnedProcessRecord(file, existing.commandToken || commandToken);
177
+ if (state.state === "owned" || state.state === "unknown") {
178
+ throw new Error(`lock 已被正在运行的进程占用:${file} pid=${existing.pid} state=${state.state}`);
179
+ }
180
+ }
181
+ const releaseReclaim = tryAcquireReclaimLock(file, commandToken);
182
+ if (!releaseReclaim)
183
+ continue;
184
+ try {
185
+ let latest = null;
186
+ try {
187
+ latest = JSON.parse(fs.readFileSync(file, "utf8"));
188
+ }
189
+ catch {
190
+ let age;
191
+ try {
192
+ age = Date.now() - fs.statSync(file).mtimeMs;
193
+ }
194
+ catch (statError) {
195
+ if (statError.code === "ENOENT")
196
+ continue;
197
+ throw statError;
198
+ }
199
+ if (age < malformedGraceMs)
200
+ throw new Error(`lock 正在创建,暂不能接管:${file}`);
201
+ }
202
+ if (latest) {
203
+ const latestState = readOwnedProcessRecord(file, latest.commandToken || commandToken);
204
+ if (latestState.state === "owned" || latestState.state === "unknown") {
205
+ throw new Error(`lock 已被正在运行的进程占用:${file} pid=${latest.pid} state=${latestState.state}`);
206
+ }
207
+ }
208
+ fs.rmSync(file, { force: true });
209
+ }
210
+ finally {
211
+ releaseReclaim();
212
+ }
213
+ }
214
+ }
215
+ if (!created)
216
+ throw new Error(`无法取得 lock(并发重试耗尽):${file}`);
217
+ const release = () => {
218
+ const current = readJson(file, null);
219
+ if (current?.nonce === nonce && current?.pid === process.pid)
220
+ fs.rmSync(file, { force: true });
221
+ };
222
+ return { file, record, release };
223
+ }
@@ -0,0 +1,61 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ export const RELEASE_TARGETS = Object.freeze([
5
+ { platform: "darwin", arch: "arm64", bunTarget: "bun-darwin-arm64" },
6
+ { platform: "darwin", arch: "x64", bunTarget: "bun-darwin-x64-baseline" },
7
+ { platform: "linux", arch: "arm64", bunTarget: "bun-linux-arm64" },
8
+ { platform: "linux", arch: "x64", bunTarget: "bun-linux-x64-baseline" },
9
+ ]);
10
+ export function artifactFilename(version, platform, arch) {
11
+ if (!/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(version))
12
+ throw new Error(`invalid release version: ${version}`);
13
+ if (!RELEASE_TARGETS.some((target) => target.platform === platform && target.arch === arch)) {
14
+ throw new Error(`unsupported release target: ${platform}-${arch}`);
15
+ }
16
+ return `larkin-v${version}-${platform}-${arch}`;
17
+ }
18
+ export function sha256File(file) {
19
+ return crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex");
20
+ }
21
+ export function selectReleaseArtifact(manifest, platform, arch) {
22
+ const record = manifest.artifacts.find((candidate) => candidate.platform === platform && candidate.arch === arch);
23
+ if (!record || !RELEASE_TARGETS.some((target) => target.platform === platform && target.arch === arch)) {
24
+ throw new Error(`unsupported platform: ${platform}-${arch}`);
25
+ }
26
+ if (record.file !== artifactFilename(manifest.version, record.platform, record.arch) || !/^[a-f0-9]{64}$/.test(record.sha256)) {
27
+ throw new Error(`invalid artifact manifest entry: ${record.file}`);
28
+ }
29
+ return record;
30
+ }
31
+ export function verifyReleaseArtifact(directory, record) {
32
+ const root = path.resolve(directory);
33
+ const file = path.resolve(root, record.file);
34
+ if (path.dirname(file) !== root)
35
+ throw new Error("artifact path escaped release directory");
36
+ const stat = fs.lstatSync(file);
37
+ if (!stat.isFile() || !Number.isSafeInteger(record.size) || record.size <= 0 || stat.size !== record.size) {
38
+ throw new Error(`invalid artifact size for ${record.file}`);
39
+ }
40
+ const actual = sha256File(file);
41
+ if (actual !== record.sha256)
42
+ throw new Error(`checksum mismatch for ${record.file}`);
43
+ return file;
44
+ }
45
+ export function verifyReleaseNotices(directory, manifest) {
46
+ const record = manifest.notices;
47
+ if (!record || record.file !== "THIRD_PARTY_NOTICES.txt" || record.scope !== "runtime-closure" || !/^[a-f0-9]{64}$/.test(record.sha256)) {
48
+ throw new Error("invalid runtime notices manifest entry");
49
+ }
50
+ const root = path.resolve(directory);
51
+ const file = path.resolve(root, record.file);
52
+ if (path.dirname(file) !== root)
53
+ throw new Error("runtime notices path escaped release directory");
54
+ const stat = fs.lstatSync(file);
55
+ if (!stat.isFile() || !Number.isSafeInteger(record.size) || record.size <= 0 || stat.size !== record.size) {
56
+ throw new Error("invalid runtime notices size");
57
+ }
58
+ if (sha256File(file) !== record.sha256)
59
+ throw new Error("runtime notices checksum mismatch");
60
+ return file;
61
+ }