larkin 0.4.2 → 0.4.6

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 (35) hide show
  1. package/README.md +5 -1
  2. package/artifacts/release/THIRD_PARTY_NOTICES.txt +218 -228
  3. package/assets/readme/larkin-github-cover.png +0 -0
  4. package/dist/agent/agent-state-store.cjs +1 -2
  5. package/dist/agent/agent-state-store.mjs +1 -2
  6. package/dist/app/agent-config.mjs +73 -6
  7. package/dist/app/agent-readiness.cjs +22 -2
  8. package/dist/app/agent-readiness.mjs +20 -2
  9. package/dist/app/binary-entry.mjs +6 -0
  10. package/dist/app/cli.mjs +4 -0
  11. package/dist/app/run.mjs +8 -0
  12. package/dist/app/runtime-process.mjs +13 -1
  13. package/dist/dashboard/dashboard-view-model.mjs +12 -3
  14. package/dist/feishu/host-shell.cjs +33 -10
  15. package/dist/feishu/host-shell.mjs +34 -11
  16. package/dist/platform/config.cjs +406 -33
  17. package/dist/platform/config.mjs +405 -33
  18. package/dist/platform/process-boundary-trace.cjs +108 -0
  19. package/dist/platform/process-boundary-trace.mjs +102 -0
  20. package/dist/runtime/pi-artifact-provenance.cjs +147 -0
  21. package/dist/runtime/pi-artifact-provenance.mjs +139 -0
  22. package/dist/runtime/pi-compaction-recovery.cjs +120 -40
  23. package/dist/runtime/pi-compaction-recovery.mjs +117 -40
  24. package/dist/runtime/pi-model-catalog.cjs +2 -2
  25. package/dist/runtime/pi-model-catalog.mjs +2 -2
  26. package/dist/runtime/pi-profile-migration.cjs +556 -0
  27. package/dist/runtime/pi-profile-migration.mjs +515 -0
  28. package/dist/runtime/pi-provider-config.cjs +1 -1
  29. package/dist/runtime/pi-provider-config.mjs +1 -1
  30. package/dist/runtime/runtime-adapters.mjs +97 -11
  31. package/dist/runtime/runtime-host.cjs +162 -39
  32. package/dist/runtime/runtime-host.mjs +163 -40
  33. package/dist/runtime/runtime-readiness.cjs +6 -4
  34. package/dist/runtime/runtime-readiness.mjs +6 -4
  35. package/package.json +3 -3
