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.
- package/CONTRIBUTING.md +48 -0
- package/LICENSE +201 -0
- package/README.md +122 -0
- package/SECURITY.md +14 -0
- package/artifacts/release/THIRD_PARTY_NOTICES.txt +5488 -0
- package/dist/agent/agent-cli-capabilities.cjs +43 -0
- package/dist/agent/agent-cli-capabilities.mjs +35 -0
- package/dist/agent/agent-state-store.cjs +961 -0
- package/dist/agent/agent-state-store.mjs +923 -0
- package/dist/agent/agent-transport.cjs +201 -0
- package/dist/agent/agent-transport.mjs +194 -0
- package/dist/agent/config-cli-contract.cjs +19 -0
- package/dist/agent/config-cli-contract.mjs +16 -0
- package/dist/agent/context-prompt.cjs +159 -0
- package/dist/agent/context-prompt.mjs +155 -0
- package/dist/agent/freshness-gate.mjs +24 -0
- package/dist/agent/history-projection.cjs +72 -0
- package/dist/agent/history-projection.mjs +65 -0
- package/dist/agent/host-reminder-orchestrator.cjs +289 -0
- package/dist/agent/host-reminder-orchestrator.mjs +252 -0
- package/dist/agent/inbox-projection.cjs +92 -0
- package/dist/agent/inbox-projection.mjs +85 -0
- package/dist/agent/interaction-state-machine.cjs +710 -0
- package/dist/agent/interaction-state-machine.mjs +705 -0
- package/dist/agent/reminder-routes.cjs +192 -0
- package/dist/agent/reminder-routes.mjs +156 -0
- package/dist/agent/reminder-store.cjs +220 -0
- package/dist/agent/reminder-store.mjs +205 -0
- package/dist/agent/transport-business-context.cjs +486 -0
- package/dist/agent/transport-business-context.mjs +446 -0
- package/dist/agent/transport-shell.cjs +45 -0
- package/dist/agent/transport-shell.mjs +39 -0
- package/dist/app/agent-cli.mjs +596 -0
- package/dist/app/agent-config.mjs +633 -0
- package/dist/app/agent-lark-cli-workspace.cjs +95 -0
- package/dist/app/agent-lark-cli-workspace.mjs +85 -0
- package/dist/app/agent-readiness.cjs +42 -0
- package/dist/app/agent-readiness.mjs +38 -0
- package/dist/app/binary-entry.mjs +86 -0
- package/dist/app/cli.mjs +176 -0
- package/dist/app/dashboard.mjs +205 -0
- package/dist/app/internal-command.cjs +57 -0
- package/dist/app/internal-command.mjs +48 -0
- package/dist/app/lark-channel-secret.mjs +22 -0
- package/dist/app/lark-cli.mjs +887 -0
- package/dist/app/lark.mjs +45 -0
- package/dist/app/local-control.mjs +728 -0
- package/dist/app/official-lark-cli.cjs +138 -0
- package/dist/app/official-lark-cli.mjs +128 -0
- package/dist/app/pi-auth-cli.mjs +67 -0
- package/dist/app/run.mjs +364 -0
- package/dist/app/runtime-agent-config.mjs +416 -0
- package/dist/app/runtime-model-directory.mjs +78 -0
- package/dist/app/runtime-process.mjs +113 -0
- package/dist/app/session-cli.mjs +83 -0
- package/dist/app/setup-dashboard.mjs +26 -0
- package/dist/app/setup.mjs +166 -0
- package/dist/app/telemetry.mjs +83 -0
- package/dist/dashboard/dashboard-assets.mjs +21 -0
- package/dist/dashboard/dashboard-avatar.mjs +128 -0
- package/dist/dashboard/dashboard-config-controller.mjs +525 -0
- package/dist/dashboard/dashboard-lifecycle.cjs +97 -0
- package/dist/dashboard/dashboard-lifecycle.mjs +88 -0
- package/dist/dashboard/dashboard-template.mjs +29 -0
- package/dist/dashboard/dashboard-view-model.mjs +556 -0
- package/dist/dashboard/dashboard-workspace.mjs +104 -0
- package/dist/dashboard/web/assets/dashboard.css +3 -0
- package/dist/dashboard/web/assets/dashboard.js +49 -0
- package/dist/feishu/channel-transport.cjs +151 -0
- package/dist/feishu/channel-transport.mjs +148 -0
- package/dist/feishu/document-comment.cjs +120 -0
- package/dist/feishu/document-comment.mjs +109 -0
- package/dist/feishu/host-business-state.cjs +477 -0
- package/dist/feishu/host-business-state.mjs +462 -0
- package/dist/feishu/host-channel-business.cjs +161 -0
- package/dist/feishu/host-channel-business.mjs +157 -0
- package/dist/feishu/host-processing-eye.cjs +185 -0
- package/dist/feishu/host-processing-eye.mjs +181 -0
- package/dist/feishu/host-shell.cjs +1425 -0
- package/dist/feishu/host-shell.mjs +1385 -0
- package/dist/feishu/im-freshness-adapter.mjs +89 -0
- package/dist/feishu/interaction-orchestrator.cjs +279 -0
- package/dist/feishu/interaction-orchestrator.mjs +242 -0
- package/dist/feishu/lark-passthrough.mjs +70 -0
- package/dist/feishu/message-policy.cjs +111 -0
- package/dist/feishu/message-policy.mjs +98 -0
- package/dist/feishu/outbound-transport.cjs +221 -0
- package/dist/feishu/outbound-transport.mjs +215 -0
- package/dist/platform/build-info.cjs +25 -0
- package/dist/platform/build-info.mjs +18 -0
- package/dist/platform/callback-capability.cjs +301 -0
- package/dist/platform/callback-capability.mjs +252 -0
- package/dist/platform/check-bun-version.cjs +12 -0
- package/dist/platform/config.cjs +751 -0
- package/dist/platform/config.mjs +694 -0
- package/dist/platform/process-inspect.cjs +89 -0
- package/dist/platform/process-state.cjs +237 -0
- package/dist/platform/process-state.mjs +223 -0
- package/dist/platform/release-artifacts.mjs +61 -0
- package/dist/platform/root-layout.cjs +111 -0
- package/dist/platform/root-layout.mjs +72 -0
- package/dist/platform/telemetry-config.cjs +64 -0
- package/dist/platform/telemetry-config.mjs +56 -0
- package/dist/platform/telemetry-spool.cjs +661 -0
- package/dist/platform/telemetry-spool.mjs +653 -0
- package/dist/platform/telemetry-tracing.cjs +602 -0
- package/dist/platform/telemetry-tracing.mjs +596 -0
- package/dist/platform/telemetry-uploader.cjs +150 -0
- package/dist/platform/telemetry-uploader.mjs +146 -0
- package/dist/platform/workspace-service.cjs +469 -0
- package/dist/platform/workspace-service.mjs +430 -0
- package/dist/runtime/builtin-pi-assets.mjs +33 -0
- package/dist/runtime/claude-model-catalog.cjs +102 -0
- package/dist/runtime/claude-model-catalog.mjs +99 -0
- package/dist/runtime/codex-model-catalog.cjs +108 -0
- package/dist/runtime/codex-model-catalog.mjs +105 -0
- package/dist/runtime/pi-bash-timeout-extension.mjs +60 -0
- package/dist/runtime/pi-bash-timeout-injection.mjs +60 -0
- package/dist/runtime/pi-bash-timeout.bundle.js +43 -0
- package/dist/runtime/pi-model-catalog.cjs +116 -0
- package/dist/runtime/pi-model-catalog.mjs +75 -0
- package/dist/runtime/pi-official-auth.mjs +480 -0
- package/dist/runtime/pi-provider-config.cjs +252 -0
- package/dist/runtime/pi-provider-config.mjs +239 -0
- package/dist/runtime/pi-rpc-client.cjs +255 -0
- package/dist/runtime/pi-rpc-client.mjs +251 -0
- package/dist/runtime/pi-subagent-injection.mjs +129 -0
- package/dist/runtime/pi-subagents.bundle.js +8990 -0
- package/dist/runtime/runtime-adapters.mjs +1019 -0
- package/dist/runtime/runtime-contracts.cjs +2 -0
- package/dist/runtime/runtime-contracts.mjs +1 -0
- package/dist/runtime/runtime-host.cjs +930 -0
- package/dist/runtime/runtime-host.mjs +922 -0
- package/dist/runtime/runtime-model-catalog.cjs +43 -0
- package/dist/runtime/runtime-model-catalog.mjs +40 -0
- package/dist/runtime/runtime-readiness.cjs +177 -0
- package/dist/runtime/runtime-readiness.mjs +133 -0
- package/dist/setup/bot-register.mjs +655 -0
- package/dist/setup/grant-scopes.mjs +133 -0
- package/dist/setup/run-credential-preflight.mjs +89 -0
- package/dist/setup/setup-agent-choice.mjs +172 -0
- package/dist/setup/setup-bind.mjs +337 -0
- package/dist/setup/setup-binding.mjs +52 -0
- package/package.json +112 -0
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.reconcileAgentWorkspace = reconcileAgentWorkspace;
|
|
40
|
+
const crypto = __importStar(require("node:crypto"));
|
|
41
|
+
const fs = __importStar(require("node:fs"));
|
|
42
|
+
const path = __importStar(require("node:path"));
|
|
43
|
+
const process_inspect_cjs_1 = __importDefault(require("./process-inspect.cjs"));
|
|
44
|
+
const START = "<!-- larkin:platform-rules:start -->";
|
|
45
|
+
const END = "<!-- larkin:platform-rules:end -->";
|
|
46
|
+
const START_BYTES = Buffer.from(START, "ascii");
|
|
47
|
+
const END_BYTES = Buffer.from(END, "ascii");
|
|
48
|
+
const PROMPT_FILES = ["AGENTS.md", "CLAUDE.md"];
|
|
49
|
+
const AGENT_ID = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$/;
|
|
50
|
+
const NOFOLLOW = fs.constants.O_NOFOLLOW || 0;
|
|
51
|
+
const LOCK_FILE = "workspace-reconcile.lock.json";
|
|
52
|
+
const LOCK_RETRY_ATTEMPTS = 100;
|
|
53
|
+
const LOCK_RETRY_MS = 20;
|
|
54
|
+
const MALFORMED_LOCK_GRACE_MS = 1_000;
|
|
55
|
+
function isMissing(error) {
|
|
56
|
+
return !!error && typeof error === "object" && "code" in error && error.code === "ENOENT";
|
|
57
|
+
}
|
|
58
|
+
function identity(stat) {
|
|
59
|
+
return { dev: stat.dev, ino: stat.ino };
|
|
60
|
+
}
|
|
61
|
+
function sameIdentity(left, right) {
|
|
62
|
+
return left.dev === right.dev && left.ino === right.ino;
|
|
63
|
+
}
|
|
64
|
+
function inside(root, candidate) {
|
|
65
|
+
const relative = path.relative(root, candidate);
|
|
66
|
+
return relative !== "" && !relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative);
|
|
67
|
+
}
|
|
68
|
+
function occurrences(content, marker) {
|
|
69
|
+
const found = [];
|
|
70
|
+
for (let at = content.indexOf(marker); at >= 0; at = content.indexOf(marker, at + marker.length))
|
|
71
|
+
found.push(at);
|
|
72
|
+
return found;
|
|
73
|
+
}
|
|
74
|
+
function promptMigration(content, file) {
|
|
75
|
+
const starts = occurrences(content, START_BYTES);
|
|
76
|
+
const ends = occurrences(content, END_BYTES);
|
|
77
|
+
if (starts.length === 0 && ends.length === 0)
|
|
78
|
+
return { next: content, spans: [] };
|
|
79
|
+
if (starts.length !== 1 || ends.length !== 1 || starts[0] > ends[0]) {
|
|
80
|
+
throw new Error(`managed prompt markers are malformed or duplicate: ${file}`);
|
|
81
|
+
}
|
|
82
|
+
const start = starts[0];
|
|
83
|
+
const length = ends[0] + END_BYTES.length - start;
|
|
84
|
+
const next = Buffer.from(content);
|
|
85
|
+
next.fill(0x20, start, start + length);
|
|
86
|
+
return { next, spans: [{ start, length }] };
|
|
87
|
+
}
|
|
88
|
+
function readStableFd(fd, label) {
|
|
89
|
+
for (let attempt = 0; attempt < 4; attempt += 1) {
|
|
90
|
+
const before = fs.fstatSync(fd);
|
|
91
|
+
if (!before.isFile())
|
|
92
|
+
throw new Error(`prompt is no longer a regular file: ${label}`);
|
|
93
|
+
const size = before.size;
|
|
94
|
+
const content = Buffer.alloc(size);
|
|
95
|
+
let offset = 0;
|
|
96
|
+
while (offset < size) {
|
|
97
|
+
const read = fs.readSync(fd, content, offset, size - offset, offset);
|
|
98
|
+
if (read === 0)
|
|
99
|
+
break;
|
|
100
|
+
offset += read;
|
|
101
|
+
}
|
|
102
|
+
const after = fs.fstatSync(fd);
|
|
103
|
+
if (offset === size && after.size === size && after.mtimeMs === before.mtimeMs && after.ctimeMs === before.ctimeMs) {
|
|
104
|
+
return content;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
throw new Error(`owner prompt remained under sustained concurrent writes: ${label}`);
|
|
108
|
+
}
|
|
109
|
+
function promptPlan(file) {
|
|
110
|
+
let stat;
|
|
111
|
+
try {
|
|
112
|
+
stat = fs.lstatSync(file);
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
if (isMissing(error))
|
|
116
|
+
return null;
|
|
117
|
+
throw error;
|
|
118
|
+
}
|
|
119
|
+
if (stat.isSymbolicLink())
|
|
120
|
+
throw new Error(`unsafe prompt symlink: ${file}`);
|
|
121
|
+
if (!stat.isFile())
|
|
122
|
+
throw new Error(`prompt path is not a regular file: ${file}`);
|
|
123
|
+
const fd = fs.openSync(file, fs.constants.O_RDONLY | NOFOLLOW);
|
|
124
|
+
try {
|
|
125
|
+
const opened = fs.fstatSync(fd);
|
|
126
|
+
if (!opened.isFile() || !sameIdentity(identity(opened), identity(stat))) {
|
|
127
|
+
throw new Error(`prompt changed while opening: ${file}`);
|
|
128
|
+
}
|
|
129
|
+
const current = readStableFd(fd, file);
|
|
130
|
+
const migration = promptMigration(current, file);
|
|
131
|
+
return { file, current, ...migration, mode: opened.mode & 0o777, identity: identity(opened), fd };
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
fs.closeSync(fd);
|
|
135
|
+
throw error;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function assertPromptUnchanged(plan) {
|
|
139
|
+
let stat;
|
|
140
|
+
try {
|
|
141
|
+
stat = fs.lstatSync(plan.file);
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
throw new Error(`owner prompt changed concurrently: ${plan.file}`);
|
|
145
|
+
}
|
|
146
|
+
if (stat.isSymbolicLink() || !stat.isFile() ||
|
|
147
|
+
!sameIdentity(identity(stat), plan.identity) || (stat.mode & 0o777) !== plan.mode) {
|
|
148
|
+
throw new Error(`owner prompt inode or mode changed concurrently: ${plan.file}`);
|
|
149
|
+
}
|
|
150
|
+
const opened = fs.fstatSync(plan.fd);
|
|
151
|
+
if (!opened.isFile() || !sameIdentity(identity(opened), plan.identity) || (opened.mode & 0o777) !== plan.mode) {
|
|
152
|
+
throw new Error(`owner prompt descriptor changed concurrently: ${plan.file}`);
|
|
153
|
+
}
|
|
154
|
+
const current = readStableFd(plan.fd, plan.file);
|
|
155
|
+
if (!current.equals(plan.current))
|
|
156
|
+
throw new Error(`owner prompt content changed concurrently: ${plan.file}`);
|
|
157
|
+
}
|
|
158
|
+
function directoryIdentity(file, label) {
|
|
159
|
+
const stat = fs.lstatSync(file);
|
|
160
|
+
if (stat.isSymbolicLink() || !stat.isDirectory())
|
|
161
|
+
throw new Error(`${label} is no longer a stable directory: ${file}`);
|
|
162
|
+
return identity(stat);
|
|
163
|
+
}
|
|
164
|
+
function assertDirectoryIdentity(file, expected, label) {
|
|
165
|
+
let current;
|
|
166
|
+
try {
|
|
167
|
+
current = directoryIdentity(file, label);
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
throw new Error(`${label} changed during workspace reconciliation: ${file}`);
|
|
171
|
+
}
|
|
172
|
+
if (!sameIdentity(current, expected))
|
|
173
|
+
throw new Error(`${label} inode changed during workspace reconciliation: ${file}`);
|
|
174
|
+
}
|
|
175
|
+
function closePromptPlan(plan) {
|
|
176
|
+
try {
|
|
177
|
+
fs.closeSync(plan.fd);
|
|
178
|
+
}
|
|
179
|
+
catch { /* already closed during cleanup */ }
|
|
180
|
+
}
|
|
181
|
+
function writeManagedSpans(plan, beforeWrite) {
|
|
182
|
+
assertPromptUnchanged(plan);
|
|
183
|
+
const fd = fs.openSync(plan.file, fs.constants.O_RDWR | NOFOLLOW);
|
|
184
|
+
try {
|
|
185
|
+
const opened = fs.fstatSync(fd);
|
|
186
|
+
if (!opened.isFile() || !sameIdentity(identity(opened), plan.identity) || (opened.mode & 0o777) !== plan.mode) {
|
|
187
|
+
throw new Error(`owner prompt changed while opening for migration: ${plan.file}`);
|
|
188
|
+
}
|
|
189
|
+
beforeWrite?.(plan.file);
|
|
190
|
+
assertPromptUnchanged(plan);
|
|
191
|
+
const current = readStableFd(fd, plan.file);
|
|
192
|
+
if (!current.equals(plan.current))
|
|
193
|
+
throw new Error(`owner prompt content changed concurrently: ${plan.file}`);
|
|
194
|
+
for (const span of plan.spans) {
|
|
195
|
+
const blank = Buffer.alloc(span.length, 0x20);
|
|
196
|
+
let offset = 0;
|
|
197
|
+
while (offset < blank.length) {
|
|
198
|
+
const written = fs.writeSync(fd, blank, offset, blank.length - offset, span.start + offset);
|
|
199
|
+
if (written === 0)
|
|
200
|
+
throw new Error(`managed prompt migration made no write progress: ${plan.file}`);
|
|
201
|
+
offset += written;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
fs.fsyncSync(fd);
|
|
205
|
+
const after = fs.fstatSync(fd);
|
|
206
|
+
const canonical = fs.lstatSync(plan.file);
|
|
207
|
+
if (!after.isFile() || !sameIdentity(identity(after), plan.identity) || (after.mode & 0o777) !== plan.mode ||
|
|
208
|
+
after.size !== plan.current.length || canonical.isSymbolicLink() || !canonical.isFile() ||
|
|
209
|
+
!sameIdentity(identity(canonical), plan.identity) || canonical.size !== plan.current.length) {
|
|
210
|
+
throw new Error(`owner prompt inode, mode, or size changed during migration: ${plan.file}`);
|
|
211
|
+
}
|
|
212
|
+
const installed = readStableFd(fd, plan.file);
|
|
213
|
+
if (!installed.equals(plan.next))
|
|
214
|
+
throw new Error(`managed prompt migration readback mismatch: ${plan.file}`);
|
|
215
|
+
}
|
|
216
|
+
finally {
|
|
217
|
+
fs.closeSync(fd);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
function sleepSync(ms) {
|
|
221
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
222
|
+
}
|
|
223
|
+
function lockRecord() {
|
|
224
|
+
const inspected = process_inspect_cjs_1.default.inspectProcess(process.pid);
|
|
225
|
+
if (!inspected?.ok || !inspected.startToken) {
|
|
226
|
+
throw new Error(`workspace reconciliation cannot establish process ownership (${inspected?.reason || "inspection failed"})`);
|
|
227
|
+
}
|
|
228
|
+
return {
|
|
229
|
+
pid: process.pid,
|
|
230
|
+
processStartToken: inspected.startToken,
|
|
231
|
+
nonce: crypto.randomUUID(),
|
|
232
|
+
startedAt: new Date().toISOString(),
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
function isLockRecord(value) {
|
|
236
|
+
if (!value || typeof value !== "object")
|
|
237
|
+
return false;
|
|
238
|
+
const record = value;
|
|
239
|
+
return Number.isInteger(record.pid) && Number(record.pid) > 0 &&
|
|
240
|
+
typeof record.processStartToken === "string" && record.processStartToken.length > 0 &&
|
|
241
|
+
typeof record.nonce === "string" && record.nonce.length > 0 &&
|
|
242
|
+
typeof record.startedAt === "string";
|
|
243
|
+
}
|
|
244
|
+
function createLockFile(file, record) {
|
|
245
|
+
let fd;
|
|
246
|
+
try {
|
|
247
|
+
fd = fs.openSync(file, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | NOFOLLOW, 0o600);
|
|
248
|
+
}
|
|
249
|
+
catch (error) {
|
|
250
|
+
if (error && typeof error === "object" && "code" in error && error.code === "EEXIST")
|
|
251
|
+
return null;
|
|
252
|
+
throw error;
|
|
253
|
+
}
|
|
254
|
+
try {
|
|
255
|
+
fs.fchmodSync(fd, 0o600);
|
|
256
|
+
fs.writeFileSync(fd, JSON.stringify(record) + "\n");
|
|
257
|
+
fs.fsyncSync(fd);
|
|
258
|
+
return identity(fs.fstatSync(fd));
|
|
259
|
+
}
|
|
260
|
+
finally {
|
|
261
|
+
fs.closeSync(fd);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
function readLockSnapshot(file) {
|
|
265
|
+
let stat;
|
|
266
|
+
try {
|
|
267
|
+
stat = fs.lstatSync(file);
|
|
268
|
+
}
|
|
269
|
+
catch (error) {
|
|
270
|
+
if (isMissing(error))
|
|
271
|
+
return null;
|
|
272
|
+
throw error;
|
|
273
|
+
}
|
|
274
|
+
if (stat.isSymbolicLink() || !stat.isFile())
|
|
275
|
+
throw new Error("workspace reconciliation lock path is unsafe");
|
|
276
|
+
let fd;
|
|
277
|
+
try {
|
|
278
|
+
fd = fs.openSync(file, fs.constants.O_RDONLY | NOFOLLOW);
|
|
279
|
+
}
|
|
280
|
+
catch (error) {
|
|
281
|
+
if (isMissing(error))
|
|
282
|
+
return null;
|
|
283
|
+
throw error;
|
|
284
|
+
}
|
|
285
|
+
try {
|
|
286
|
+
const opened = fs.fstatSync(fd);
|
|
287
|
+
if (!opened.isFile() || !sameIdentity(identity(opened), identity(stat)))
|
|
288
|
+
return null;
|
|
289
|
+
let parsed = null;
|
|
290
|
+
try {
|
|
291
|
+
parsed = JSON.parse(fs.readFileSync(fd, "utf8"));
|
|
292
|
+
}
|
|
293
|
+
catch { /* malformed/in-flight record */ }
|
|
294
|
+
return {
|
|
295
|
+
record: isLockRecord(parsed) ? parsed : null,
|
|
296
|
+
identity: identity(opened),
|
|
297
|
+
ageMs: Math.max(0, Date.now() - opened.mtimeMs),
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
finally {
|
|
301
|
+
fs.closeSync(fd);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
function lockState(snapshot) {
|
|
305
|
+
if (!snapshot.record)
|
|
306
|
+
return snapshot.ageMs >= MALFORMED_LOCK_GRACE_MS ? "stale" : "unknown";
|
|
307
|
+
if (!process_inspect_cjs_1.default.pidAlive(snapshot.record.pid))
|
|
308
|
+
return "stale";
|
|
309
|
+
const inspected = process_inspect_cjs_1.default.inspectProcess(snapshot.record.pid);
|
|
310
|
+
if (!inspected?.ok || !inspected.startToken) {
|
|
311
|
+
return process_inspect_cjs_1.default.pidAlive(snapshot.record.pid) ? "unknown" : "stale";
|
|
312
|
+
}
|
|
313
|
+
return inspected.startToken === snapshot.record.processStartToken ? "live" : "stale";
|
|
314
|
+
}
|
|
315
|
+
function unlinkIfIdentity(file, expected) {
|
|
316
|
+
let stat;
|
|
317
|
+
try {
|
|
318
|
+
stat = fs.lstatSync(file);
|
|
319
|
+
}
|
|
320
|
+
catch (error) {
|
|
321
|
+
if (isMissing(error))
|
|
322
|
+
return false;
|
|
323
|
+
throw error;
|
|
324
|
+
}
|
|
325
|
+
if (!stat.isFile() || !sameIdentity(identity(stat), expected))
|
|
326
|
+
return false;
|
|
327
|
+
fs.unlinkSync(file);
|
|
328
|
+
return true;
|
|
329
|
+
}
|
|
330
|
+
function tryAcquireReclaimGuard(file, record) {
|
|
331
|
+
let guardIdentity = createLockFile(file, record);
|
|
332
|
+
if (!guardIdentity) {
|
|
333
|
+
const existing = readLockSnapshot(file);
|
|
334
|
+
if (!existing || lockState(existing) !== "stale" || !unlinkIfIdentity(file, existing.identity))
|
|
335
|
+
return null;
|
|
336
|
+
guardIdentity = createLockFile(file, record);
|
|
337
|
+
if (!guardIdentity)
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
const ownedIdentity = guardIdentity;
|
|
341
|
+
return () => {
|
|
342
|
+
try {
|
|
343
|
+
const current = readLockSnapshot(file);
|
|
344
|
+
if (current?.record?.pid === record.pid && current.record.nonce === record.nonce &&
|
|
345
|
+
sameIdentity(current.identity, ownedIdentity))
|
|
346
|
+
unlinkIfIdentity(file, ownedIdentity);
|
|
347
|
+
}
|
|
348
|
+
catch { /* another owner or already removed */ }
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
function acquireWorkspaceLock(lockDirReal) {
|
|
352
|
+
const file = path.join(lockDirReal, LOCK_FILE);
|
|
353
|
+
const reclaimFile = path.join(lockDirReal, `${LOCK_FILE}.reclaim`);
|
|
354
|
+
const record = lockRecord();
|
|
355
|
+
let lastOwnerPid = null;
|
|
356
|
+
for (let attempt = 0; attempt < LOCK_RETRY_ATTEMPTS; attempt += 1) {
|
|
357
|
+
const ownedIdentity = createLockFile(file, record);
|
|
358
|
+
if (ownedIdentity) {
|
|
359
|
+
return () => {
|
|
360
|
+
try {
|
|
361
|
+
const current = readLockSnapshot(file);
|
|
362
|
+
if (current?.record?.pid === record.pid && current.record.nonce === record.nonce &&
|
|
363
|
+
sameIdentity(current.identity, ownedIdentity))
|
|
364
|
+
unlinkIfIdentity(file, ownedIdentity);
|
|
365
|
+
}
|
|
366
|
+
catch { /* another owner or already removed */ }
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
const observed = readLockSnapshot(file);
|
|
370
|
+
if (!observed)
|
|
371
|
+
continue;
|
|
372
|
+
lastOwnerPid = observed.record?.pid || null;
|
|
373
|
+
if (lockState(observed) === "stale") {
|
|
374
|
+
const releaseReclaim = tryAcquireReclaimGuard(reclaimFile, record);
|
|
375
|
+
if (releaseReclaim) {
|
|
376
|
+
try {
|
|
377
|
+
const latest = readLockSnapshot(file);
|
|
378
|
+
if (latest && lockState(latest) === "stale")
|
|
379
|
+
unlinkIfIdentity(file, latest.identity);
|
|
380
|
+
}
|
|
381
|
+
finally {
|
|
382
|
+
releaseReclaim();
|
|
383
|
+
}
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
sleepSync(LOCK_RETRY_MS);
|
|
388
|
+
}
|
|
389
|
+
throw new Error(`workspace reconciliation remained busy after bounded retry${lastOwnerPid ? ` (pid=${lastOwnerPid})` : ""}`);
|
|
390
|
+
}
|
|
391
|
+
function reconcileAgentWorkspace(options) {
|
|
392
|
+
const { workspaceDir, trustedWorkspaceRoot, lockDir, agentId, testHooks } = options || {};
|
|
393
|
+
if (!AGENT_ID.test(agentId || "") || agentId === "." || agentId === ".." || /[\\/]/.test(agentId || "")) {
|
|
394
|
+
throw new Error(`invalid agent id: ${JSON.stringify(agentId)}`);
|
|
395
|
+
}
|
|
396
|
+
if (!workspaceDir || !trustedWorkspaceRoot || !lockDir) {
|
|
397
|
+
throw new Error("workspaceDir, trustedWorkspaceRoot, and lockDir are required");
|
|
398
|
+
}
|
|
399
|
+
const rootPath = path.resolve(trustedWorkspaceRoot);
|
|
400
|
+
const workspacePath = path.resolve(workspaceDir);
|
|
401
|
+
if (!inside(rootPath, workspacePath))
|
|
402
|
+
throw new Error(`workspace path escapes trusted workspace root: ${workspacePath}`);
|
|
403
|
+
fs.mkdirSync(rootPath, { recursive: true, mode: 0o700 });
|
|
404
|
+
if (!fs.statSync(rootPath).isDirectory())
|
|
405
|
+
throw new Error(`trusted workspace root is not a directory: ${rootPath}`);
|
|
406
|
+
let workspaceStat;
|
|
407
|
+
try {
|
|
408
|
+
workspaceStat = fs.lstatSync(workspacePath);
|
|
409
|
+
}
|
|
410
|
+
catch (error) {
|
|
411
|
+
if (!isMissing(error))
|
|
412
|
+
throw error;
|
|
413
|
+
fs.mkdirSync(workspacePath, { mode: 0o700 });
|
|
414
|
+
workspaceStat = fs.lstatSync(workspacePath);
|
|
415
|
+
}
|
|
416
|
+
if (workspaceStat.isSymbolicLink())
|
|
417
|
+
throw new Error(`workspace directory symlink is unsafe: ${workspacePath}`);
|
|
418
|
+
if (!fs.statSync(workspacePath).isDirectory())
|
|
419
|
+
throw new Error(`workspace is not a directory: ${workspacePath}`);
|
|
420
|
+
const rootReal = fs.realpathSync(rootPath);
|
|
421
|
+
const workspaceReal = fs.realpathSync(workspacePath);
|
|
422
|
+
if (!inside(rootReal, workspaceReal))
|
|
423
|
+
throw new Error(`workspace realpath is outside trusted workspace root: ${workspaceReal}`);
|
|
424
|
+
const lockDirPath = path.resolve(lockDir);
|
|
425
|
+
fs.mkdirSync(lockDirPath, { recursive: true, mode: 0o700 });
|
|
426
|
+
const lockDirReal = fs.realpathSync(lockDirPath);
|
|
427
|
+
if (lockDirReal === workspaceReal || inside(workspaceReal, lockDirReal)) {
|
|
428
|
+
throw new Error("workspace reconciliation lockDir must be outside the user workspace");
|
|
429
|
+
}
|
|
430
|
+
// From this point on, never traverse the mutable canonical bridge again. Capture stable directory identities,
|
|
431
|
+
// lock through the Agent's internal state directory, and re-check all directories before preflight/write.
|
|
432
|
+
const rootIdentity = directoryIdentity(rootReal, "trusted workspace root");
|
|
433
|
+
const workspaceIdentity = directoryIdentity(workspaceReal, "workspace");
|
|
434
|
+
const lockDirIdentity = directoryIdentity(lockDirReal, "workspace lock directory");
|
|
435
|
+
const releaseLock = acquireWorkspaceLock(lockDirReal);
|
|
436
|
+
const openPlans = [];
|
|
437
|
+
try {
|
|
438
|
+
assertDirectoryIdentity(rootReal, rootIdentity, "trusted workspace root");
|
|
439
|
+
assertDirectoryIdentity(workspaceReal, workspaceIdentity, "workspace");
|
|
440
|
+
assertDirectoryIdentity(lockDirReal, lockDirIdentity, "workspace lock directory");
|
|
441
|
+
const plans = PROMPT_FILES.map((name) => promptPlan(path.join(workspaceReal, name)))
|
|
442
|
+
.filter((plan) => plan !== null);
|
|
443
|
+
openPlans.push(...plans);
|
|
444
|
+
const changed = plans.filter((plan) => !plan.next.equals(plan.current));
|
|
445
|
+
assertDirectoryIdentity(rootReal, rootIdentity, "trusted workspace root");
|
|
446
|
+
assertDirectoryIdentity(workspaceReal, workspaceIdentity, "workspace");
|
|
447
|
+
assertDirectoryIdentity(lockDirReal, lockDirIdentity, "workspace lock directory");
|
|
448
|
+
for (const plan of plans)
|
|
449
|
+
assertPromptUnchanged(plan);
|
|
450
|
+
const writtenPlans = new Set();
|
|
451
|
+
for (const plan of changed) {
|
|
452
|
+
assertDirectoryIdentity(rootReal, rootIdentity, "trusted workspace root");
|
|
453
|
+
assertDirectoryIdentity(workspaceReal, workspaceIdentity, "workspace");
|
|
454
|
+
assertDirectoryIdentity(lockDirReal, lockDirIdentity, "workspace lock directory");
|
|
455
|
+
for (const pending of plans) {
|
|
456
|
+
if (!writtenPlans.has(pending))
|
|
457
|
+
assertPromptUnchanged(pending);
|
|
458
|
+
}
|
|
459
|
+
writeManagedSpans(plan, testHooks?.beforeWrite);
|
|
460
|
+
writtenPlans.add(plan);
|
|
461
|
+
}
|
|
462
|
+
return { workspaceDir: workspaceReal, changed: changed.map((plan) => path.basename(plan.file)) };
|
|
463
|
+
}
|
|
464
|
+
finally {
|
|
465
|
+
for (const plan of openPlans)
|
|
466
|
+
closePromptPlan(plan);
|
|
467
|
+
releaseLock();
|
|
468
|
+
}
|
|
469
|
+
}
|