@@ -0,0 +1,515 @@
1
+ import * as crypto from "node:crypto";
2
+ import * as fs from "node:fs";
3
+ import * as os from "node:os";
4
+ import * as path from "node:path";
5
+ import { spawnSync } from "node:child_process";
6
+ import { resolveRuntimeExecutable } from "./runtime-readiness.mjs";
7
+ import { mergeOwnedPiSettings, parsePiExecutableVersion } from "./pi-compaction-recovery.mjs";
8
+ import { BUNDLED_PI_VERSION } from "./pi-provider-config.mjs";
9
+ import { attestedPiRuntimeArtifactNames, PI_RUNTIME_ARTIFACT_MANIFEST } from "./pi-artifact-provenance.mjs";
10
+ const AGENT_ID = /^cli_[A-Za-z0-9]+$/;
11
+ const FILES = ["auth.json", "models.json", "settings.json"];
12
+ const BUILTIN_PI_PACKAGE = /^npm:@tintinweb\/pi-subagents(?:@[^/]+)?$|^@tintinweb\/pi-subagents(?:@[^/]+)?$/i;
13
+ const FILE_LIMIT = 8 * 1024 * 1024;
14
+ const EXECUTABLE_LIMIT = 256 * 1024 * 1024;
15
+ const SOURCE_MODES = new Set([0o600, 0o644, 0o640]);
16
+ const TARGET_LOCK_SUFFIX = ".larkin-pi-import.lock";
17
+ function hash(bytes) { return crypto.createHash("sha256").update(bytes).digest("hex"); }
18
+ function targetLockFile(state) { return path.join(path.dirname(state.targetDir), `${state.agentId}${TARGET_LOCK_SUFFIX}`); }
19
+ function acquireTargetLock(state) {
20
+ const file = targetLockFile(state);
21
+ assertNoSymlinkAncestors(path.dirname(file));
22
+ let fd;
23
+ try {
24
+ fd = fs.openSync(file, "wx", 0o600);
25
+ fs.writeFileSync(fd, `${process.pid}\n`);
26
+ fs.fsyncSync(fd);
27
+ }
28
+ catch (error) {
29
+ if (error.code === "EEXIST")
30
+ throw new Error("Pi provider target is busy; refusing migration");
31
+ throw error;
32
+ }
33
+ finally {
34
+ if (fd !== undefined)
35
+ fs.closeSync(fd);
36
+ }
37
+ fsyncDirectory(path.dirname(file));
38
+ }
39
+ export function releasePiProfileMigrationLock(state) {
40
+ try {
41
+ fs.unlinkSync(targetLockFile(state));
42
+ fsyncDirectory(path.dirname(targetLockFile(state)));
43
+ }
44
+ catch (error) {
45
+ if (error.code !== "ENOENT")
46
+ throw error;
47
+ }
48
+ }
49
+ function isKnownSystemSymlink(value) {
50
+ return value === "/tmp" || value === "/private/tmp" || value === "/var" || value === "/private/var"
51
+ || value === "/var/folders" || /^\/var\/folders\/[^/]+$/.test(value)
52
+ || value === "/private/var/folders" || /^\/private\/var\/folders\/[^/]+$/.test(value);
53
+ }
54
+ function assertNoSymlinkAncestors(value) {
55
+ const resolved = path.resolve(value);
56
+ let current = path.parse(resolved).root;
57
+ for (const part of path.relative(current, resolved).split(path.sep).filter(Boolean)) {
58
+ current = path.join(current, part);
59
+ let stat;
60
+ try {
61
+ stat = fs.lstatSync(current);
62
+ }
63
+ catch (error) {
64
+ if (error.code === "ENOENT")
65
+ return;
66
+ throw error;
67
+ }
68
+ if (stat.isSymbolicLink() && !isKnownSystemSymlink(current))
69
+ throw new Error("Pi profile path contains a symlink ancestor");
70
+ if (!stat.isDirectory() && !stat.isSymbolicLink())
71
+ throw new Error("Pi profile path ancestor is not a directory");
72
+ }
73
+ }
74
+ function assertOwner(stat, label) {
75
+ if (typeof process.getuid === "function" && stat.uid !== process.getuid())
76
+ throw new Error(`${label} owner is unsafe`);
77
+ }
78
+ function assertDirectory(directory, label, required = true) {
79
+ try {
80
+ const stat = fs.lstatSync(directory);
81
+ if (!stat.isDirectory() || stat.isSymbolicLink())
82
+ throw new Error(`${label} is unsafe`);
83
+ assertOwner(stat, label);
84
+ return stat;
85
+ }
86
+ catch (error) {
87
+ if (error.code === "ENOENT" && !required)
88
+ return null;
89
+ throw error;
90
+ }
91
+ }
92
+ function readBounded(file, label, limit, required = true) {
93
+ let fd;
94
+ try {
95
+ const initial = fs.lstatSync(file);
96
+ if (initial.isSymbolicLink())
97
+ throw new Error(`${label} is unsafe`);
98
+ const noFollow = fs.constants.O_NOFOLLOW ?? 0;
99
+ fd = fs.openSync(file, fs.constants.O_RDONLY | noFollow);
100
+ const before = fs.fstatSync(fd);
101
+ if (!before.isFile() || before.nlink !== 1)
102
+ throw new Error(`${label} is unsafe`);
103
+ assertOwner(before, label);
104
+ if (before.size > limit)
105
+ throw new Error(`${label} is too large`);
106
+ const bytes = Buffer.allocUnsafe(before.size);
107
+ let offset = 0;
108
+ while (offset < before.size) {
109
+ const read = fs.readSync(fd, bytes, offset, before.size - offset, null);
110
+ if (read === 0)
111
+ throw new Error(`${label} changed while reading`);
112
+ offset += read;
113
+ }
114
+ const after = fs.fstatSync(fd);
115
+ if (after.dev !== before.dev || after.ino !== before.ino || after.size !== before.size || after.nlink !== before.nlink
116
+ || after.uid !== before.uid || after.gid !== before.gid || (after.mode & 0o777) !== (before.mode & 0o777))
117
+ throw new Error(`${label} changed while reading`);
118
+ return { bytes, mode: after.mode & 0o777 };
119
+ }
120
+ catch (error) {
121
+ if (error.code === "ENOENT" && !required)
122
+ return null;
123
+ if (error.code === "ELOOP")
124
+ throw new Error(`${label} is unsafe`);
125
+ throw error;
126
+ }
127
+ finally {
128
+ if (fd !== undefined)
129
+ fs.closeSync(fd);
130
+ }
131
+ }
132
+ function readRegular(file, label, required = true) {
133
+ return readBounded(file, label, FILE_LIMIT, required);
134
+ }
135
+ function readExecutable(file, label) {
136
+ let resolved;
137
+ try {
138
+ resolved = fs.realpathSync(file);
139
+ }
140
+ catch {
141
+ throw new Error(`${label} is unavailable`);
142
+ }
143
+ const readable = readBounded(resolved, label, EXECUTABLE_LIMIT);
144
+ if (!readable || (readable.mode & 0o022) !== 0 || (readable.mode & 0o111) === 0)
145
+ throw new Error(`${label} is unsafe`);
146
+ return { path: resolved, sha256: hash(readable.bytes), bytes: readable.bytes.length, mode: readable.mode };
147
+ }
148
+ function sourceDirectory(env) {
149
+ const home = env.HOME || os.homedir();
150
+ const configured = String(env.PI_CODING_AGENT_DIR || "").trim();
151
+ const expanded = configured.startsWith("~") ? path.join(home, configured.slice(1)) : configured;
152
+ return path.resolve(expanded || path.join(home, ".pi", "agent"));
153
+ }
154
+ function verifySourceProfile(directory) {
155
+ assertNoSymlinkAncestors(directory);
156
+ assertDirectory(directory, "external Pi profile");
157
+ }
158
+ function safeState(file, includeContent) {
159
+ if (!file)
160
+ return { present: false };
161
+ return { present: true, sha256: hash(file.bytes), bytes: file.bytes.length, mode: file.mode, ...(includeContent ? { content: file.bytes.toString("base64") } : {}) };
162
+ }
163
+ function targetDirectory(configDir, agentId) {
164
+ if (!AGENT_ID.test(agentId))
165
+ throw new Error("Pi Agent ID 格式无效");
166
+ return path.join(path.resolve(configDir), "providers", "pi", agentId);
167
+ }
168
+ function sourceVersion(env, cwd) {
169
+ const command = String(env.LARKIN_PI_COMMAND || "pi");
170
+ const executable = resolveRuntimeExecutable(command, env);
171
+ if (!executable)
172
+ throw new Error("外部 Pi 0.84.2 profile import requires an installed Pi executable");
173
+ const executableState = readExecutable(executable, "external Pi executable");
174
+ const result = spawnSync(executableState.path, ["--version"], { cwd, env, encoding: "utf8", timeout: 5_000, maxBuffer: 64 * 1024 });
175
+ if (result.status !== 0)
176
+ throw new Error("external Pi version probe failed");
177
+ try {
178
+ parsePiExecutableVersion(String(result.stdout || result.stderr || "").trim());
179
+ }
180
+ catch {
181
+ throw new Error(`external Pi must be exactly ${BUNDLED_PI_VERSION}`);
182
+ }
183
+ const afterProbe = readExecutable(executableState.path, "external Pi executable");
184
+ if (afterProbe.sha256 !== executableState.sha256 || afterProbe.bytes !== executableState.bytes || afterProbe.mode !== executableState.mode) {
185
+ throw new Error("external Pi executable changed during version probe");
186
+ }
187
+ return executableState;
188
+ }
189
+ function mergeSettings(bytes, distribution) {
190
+ let value;
191
+ try {
192
+ value = JSON.parse(bytes.toString("utf8"));
193
+ }
194
+ catch {
195
+ throw new Error("external Pi settings.json is invalid");
196
+ }
197
+ const merged = mergeOwnedPiSettings(value);
198
+ // Builtin Pi always loads Larkin's inline subagents factory. Remove only that
199
+ // duplicate package during builtin-profile import; external Pi must retain the
200
+ // user's package so its resolver can suppress inline injection.
201
+ if (distribution === "builtin" && Array.isArray(merged.packages)) {
202
+ merged.packages = merged.packages.filter((entry) => typeof entry !== "string" || !BUILTIN_PI_PACKAGE.test(entry));
203
+ }
204
+ return Buffer.from(`${JSON.stringify(merged, null, 2)}\n`);
205
+ }
206
+ function targetPrior(directory) {
207
+ const stat = assertDirectory(directory, "Pi provider target", false);
208
+ if (stat && (stat.mode & 0o777) !== 0o700)
209
+ throw new Error("Pi provider target must be 0700");
210
+ const entries = stat ? fs.readdirSync(directory).sort() : [];
211
+ const files = Object.fromEntries(FILES.map((name) => {
212
+ const file = readRegular(path.join(directory, name), `Pi provider target ${name}`, false);
213
+ if (file && file.mode !== 0o600)
214
+ throw new Error("Pi provider target files must be 0600");
215
+ return [name, safeState(file, true)];
216
+ }));
217
+ return { stat, entries, files };
218
+ }
219
+ export function preparePiProfileMigration(env, configDir, agentId, distribution = "builtin") {
220
+ const sourceDir = sourceDirectory(env);
221
+ verifySourceProfile(sourceDir);
222
+ const sourceDirectoryState = assertDirectory(sourceDir, "external Pi profile");
223
+ const sourceExecutable = sourceVersion(env, sourceDir);
224
+ const sourceFiles = Object.fromEntries(FILES.map((name) => {
225
+ const file = readRegular(path.join(sourceDir, name), `external Pi ${name}`);
226
+ if (!file)
227
+ throw new Error(`external Pi ${name} is required`);
228
+ if (name === "auth.json" && file.mode !== 0o600)
229
+ throw new Error("external Pi auth.json has an unsafe mode");
230
+ if (name !== "auth.json" && !SOURCE_MODES.has(file.mode))
231
+ throw new Error(`external Pi ${name} has an unsafe mode`);
232
+ return [name, file];
233
+ }));
234
+ const targetDir = targetDirectory(configDir, agentId);
235
+ assertNoSymlinkAncestors(targetDir);
236
+ const target = targetPrior(targetDir);
237
+ const imported = {
238
+ "auth.json": sourceFiles["auth.json"].bytes,
239
+ "models.json": sourceFiles["models.json"].bytes,
240
+ "settings.json": mergeSettings(sourceFiles["settings.json"].bytes, distribution),
241
+ };
242
+ const afterFiles = Object.fromEntries(FILES.map((name) => [name, safeState({ bytes: imported[name], mode: 0o600 }, false)]));
243
+ return {
244
+ state: { version: 1, agentId, sourceDir, sourceDirMode: sourceDirectoryState.mode & 0o777,
245
+ sourceCommand: String(env.LARKIN_PI_COMMAND || "pi"), sourceExecutable,
246
+ sourceFiles: Object.fromEntries(FILES.map((name) => [name, safeState(sourceFiles[name], false)])),
247
+ targetDir, targetDirExisted: Boolean(target.stat), targetDirMode: target.stat ? target.stat.mode & 0o777 : 0o700,
248
+ ...(target.stat ? { targetDirDevice: target.stat.dev, targetDirInode: target.stat.ino } : {}), targetEntries: target.entries,
249
+ priorFiles: target.files, afterFiles },
250
+ sourceBytes: { "auth.json": sourceFiles["auth.json"].bytes, "models.json": sourceFiles["models.json"].bytes,
251
+ "settings.json": imported["settings.json"] },
252
+ sourceEnvironment: { PATH: env.PATH, LARKIN_PI_COMMAND: env.LARKIN_PI_COMMAND },
253
+ };
254
+ }
255
+ export function validatePiProfileMigrationState(value) {
256
+ if (!value || typeof value !== "object" || Array.isArray(value))
257
+ throw new Error("Pi profile migration state is invalid");
258
+ const state = value;
259
+ if (state.version !== 1 || typeof state.agentId !== "string" || !AGENT_ID.test(state.agentId)
260
+ || typeof state.sourceDir !== "string" || !Number.isInteger(state.sourceDirMode)
261
+ || typeof state.sourceCommand !== "string" || !state.sourceCommand.trim() || /[\r\n\0]/.test(state.sourceCommand)
262
+ || !state.sourceExecutable || typeof state.sourceExecutable.path !== "string"
263
+ || typeof state.sourceExecutable.sha256 !== "string" || !Number.isSafeInteger(state.sourceExecutable.bytes)
264
+ || state.sourceExecutable.bytes < 0 || !Number.isInteger(state.sourceExecutable.mode)
265
+ || typeof state.targetDir !== "string" || typeof state.targetDirExisted !== "boolean"
266
+ || !Number.isInteger(state.targetDirMode) || (state.targetDirExisted && (!Number.isSafeInteger(state.targetDirDevice) || !Number.isSafeInteger(state.targetDirInode)))
267
+ || !Array.isArray(state.targetEntries) || state.targetEntries.some((entry) => typeof entry !== "string" || !entry || entry === "." || entry === ".." || entry.includes("/"))
268
+ || !state.sourceFiles || !state.priorFiles || !state.afterFiles)
269
+ throw new Error("Pi profile migration state is invalid");
270
+ const collections = [
271
+ { files: state.sourceFiles, prior: false },
272
+ { files: state.priorFiles, prior: true },
273
+ { files: state.afterFiles, prior: false },
274
+ ];
275
+ for (const name of FILES) {
276
+ for (const collection of collections) {
277
+ const file = collection.files[name];
278
+ if (!file || typeof file !== "object" || typeof file.present !== "boolean")
279
+ throw new Error("Pi profile migration state is invalid");
280
+ const record = file;
281
+ if (record.present && (typeof record.sha256 !== "string" || typeof record.bytes !== "number" || !Number.isSafeInteger(record.bytes) || record.bytes < 0))
282
+ throw new Error("Pi profile migration state is invalid");
283
+ if (collection.prior && record.present) {
284
+ if (typeof record.content !== "string" || record.mode !== 0o600)
285
+ throw new Error("Pi profile migration state is invalid");
286
+ const bytes = Buffer.from(record.content, "base64");
287
+ if (bytes.length !== record.bytes || hash(bytes) !== record.sha256)
288
+ throw new Error("Pi profile migration state is invalid");
289
+ }
290
+ }
291
+ }
292
+ }
293
+ function assertSourceUnchanged(state, resolutionEnvironment = process.env, verifyResolution = true) {
294
+ const directory = assertDirectory(state.sourceDir, "external Pi profile");
295
+ if ((directory.mode & 0o777) !== state.sourceDirMode)
296
+ throw new Error("external Pi profile changed; refusing migration");
297
+ verifySourceProfile(state.sourceDir);
298
+ const probeEnvironment = { ...process.env, ...resolutionEnvironment };
299
+ const resolved = verifyResolution ? resolveRuntimeExecutable(state.sourceCommand, probeEnvironment) : state.sourceExecutable.path;
300
+ if (!resolved)
301
+ throw new Error("external Pi executable changed; refusing migration");
302
+ const executable = readExecutable(resolved, "external Pi executable");
303
+ if (executable.path !== state.sourceExecutable.path || executable.sha256 !== state.sourceExecutable.sha256
304
+ || executable.bytes !== state.sourceExecutable.bytes || executable.mode !== state.sourceExecutable.mode) {
305
+ throw new Error("external Pi executable changed; refusing migration");
306
+ }
307
+ const result = spawnSync(executable.path, ["--version"], { cwd: state.sourceDir, env: probeEnvironment, encoding: "utf8", timeout: 5_000, maxBuffer: 64 * 1024 });
308
+ if (result.status !== 0)
309
+ throw new Error("external Pi version probe failed");
310
+ try {
311
+ parsePiExecutableVersion(String(result.stdout || result.stderr || "").trim());
312
+ }
313
+ catch {
314
+ throw new Error(`external Pi must be exactly ${BUNDLED_PI_VERSION}`);
315
+ }
316
+ const afterProbe = readExecutable(executable.path, "external Pi executable");
317
+ if (afterProbe.sha256 !== executable.sha256 || afterProbe.bytes !== executable.bytes || afterProbe.mode !== executable.mode) {
318
+ throw new Error("external Pi executable changed during version probe");
319
+ }
320
+ for (const name of FILES) {
321
+ const file = readRegular(path.join(state.sourceDir, name), `external Pi ${name}`);
322
+ const expected = state.sourceFiles[name];
323
+ if (!file || !expected.present || hash(file.bytes) !== expected.sha256 || file.bytes.length !== expected.bytes || file.mode !== expected.mode) {
324
+ throw new Error("external Pi profile changed; refusing migration");
325
+ }
326
+ }
327
+ }
328
+ function atomicPrivateWrite(file, bytes) {
329
+ const temp = `${file}.larkin-pi-import-${process.pid}-${crypto.randomUUID()}.tmp`;
330
+ const fd = fs.openSync(temp, "wx", 0o600);
331
+ try {
332
+ fs.writeFileSync(fd, bytes);
333
+ fs.fsyncSync(fd);
334
+ }
335
+ finally {
336
+ fs.closeSync(fd);
337
+ }
338
+ try {
339
+ fs.renameSync(temp, file);
340
+ fs.chmodSync(file, 0o600);
341
+ }
342
+ finally {
343
+ try {
344
+ fs.unlinkSync(temp);
345
+ }
346
+ catch { /* renamed */ }
347
+ }
348
+ }
349
+ function currentFileState(file) { return safeState(readRegular(file, "Pi provider target file", false), false); }
350
+ export function assertPiProfileMigrationAfterState(state) {
351
+ validatePiProfileMigrationState(state);
352
+ const target = assertDirectory(state.targetDir, "Pi provider target");
353
+ if ((target.mode & 0o777) !== 0o700)
354
+ throw new Error("Pi provider migration target is unsafe");
355
+ if (state.targetDirExisted && (target.dev !== state.targetDirDevice || target.ino !== state.targetDirInode))
356
+ throw new Error("Pi provider migration target changed");
357
+ const expectedEntries = [...new Set([...state.targetEntries, ...FILES])].sort();
358
+ if (JSON.stringify(fs.readdirSync(state.targetDir).sort()) !== JSON.stringify(expectedEntries))
359
+ throw new Error("Pi provider migration target entries changed");
360
+ for (const name of FILES) {
361
+ const current = currentFileState(path.join(state.targetDir, name));
362
+ const expected = state.afterFiles[name];
363
+ if (!current.present || !expected.present || current.sha256 !== expected.sha256 || current.bytes !== expected.bytes || current.mode !== 0o600) {
364
+ throw new Error("Pi provider migration target is incomplete");
365
+ }
366
+ }
367
+ }
368
+ function assertTargetUnchanged(state) {
369
+ const target = assertDirectory(state.targetDir, "Pi provider target", false);
370
+ if (Boolean(target) !== state.targetDirExisted)
371
+ throw new Error("Pi provider target changed; refusing migration");
372
+ if (target && ((target.mode & 0o777) !== state.targetDirMode))
373
+ throw new Error("Pi provider target changed; refusing migration");
374
+ if (target && state.targetDirExisted && (target.dev !== state.targetDirDevice || target.ino !== state.targetDirInode))
375
+ throw new Error("Pi provider target changed; refusing migration");
376
+ if (target && JSON.stringify(fs.readdirSync(state.targetDir).sort()) !== JSON.stringify(state.targetEntries))
377
+ throw new Error("Pi provider target entries changed; refusing migration");
378
+ for (const name of FILES) {
379
+ const current = currentFileState(path.join(state.targetDir, name));
380
+ const prior = state.priorFiles[name];
381
+ if (current.present !== prior.present || (current.present && (current.sha256 !== prior.sha256 || current.bytes !== prior.bytes || current.mode !== prior.mode))) {
382
+ throw new Error("Pi provider target changed; refusing migration");
383
+ }
384
+ }
385
+ }
386
+ function assertPrior(state) {
387
+ const target = assertDirectory(state.targetDir, "Pi provider target");
388
+ if ((target.mode & 0o777) !== state.targetDirMode)
389
+ throw new Error("Pi provider target rollback directory changed");
390
+ if (state.targetDirExisted && (target.dev !== state.targetDirDevice || target.ino !== state.targetDirInode))
391
+ throw new Error("Pi provider target rollback directory changed");
392
+ if (JSON.stringify(fs.readdirSync(state.targetDir).sort()) !== JSON.stringify(state.targetEntries))
393
+ throw new Error("Pi provider target rollback entries changed");
394
+ for (const name of FILES) {
395
+ const current = currentFileState(path.join(state.targetDir, name));
396
+ const prior = state.priorFiles[name];
397
+ if (current.present !== prior.present || (current.present && (current.sha256 !== prior.sha256 || current.bytes !== prior.bytes || current.mode !== prior.mode)))
398
+ throw new Error("Pi provider target rollback is incomplete or conflicted");
399
+ }
400
+ }
401
+ function restoreFile(file, prior) {
402
+ if (!prior.present) {
403
+ try {
404
+ fs.unlinkSync(file);
405
+ }
406
+ catch (error) {
407
+ if (error.code !== "ENOENT")
408
+ throw error;
409
+ }
410
+ return;
411
+ }
412
+ if (typeof prior.content !== "string")
413
+ throw new Error("Pi provider rollback bytes are missing");
414
+ atomicPrivateWrite(file, Buffer.from(prior.content, "base64"));
415
+ if (prior.mode !== undefined)
416
+ fs.chmodSync(file, prior.mode);
417
+ }
418
+ export function applyPiProfileMigration(plan) {
419
+ const sameEnvironment = process.env.PATH === plan.sourceEnvironment.PATH
420
+ && process.env.LARKIN_PI_COMMAND === plan.sourceEnvironment.LARKIN_PI_COMMAND;
421
+ assertSourceUnchanged(plan.state, sameEnvironment ? process.env : plan.sourceEnvironment);
422
+ const parent = path.dirname(plan.state.targetDir);
423
+ assertNoSymlinkAncestors(parent);
424
+ fs.mkdirSync(parent, { recursive: true, mode: 0o700 });
425
+ fs.chmodSync(parent, 0o700);
426
+ acquireTargetLock(plan.state);
427
+ assertTargetUnchanged(plan.state);
428
+ const existing = assertDirectory(plan.state.targetDir, "Pi provider target", false);
429
+ if (existing && (existing.mode & 0o777) !== 0o700)
430
+ throw new Error("Pi provider target must be 0700");
431
+ if (!existing) {
432
+ try {
433
+ fs.mkdirSync(plan.state.targetDir, { mode: 0o700 });
434
+ }
435
+ catch (error) {
436
+ if (error.code === "EEXIST")
437
+ throw new Error("Pi provider target changed; refusing migration");
438
+ throw error;
439
+ }
440
+ fs.chmodSync(plan.state.targetDir, 0o700);
441
+ }
442
+ for (const name of FILES) {
443
+ const current = currentFileState(path.join(plan.state.targetDir, name));
444
+ const prior = plan.state.priorFiles[name];
445
+ if (current.present !== prior.present || (current.present && (current.sha256 !== prior.sha256 || current.bytes !== prior.bytes || current.mode !== prior.mode))) {
446
+ throw new Error("Pi provider target changed; refusing migration");
447
+ }
448
+ atomicPrivateWrite(path.join(plan.state.targetDir, name), plan.sourceBytes[name]);
449
+ }
450
+ assertPiProfileMigrationAfterState(plan.state);
451
+ fsyncDirectory(plan.state.targetDir);
452
+ }
453
+ function fsyncDirectory(directory) {
454
+ try {
455
+ const fd = fs.openSync(directory, fs.constants.O_RDONLY);
456
+ try {
457
+ fs.fsyncSync(fd);
458
+ }
459
+ finally {
460
+ fs.closeSync(fd);
461
+ }
462
+ }
463
+ catch { /* best effort on platforms without directory fsync */ }
464
+ }
465
+ export function rollbackPiProfileMigration(state) {
466
+ validatePiProfileMigrationState(state);
467
+ assertSourceUnchanged(state, process.env, false);
468
+ const target = assertDirectory(state.targetDir, "Pi provider target", false);
469
+ if (!target) {
470
+ if (state.targetDirExisted)
471
+ throw new Error("Pi provider target disappeared during rollback");
472
+ releasePiProfileMigrationLock(state);
473
+ return;
474
+ }
475
+ for (const name of FILES) {
476
+ const current = currentFileState(path.join(state.targetDir, name));
477
+ const prior = state.priorFiles[name];
478
+ const expected = state.afterFiles[name];
479
+ const isPrior = current.present === prior.present && (!current.present || (current.sha256 === prior.sha256 && current.bytes === prior.bytes && current.mode === prior.mode));
480
+ const isExpected = current.present && current.sha256 === expected.sha256 && current.bytes === expected.bytes && current.mode === 0o600;
481
+ if (!isPrior && !isExpected)
482
+ throw new Error("Pi provider target changed; refusing migration rollback");
483
+ }
484
+ for (const name of FILES) {
485
+ const current = currentFileState(path.join(state.targetDir, name));
486
+ const prior = state.priorFiles[name];
487
+ const isPrior = current.present === prior.present && (!current.present || (current.sha256 === prior.sha256 && current.bytes === prior.bytes && current.mode === prior.mode));
488
+ if (!isPrior)
489
+ restoreFile(path.join(state.targetDir, name), prior);
490
+ }
491
+ if (!state.targetDirExisted) {
492
+ const entries = fs.readdirSync(state.targetDir).filter((name) => !FILES.includes(name) && !name.startsWith(".larkin-pi-import-"));
493
+ const attested = attestedPiRuntimeArtifactNames(state.targetDir);
494
+ const removable = entries.filter((name) => attested.has(name) || name === PI_RUNTIME_ARTIFACT_MANIFEST);
495
+ const unknown = entries.filter((name) => !removable.includes(name));
496
+ // An allowed name is not provenance. Recursive cleanup is permitted only when
497
+ // every surviving artifact is identity-attested by the runtime-owned manifest.
498
+ if (unknown.length === 0 && (entries.length === 0 || (attested.size > 0 && removable.includes(PI_RUNTIME_ARTIFACT_MANIFEST)))) {
499
+ for (const name of removable) {
500
+ const artifact = path.join(state.targetDir, name);
501
+ const artifactStat = fs.lstatSync(artifact);
502
+ if (artifactStat.isSymbolicLink())
503
+ throw new Error("Pi provider rollback artifact is a symlink");
504
+ fs.rmSync(artifact, { recursive: artifactStat.isDirectory(), force: true });
505
+ }
506
+ fs.rmdirSync(state.targetDir);
507
+ }
508
+ }
509
+ else
510
+ fs.chmodSync(state.targetDir, state.targetDirMode);
511
+ if (state.targetDirExisted)
512
+ assertPrior(state);
513
+ fsyncDirectory(path.dirname(state.targetDir));
514
+ releasePiProfileMigrationLock(state);
515
+ }
@@ -16,7 +16,7 @@ const node_crypto_1 = __importDefault(require("node:crypto"));
16
16
  const node_fs_1 = __importDefault(require("node:fs"));
17
17
  const node_path_1 = __importDefault(require("node:path"));
18
18
  const secure_metadata_js_1 = require("../platform/secure-metadata.cjs");
19
- exports.BUNDLED_PI_VERSION = "0.83.0";
19
+ exports.BUNDLED_PI_VERSION = "0.84.2";
20
20
  // These values mirror the catalog shipped by the pinned official Pi distribution.
21
21
  // Keeping the user-facing defaults here makes endpoint changes reviewable in Larkin.
22
22
  exports.PI_PROVIDER_PRESETS = Object.freeze([
@@ -2,7 +2,7 @@ import crypto from "node:crypto";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { isWindows, secureWindowsDirectoryAcl } from "../platform/secure-metadata.mjs";
5
- export const BUNDLED_PI_VERSION = "0.83.0";
5
+ export const BUNDLED_PI_VERSION = "0.84.2";
6
6
  // These values mirror the catalog shipped by the pinned official Pi distribution.
7
7
  // Keeping the user-facing defaults here makes endpoint changes reviewable in Larkin.
8
8
  export const PI_PROVIDER_PRESETS = Object.freeze([