larkin 0.5.0 → 0.5.2
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/CHANGELOG.md +8 -0
- package/dist/dashboard/dashboard-config-controller.mjs +2 -3
- package/dist/runtime/pi-compaction-recovery.cjs +87 -36
- package/dist/runtime/pi-compaction-recovery.mjs +84 -34
- package/dist/runtime/pi-model-catalog.cjs +3 -39
- package/dist/runtime/pi-model-catalog.mjs +3 -6
- package/dist/runtime/pi-subagent-injection.mjs +42 -14
- package/dist/runtime/runtime-adapters.mjs +31 -45
- package/dist/runtime/runtime-readiness.cjs +24 -1
- package/dist/runtime/runtime-readiness.mjs +24 -1
- package/package.json +1 -1
- package/dist/runtime/pi-artifact-provenance.mjs +0 -139
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.2
|
|
4
|
+
|
|
5
|
+
External pi now runs with the user's own Pi home; compaction settings move to the Agent workspace's `.pi/settings.json`; 0.5.0/0.5.1 started external pi with an empty agent dir and saw no logins. Stock pi 0.84.x does not emit `get_state.compactionCapabilities` (that handshake only existed for the bundled build); Larkin accepts the absence and keeps native compaction via the workspace settings file, while a present handshake must still match exactly.
|
|
6
|
+
|
|
7
|
+
## 0.5.1
|
|
8
|
+
|
|
9
|
+
External pi is accepted at 0.84.2 or newer instead of exactly 0.84.2; 0.5.0 refused newer pi and left every migrated Agent not ready.
|
|
10
|
+
|
|
3
11
|
## 0.5.0
|
|
4
12
|
|
|
5
13
|
BREAKING — builtin Pi is removed. Larkin supports only externally installed `pi`, `codex`, and `claude`. If the selected runtime is not installed, setup, runtime switch, and readiness fail with an explicit missing-install message.
|
|
@@ -200,7 +200,7 @@ export function createPiModelDirectoryResolver(options = {}) {
|
|
|
200
200
|
}
|
|
201
201
|
},
|
|
202
202
|
async resolve(input) {
|
|
203
|
-
const key = [input.agentId, input.cwd, input.
|
|
203
|
+
const key = [input.agentId, input.cwd, input.command ?? "", ...(input.commandArgs ?? [])].join("\u0000");
|
|
204
204
|
const started = generationOf(input.agentId);
|
|
205
205
|
pruneCache(cache, (entry) => now() >= entry.expiresAt, 64);
|
|
206
206
|
pruneCache(failures, (entry) => now() >= entry.expiresAt, 64);
|
|
@@ -215,7 +215,6 @@ export function createPiModelDirectoryResolver(options = {}) {
|
|
|
215
215
|
try {
|
|
216
216
|
const catalog = await discover({
|
|
217
217
|
cwd: input.cwd,
|
|
218
|
-
...(input.agentDir ? { agentDir: input.agentDir } : {}),
|
|
219
218
|
...(input.command ? { command: input.command } : {}),
|
|
220
219
|
...(input.commandArgs ? { commandArgs: input.commandArgs } : {}),
|
|
221
220
|
});
|
|
@@ -424,7 +423,7 @@ export function createDashboardConfigController({ csrfCapability, env = process.
|
|
|
424
423
|
const agent = config.agents[agentId];
|
|
425
424
|
if (!agent)
|
|
426
425
|
throw new Error("unknown agent");
|
|
427
|
-
// 外部 `pi --mode rpc` 使用用户自己的 Pi home
|
|
426
|
+
// 外部 `pi --mode rpc` 使用用户自己的 Pi home;catalog 探测不改写子进程 agent 目录。
|
|
428
427
|
return await piModelDirectoryResolver.resolve({
|
|
429
428
|
agentId,
|
|
430
429
|
cwd: agent.workspaceDir,
|
|
@@ -3,20 +3,21 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.PiCompactionRecoveryMachine = exports.PiCompactionBreaker = exports.PI_COMPACTION_RECOVERY_FILE = exports.PI_COMPACTION_RECOVERY_VERSION = exports.PI_COMPACTION_THRESHOLD = exports.COMPACTION_KEEP_RECENT_TOKENS = exports.COMPACTION_RESERVE_TOKENS = exports.PI_CONTEXT_WINDOW = exports.
|
|
6
|
+
exports.PiCompactionRecoveryMachine = exports.PiCompactionBreaker = exports.PI_COMPACTION_RECOVERY_FILE = exports.PI_COMPACTION_RECOVERY_VERSION = exports.PI_COMPACTION_THRESHOLD = exports.COMPACTION_KEEP_RECENT_TOKENS = exports.COMPACTION_RESERVE_TOKENS = exports.PI_CONTEXT_WINDOW = exports.MINIMUM_PI_VERSION = void 0;
|
|
7
7
|
exports.calculatePiCompactionSettings = calculatePiCompactionSettings;
|
|
8
8
|
exports.isPiNativeCompactionRequired = isPiNativeCompactionRequired;
|
|
9
9
|
exports.assertEffectivePiCompactionSettings = assertEffectivePiCompactionSettings;
|
|
10
10
|
exports.hasProjectPiCompactionOverride = hasProjectPiCompactionOverride;
|
|
11
11
|
exports.ensureOwnedPiAgentDirectory = ensureOwnedPiAgentDirectory;
|
|
12
|
+
exports.projectPiSettingsFile = projectPiSettingsFile;
|
|
12
13
|
exports.ownedPiSettings = ownedPiSettings;
|
|
13
14
|
exports.prepareOwnedPiDirectory = prepareOwnedPiDirectory;
|
|
14
15
|
exports.mergeOwnedPiSettings = mergeOwnedPiSettings;
|
|
15
16
|
exports.writeOwnedPiSettings = writeOwnedPiSettings;
|
|
16
17
|
exports.readOwnedPiSettings = readOwnedPiSettings;
|
|
18
|
+
exports.olderThanMinimumPiVersionMessage = olderThanMinimumPiVersionMessage;
|
|
17
19
|
exports.parsePiExecutableVersion = parsePiExecutableVersion;
|
|
18
20
|
exports.verifyPiCapabilities = verifyPiCapabilities;
|
|
19
|
-
exports.prepareOwnedPiEnvironment = prepareOwnedPiEnvironment;
|
|
20
21
|
exports.readProjectPiSettings = readProjectPiSettings;
|
|
21
22
|
exports.assertNoProjectPiCompactionOverride = assertNoProjectPiCompactionOverride;
|
|
22
23
|
exports.currentUserLabel = currentUserLabel;
|
|
@@ -24,7 +25,8 @@ const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
|
24
25
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
25
26
|
const node_os_1 = __importDefault(require("node:os"));
|
|
26
27
|
const node_path_1 = __importDefault(require("node:path"));
|
|
27
|
-
|
|
28
|
+
/** External `pi` must be this version or newer. Stock 0.84.x does not emit compactionCapabilities. */
|
|
29
|
+
exports.MINIMUM_PI_VERSION = "0.84.2";
|
|
28
30
|
/** Fallback values used while importing a Pi profile, before Pi reports its model. */
|
|
29
31
|
exports.PI_CONTEXT_WINDOW = 272_000;
|
|
30
32
|
exports.COMPACTION_RESERVE_TOKENS = 40_800;
|
|
@@ -172,15 +174,16 @@ function ensureOwnedPiAgentDirectory(root, agentId) {
|
|
|
172
174
|
ensureDirectory(directory);
|
|
173
175
|
return directory;
|
|
174
176
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
+
/** Larkin-owned project settings that Pi deep-merges over the user's global agent dir. */
|
|
178
|
+
function projectPiSettingsFile(workspaceDir) {
|
|
179
|
+
return node_path_1.default.join(workspaceDir, ".pi", "settings.json");
|
|
177
180
|
}
|
|
178
|
-
function
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
writeOwnedPiSettings(
|
|
183
|
-
return
|
|
181
|
+
function ownedPiSettings(workspaceDir) {
|
|
182
|
+
return projectPiSettingsFile(workspaceDir);
|
|
183
|
+
}
|
|
184
|
+
function prepareOwnedPiDirectory(workspaceDir, compaction) {
|
|
185
|
+
writeOwnedPiSettings(workspaceDir, compaction);
|
|
186
|
+
return workspaceDir;
|
|
184
187
|
}
|
|
185
188
|
function mergeOwnedPiSettings(value, compaction = {
|
|
186
189
|
reserveTokens: exports.COMPACTION_RESERVE_TOKENS,
|
|
@@ -200,10 +203,11 @@ function mergeOwnedPiSettings(value, compaction = {
|
|
|
200
203
|
reserveTokens: compaction.reserveTokens, keepRecentTokens: compaction.keepRecentTokens,
|
|
201
204
|
} };
|
|
202
205
|
}
|
|
203
|
-
function writeOwnedPiSettings(
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
206
|
+
function writeOwnedPiSettings(workspaceDir, compaction) {
|
|
207
|
+
const projectDir = node_path_1.default.join(workspaceDir, ".pi");
|
|
208
|
+
ensureDirectoryChain(node_path_1.default.resolve(workspaceDir));
|
|
209
|
+
ensureDirectory(projectDir);
|
|
210
|
+
const file = projectPiSettingsFile(workspaceDir);
|
|
207
211
|
try {
|
|
208
212
|
const existing = node_fs_1.default.lstatSync(file);
|
|
209
213
|
if (existing.isSymbolicLink() || !existing.isFile())
|
|
@@ -221,7 +225,7 @@ function writeOwnedPiSettings(directory, compaction) {
|
|
|
221
225
|
if (error.code !== "ENOENT")
|
|
222
226
|
throw new Error("Pi owned settings are invalid");
|
|
223
227
|
}
|
|
224
|
-
const temporary = node_path_1.default.join(
|
|
228
|
+
const temporary = node_path_1.default.join(projectDir, `.${node_path_1.default.basename(file)}.${process.pid}.${node_crypto_1.default.randomUUID()}.tmp`);
|
|
225
229
|
try {
|
|
226
230
|
node_fs_1.default.writeFileSync(temporary, `${JSON.stringify(mergeOwnedPiSettings(value, compaction), null, 2)}\n`, { flag: "wx", mode: 0o600 });
|
|
227
231
|
node_fs_1.default.renameSync(temporary, file);
|
|
@@ -234,10 +238,10 @@ function writeOwnedPiSettings(directory, compaction) {
|
|
|
234
238
|
catch { /* renamed or absent */ }
|
|
235
239
|
}
|
|
236
240
|
}
|
|
237
|
-
function readOwnedPiSettings(
|
|
238
|
-
|
|
239
|
-
assertOwnedDirectory(
|
|
240
|
-
const file =
|
|
241
|
+
function readOwnedPiSettings(workspaceDir) {
|
|
242
|
+
const projectDir = node_path_1.default.join(workspaceDir, ".pi");
|
|
243
|
+
assertOwnedDirectory(projectDir);
|
|
244
|
+
const file = projectPiSettingsFile(workspaceDir);
|
|
241
245
|
const stat = node_fs_1.default.lstatSync(file);
|
|
242
246
|
if (stat.isSymbolicLink() || !stat.isFile())
|
|
243
247
|
throw new Error("Pi owned settings must be a regular file");
|
|
@@ -254,25 +258,73 @@ function readOwnedPiSettings(directory) {
|
|
|
254
258
|
}
|
|
255
259
|
return { compaction: { enabled, reserveTokens: values.reserveTokens, keepRecentTokens: values.keepRecentTokens } };
|
|
256
260
|
}
|
|
261
|
+
const PI_VERSION_LINE = /^(?:pi(?:-coding-agent)?(?:\s+version)?\s+v?)?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/i;
|
|
262
|
+
const PI_VERSION_CORE = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z.-]+))?$/;
|
|
263
|
+
function comparePiNumericVersions(left, right) {
|
|
264
|
+
const leftParts = left.split(".").map(Number);
|
|
265
|
+
const rightParts = right.split(".").map(Number);
|
|
266
|
+
for (let index = 0; index < 3; index += 1) {
|
|
267
|
+
if (leftParts[index] !== rightParts[index])
|
|
268
|
+
return Math.sign(leftParts[index] - rightParts[index]);
|
|
269
|
+
}
|
|
270
|
+
return 0;
|
|
271
|
+
}
|
|
272
|
+
function olderThanMinimumPiVersionMessage(version) {
|
|
273
|
+
return `Pi executable version ${version} is older than the minimum ${exports.MINIMUM_PI_VERSION}`;
|
|
274
|
+
}
|
|
275
|
+
function assertSupportedPiVersion(version) {
|
|
276
|
+
if (typeof version !== "string")
|
|
277
|
+
throw new Error(`Pi executable version ${exports.MINIMUM_PI_VERSION} or newer is required`);
|
|
278
|
+
const raw = version.trim();
|
|
279
|
+
const match = PI_VERSION_CORE.exec(raw);
|
|
280
|
+
if (!match)
|
|
281
|
+
throw new Error(`Pi executable version ${raw || "missing"} is not a supported version`);
|
|
282
|
+
const numeric = `${match[1]}.${match[2]}.${match[3]}`;
|
|
283
|
+
if (match[4]) {
|
|
284
|
+
throw new Error(`Pi executable version ${raw} is unsupported: SemVer ${raw} < ${numeric}; Larkin supports stable external pi only`);
|
|
285
|
+
}
|
|
286
|
+
if (comparePiNumericVersions(numeric, exports.MINIMUM_PI_VERSION) < 0) {
|
|
287
|
+
throw new Error(olderThanMinimumPiVersionMessage(numeric));
|
|
288
|
+
}
|
|
289
|
+
return numeric;
|
|
290
|
+
}
|
|
257
291
|
function parsePiExecutableVersion(output) {
|
|
258
292
|
const lines = output.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
259
293
|
if (lines.length !== 1)
|
|
260
294
|
throw new Error("Pi executable version output must contain exactly one version line");
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
295
|
+
const match = PI_VERSION_LINE.exec(lines[0]);
|
|
296
|
+
if (!match)
|
|
297
|
+
throw new Error("Pi executable version output is not a supported version line");
|
|
298
|
+
const version = `${match[1]}.${match[2]}.${match[3]}`;
|
|
299
|
+
if (comparePiNumericVersions(version, exports.MINIMUM_PI_VERSION) < 0) {
|
|
300
|
+
throw new Error(olderThanMinimumPiVersionMessage(version));
|
|
301
|
+
}
|
|
302
|
+
return version;
|
|
303
|
+
}
|
|
304
|
+
function resolveOptionalHandshake(capabilities) {
|
|
305
|
+
if (capabilities.compactionCapabilities !== undefined && capabilities.compactionCapabilities !== null) {
|
|
306
|
+
return capabilities.compactionCapabilities;
|
|
307
|
+
}
|
|
308
|
+
if (capabilities.reserveTokens !== undefined
|
|
309
|
+
|| capabilities.keepRecentTokens !== undefined
|
|
310
|
+
|| capabilities.events !== undefined) {
|
|
311
|
+
return {
|
|
312
|
+
reserveTokens: capabilities.reserveTokens,
|
|
313
|
+
keepRecentTokens: capabilities.keepRecentTokens,
|
|
314
|
+
events: capabilities.events,
|
|
315
|
+
};
|
|
266
316
|
}
|
|
267
|
-
return
|
|
317
|
+
return undefined;
|
|
268
318
|
}
|
|
269
319
|
function verifyPiCapabilities(capabilities) {
|
|
270
|
-
|
|
271
|
-
throw new Error(`trusted Pi version ${exports.PINNED_PI_VERSION} is required`);
|
|
320
|
+
assertSupportedPiVersion(capabilities.version);
|
|
272
321
|
if (capabilities.trustedProtocol === true) {
|
|
273
322
|
throw new Error("external Pi cannot use a trusted protocol bypass");
|
|
274
323
|
}
|
|
275
324
|
const contextWindow = capabilities.contextWindow ?? capabilities.model?.contextWindow;
|
|
325
|
+
if (typeof contextWindow !== "number" || !Number.isFinite(contextWindow) || contextWindow <= 0) {
|
|
326
|
+
throw new Error("Pi capability context window is invalid");
|
|
327
|
+
}
|
|
276
328
|
const expected = (() => {
|
|
277
329
|
try {
|
|
278
330
|
return calculatePiCompactionSettings(contextWindow);
|
|
@@ -286,14 +338,18 @@ function verifyPiCapabilities(capabilities) {
|
|
|
286
338
|
}
|
|
287
339
|
if (capabilities.compactRpc !== true)
|
|
288
340
|
throw new Error("Pi compact RPC capability is missing");
|
|
289
|
-
|
|
290
|
-
|
|
341
|
+
const handshake = resolveOptionalHandshake(capabilities);
|
|
342
|
+
if (handshake === undefined)
|
|
343
|
+
return "larkin-settings-only";
|
|
344
|
+
if (handshake.reserveTokens !== expected.reserveTokens
|
|
345
|
+
|| handshake.keepRecentTokens !== expected.keepRecentTokens) {
|
|
291
346
|
throw new Error(`Pi external effective compaction reserve/keep settings are unproven for context window ${contextWindow}`);
|
|
292
347
|
}
|
|
293
|
-
const events = new Set(
|
|
348
|
+
const events = new Set(handshake.events || []);
|
|
294
349
|
for (const event of REQUIRED_PI_EVENTS)
|
|
295
350
|
if (!events.has(event))
|
|
296
351
|
throw new Error(`Pi capability event is unproven: ${event}`);
|
|
352
|
+
return "handshake-reported";
|
|
297
353
|
}
|
|
298
354
|
function stateFile(root) {
|
|
299
355
|
return node_path_1.default.join(root, exports.PI_COMPACTION_RECOVERY_FILE);
|
|
@@ -671,11 +727,6 @@ class PiCompactionRecoveryMachine {
|
|
|
671
727
|
}
|
|
672
728
|
}
|
|
673
729
|
exports.PiCompactionRecoveryMachine = PiCompactionRecoveryMachine;
|
|
674
|
-
function prepareOwnedPiEnvironment(input) {
|
|
675
|
-
const directory = ensureOwnedPiAgentDirectory(input.root, input.agentId);
|
|
676
|
-
writeOwnedPiSettings(directory);
|
|
677
|
-
return { ...input.env, PI_CODING_AGENT_DIR: directory };
|
|
678
|
-
}
|
|
679
730
|
function readProjectPiSettings(workspaceDir) {
|
|
680
731
|
const file = node_path_1.default.join(workspaceDir, ".pi", "settings.json");
|
|
681
732
|
try {
|
|
@@ -2,7 +2,8 @@ import crypto from "node:crypto";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
|
|
5
|
+
/** External `pi` must be this version or newer. Stock 0.84.x does not emit compactionCapabilities. */
|
|
6
|
+
export const MINIMUM_PI_VERSION = "0.84.2";
|
|
6
7
|
/** Fallback values used while importing a Pi profile, before Pi reports its model. */
|
|
7
8
|
export const PI_CONTEXT_WINDOW = 272_000;
|
|
8
9
|
export const COMPACTION_RESERVE_TOKENS = 40_800;
|
|
@@ -150,15 +151,16 @@ export function ensureOwnedPiAgentDirectory(root, agentId) {
|
|
|
150
151
|
ensureDirectory(directory);
|
|
151
152
|
return directory;
|
|
152
153
|
}
|
|
153
|
-
|
|
154
|
-
|
|
154
|
+
/** Larkin-owned project settings that Pi deep-merges over the user's global agent dir. */
|
|
155
|
+
export function projectPiSettingsFile(workspaceDir) {
|
|
156
|
+
return path.join(workspaceDir, ".pi", "settings.json");
|
|
155
157
|
}
|
|
156
|
-
export function
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
writeOwnedPiSettings(
|
|
161
|
-
return
|
|
158
|
+
export function ownedPiSettings(workspaceDir) {
|
|
159
|
+
return projectPiSettingsFile(workspaceDir);
|
|
160
|
+
}
|
|
161
|
+
export function prepareOwnedPiDirectory(workspaceDir, compaction) {
|
|
162
|
+
writeOwnedPiSettings(workspaceDir, compaction);
|
|
163
|
+
return workspaceDir;
|
|
162
164
|
}
|
|
163
165
|
export function mergeOwnedPiSettings(value, compaction = {
|
|
164
166
|
reserveTokens: COMPACTION_RESERVE_TOKENS,
|
|
@@ -178,10 +180,11 @@ export function mergeOwnedPiSettings(value, compaction = {
|
|
|
178
180
|
reserveTokens: compaction.reserveTokens, keepRecentTokens: compaction.keepRecentTokens,
|
|
179
181
|
} };
|
|
180
182
|
}
|
|
181
|
-
export function writeOwnedPiSettings(
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
183
|
+
export function writeOwnedPiSettings(workspaceDir, compaction) {
|
|
184
|
+
const projectDir = path.join(workspaceDir, ".pi");
|
|
185
|
+
ensureDirectoryChain(path.resolve(workspaceDir));
|
|
186
|
+
ensureDirectory(projectDir);
|
|
187
|
+
const file = projectPiSettingsFile(workspaceDir);
|
|
185
188
|
try {
|
|
186
189
|
const existing = fs.lstatSync(file);
|
|
187
190
|
if (existing.isSymbolicLink() || !existing.isFile())
|
|
@@ -199,7 +202,7 @@ export function writeOwnedPiSettings(directory, compaction) {
|
|
|
199
202
|
if (error.code !== "ENOENT")
|
|
200
203
|
throw new Error("Pi owned settings are invalid");
|
|
201
204
|
}
|
|
202
|
-
const temporary = path.join(
|
|
205
|
+
const temporary = path.join(projectDir, `.${path.basename(file)}.${process.pid}.${crypto.randomUUID()}.tmp`);
|
|
203
206
|
try {
|
|
204
207
|
fs.writeFileSync(temporary, `${JSON.stringify(mergeOwnedPiSettings(value, compaction), null, 2)}\n`, { flag: "wx", mode: 0o600 });
|
|
205
208
|
fs.renameSync(temporary, file);
|
|
@@ -212,10 +215,10 @@ export function writeOwnedPiSettings(directory, compaction) {
|
|
|
212
215
|
catch { /* renamed or absent */ }
|
|
213
216
|
}
|
|
214
217
|
}
|
|
215
|
-
export function readOwnedPiSettings(
|
|
216
|
-
|
|
217
|
-
assertOwnedDirectory(
|
|
218
|
-
const file =
|
|
218
|
+
export function readOwnedPiSettings(workspaceDir) {
|
|
219
|
+
const projectDir = path.join(workspaceDir, ".pi");
|
|
220
|
+
assertOwnedDirectory(projectDir);
|
|
221
|
+
const file = projectPiSettingsFile(workspaceDir);
|
|
219
222
|
const stat = fs.lstatSync(file);
|
|
220
223
|
if (stat.isSymbolicLink() || !stat.isFile())
|
|
221
224
|
throw new Error("Pi owned settings must be a regular file");
|
|
@@ -232,25 +235,73 @@ export function readOwnedPiSettings(directory) {
|
|
|
232
235
|
}
|
|
233
236
|
return { compaction: { enabled, reserveTokens: values.reserveTokens, keepRecentTokens: values.keepRecentTokens } };
|
|
234
237
|
}
|
|
238
|
+
const PI_VERSION_LINE = /^(?:pi(?:-coding-agent)?(?:\s+version)?\s+v?)?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/i;
|
|
239
|
+
const PI_VERSION_CORE = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z.-]+))?$/;
|
|
240
|
+
function comparePiNumericVersions(left, right) {
|
|
241
|
+
const leftParts = left.split(".").map(Number);
|
|
242
|
+
const rightParts = right.split(".").map(Number);
|
|
243
|
+
for (let index = 0; index < 3; index += 1) {
|
|
244
|
+
if (leftParts[index] !== rightParts[index])
|
|
245
|
+
return Math.sign(leftParts[index] - rightParts[index]);
|
|
246
|
+
}
|
|
247
|
+
return 0;
|
|
248
|
+
}
|
|
249
|
+
export function olderThanMinimumPiVersionMessage(version) {
|
|
250
|
+
return `Pi executable version ${version} is older than the minimum ${MINIMUM_PI_VERSION}`;
|
|
251
|
+
}
|
|
252
|
+
function assertSupportedPiVersion(version) {
|
|
253
|
+
if (typeof version !== "string")
|
|
254
|
+
throw new Error(`Pi executable version ${MINIMUM_PI_VERSION} or newer is required`);
|
|
255
|
+
const raw = version.trim();
|
|
256
|
+
const match = PI_VERSION_CORE.exec(raw);
|
|
257
|
+
if (!match)
|
|
258
|
+
throw new Error(`Pi executable version ${raw || "missing"} is not a supported version`);
|
|
259
|
+
const numeric = `${match[1]}.${match[2]}.${match[3]}`;
|
|
260
|
+
if (match[4]) {
|
|
261
|
+
throw new Error(`Pi executable version ${raw} is unsupported: SemVer ${raw} < ${numeric}; Larkin supports stable external pi only`);
|
|
262
|
+
}
|
|
263
|
+
if (comparePiNumericVersions(numeric, MINIMUM_PI_VERSION) < 0) {
|
|
264
|
+
throw new Error(olderThanMinimumPiVersionMessage(numeric));
|
|
265
|
+
}
|
|
266
|
+
return numeric;
|
|
267
|
+
}
|
|
235
268
|
export function parsePiExecutableVersion(output) {
|
|
236
269
|
const lines = output.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
237
270
|
if (lines.length !== 1)
|
|
238
271
|
throw new Error("Pi executable version output must contain exactly one version line");
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
272
|
+
const match = PI_VERSION_LINE.exec(lines[0]);
|
|
273
|
+
if (!match)
|
|
274
|
+
throw new Error("Pi executable version output is not a supported version line");
|
|
275
|
+
const version = `${match[1]}.${match[2]}.${match[3]}`;
|
|
276
|
+
if (comparePiNumericVersions(version, MINIMUM_PI_VERSION) < 0) {
|
|
277
|
+
throw new Error(olderThanMinimumPiVersionMessage(version));
|
|
278
|
+
}
|
|
279
|
+
return version;
|
|
280
|
+
}
|
|
281
|
+
function resolveOptionalHandshake(capabilities) {
|
|
282
|
+
if (capabilities.compactionCapabilities !== undefined && capabilities.compactionCapabilities !== null) {
|
|
283
|
+
return capabilities.compactionCapabilities;
|
|
284
|
+
}
|
|
285
|
+
if (capabilities.reserveTokens !== undefined
|
|
286
|
+
|| capabilities.keepRecentTokens !== undefined
|
|
287
|
+
|| capabilities.events !== undefined) {
|
|
288
|
+
return {
|
|
289
|
+
reserveTokens: capabilities.reserveTokens,
|
|
290
|
+
keepRecentTokens: capabilities.keepRecentTokens,
|
|
291
|
+
events: capabilities.events,
|
|
292
|
+
};
|
|
244
293
|
}
|
|
245
|
-
return
|
|
294
|
+
return undefined;
|
|
246
295
|
}
|
|
247
296
|
export function verifyPiCapabilities(capabilities) {
|
|
248
|
-
|
|
249
|
-
throw new Error(`trusted Pi version ${PINNED_PI_VERSION} is required`);
|
|
297
|
+
assertSupportedPiVersion(capabilities.version);
|
|
250
298
|
if (capabilities.trustedProtocol === true) {
|
|
251
299
|
throw new Error("external Pi cannot use a trusted protocol bypass");
|
|
252
300
|
}
|
|
253
301
|
const contextWindow = capabilities.contextWindow ?? capabilities.model?.contextWindow;
|
|
302
|
+
if (typeof contextWindow !== "number" || !Number.isFinite(contextWindow) || contextWindow <= 0) {
|
|
303
|
+
throw new Error("Pi capability context window is invalid");
|
|
304
|
+
}
|
|
254
305
|
const expected = (() => {
|
|
255
306
|
try {
|
|
256
307
|
return calculatePiCompactionSettings(contextWindow);
|
|
@@ -264,14 +315,18 @@ export function verifyPiCapabilities(capabilities) {
|
|
|
264
315
|
}
|
|
265
316
|
if (capabilities.compactRpc !== true)
|
|
266
317
|
throw new Error("Pi compact RPC capability is missing");
|
|
267
|
-
|
|
268
|
-
|
|
318
|
+
const handshake = resolveOptionalHandshake(capabilities);
|
|
319
|
+
if (handshake === undefined)
|
|
320
|
+
return "larkin-settings-only";
|
|
321
|
+
if (handshake.reserveTokens !== expected.reserveTokens
|
|
322
|
+
|| handshake.keepRecentTokens !== expected.keepRecentTokens) {
|
|
269
323
|
throw new Error(`Pi external effective compaction reserve/keep settings are unproven for context window ${contextWindow}`);
|
|
270
324
|
}
|
|
271
|
-
const events = new Set(
|
|
325
|
+
const events = new Set(handshake.events || []);
|
|
272
326
|
for (const event of REQUIRED_PI_EVENTS)
|
|
273
327
|
if (!events.has(event))
|
|
274
328
|
throw new Error(`Pi capability event is unproven: ${event}`);
|
|
329
|
+
return "handshake-reported";
|
|
275
330
|
}
|
|
276
331
|
function stateFile(root) {
|
|
277
332
|
return path.join(root, PI_COMPACTION_RECOVERY_FILE);
|
|
@@ -647,11 +702,6 @@ export class PiCompactionRecoveryMachine {
|
|
|
647
702
|
this.change("closed");
|
|
648
703
|
}
|
|
649
704
|
}
|
|
650
|
-
export function prepareOwnedPiEnvironment(input) {
|
|
651
|
-
const directory = ensureOwnedPiAgentDirectory(input.root, input.agentId);
|
|
652
|
-
writeOwnedPiSettings(directory);
|
|
653
|
-
return { ...input.env, PI_CODING_AGENT_DIR: directory };
|
|
654
|
-
}
|
|
655
705
|
export function readProjectPiSettings(workspaceDir) {
|
|
656
706
|
const file = path.join(workspaceDir, ".pi", "settings.json");
|
|
657
707
|
try {
|
|
@@ -1,37 +1,4 @@
|
|
|
1
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
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
3
|
exports.PI_THINKING_LEVELS = void 0;
|
|
37
4
|
exports.piModelId = piModelId;
|
|
@@ -40,7 +7,6 @@ exports.isPiThinkingLevel = isPiThinkingLevel;
|
|
|
40
7
|
exports.findExactPiModel = findExactPiModel;
|
|
41
8
|
exports.discoverPiModelCatalog = discoverPiModelCatalog;
|
|
42
9
|
const node_child_process_1 = require("node:child_process");
|
|
43
|
-
const path = __importStar(require("node:path"));
|
|
44
10
|
const pi_rpc_client_js_1 = require("./pi-rpc-client.cjs");
|
|
45
11
|
const runtime_readiness_js_1 = require("./runtime-readiness.cjs");
|
|
46
12
|
exports.PI_THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
|
|
@@ -77,17 +43,15 @@ function piCatalogChildEnv(options) {
|
|
|
77
43
|
...options.env,
|
|
78
44
|
NO_COLOR: "1",
|
|
79
45
|
};
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
else
|
|
83
|
-
delete env.PI_CODING_AGENT_DIR;
|
|
46
|
+
// External pi 必须看见用户自己的 home;Larkin 从不注入或转发 PI_CODING_AGENT_DIR。
|
|
47
|
+
delete env.PI_CODING_AGENT_DIR;
|
|
84
48
|
return env;
|
|
85
49
|
}
|
|
86
50
|
async function discoverPiModelCatalog(options) {
|
|
87
51
|
if (!options.spawn) {
|
|
88
52
|
const command = options.command ?? options.env?.LARKIN_PI_COMMAND ?? process.env.LARKIN_PI_COMMAND ?? "pi";
|
|
89
53
|
const childEnv = piCatalogChildEnv(options);
|
|
90
|
-
const key = `${command}|${(options.commandArgs ?? []).join("\0")}|${childEnv.
|
|
54
|
+
const key = `${command}|${(options.commandArgs ?? []).join("\0")}|${childEnv.PI_PACKAGE_DIR ?? ""}|${options.packageDir ?? ""}`;
|
|
91
55
|
const cached = discoveryCache.get(key);
|
|
92
56
|
if (cached)
|
|
93
57
|
return cached;
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { spawn as nodeSpawn } from "node:child_process";
|
|
2
|
-
import * as path from "node:path";
|
|
3
2
|
import { PiRpcClient } from "./pi-rpc-client.mjs";
|
|
4
3
|
import { classifyRuntimePrerequisite, RuntimePrerequisiteError } from "./runtime-readiness.mjs";
|
|
5
4
|
export const PI_THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
|
|
@@ -36,17 +35,15 @@ function piCatalogChildEnv(options) {
|
|
|
36
35
|
...options.env,
|
|
37
36
|
NO_COLOR: "1",
|
|
38
37
|
};
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
else
|
|
42
|
-
delete env.PI_CODING_AGENT_DIR;
|
|
38
|
+
// External pi 必须看见用户自己的 home;Larkin 从不注入或转发 PI_CODING_AGENT_DIR。
|
|
39
|
+
delete env.PI_CODING_AGENT_DIR;
|
|
43
40
|
return env;
|
|
44
41
|
}
|
|
45
42
|
export async function discoverPiModelCatalog(options) {
|
|
46
43
|
if (!options.spawn) {
|
|
47
44
|
const command = options.command ?? options.env?.LARKIN_PI_COMMAND ?? process.env.LARKIN_PI_COMMAND ?? "pi";
|
|
48
45
|
const childEnv = piCatalogChildEnv(options);
|
|
49
|
-
const key = `${command}|${(options.commandArgs ?? []).join("\0")}|${childEnv.
|
|
46
|
+
const key = `${command}|${(options.commandArgs ?? []).join("\0")}|${childEnv.PI_PACKAGE_DIR ?? ""}|${options.packageDir ?? ""}`;
|
|
50
47
|
const cached = discoveryCache.get(key);
|
|
51
48
|
if (cached)
|
|
52
49
|
return cached;
|
|
@@ -7,8 +7,9 @@ import { fileURLToPath } from "node:url";
|
|
|
7
7
|
*
|
|
8
8
|
* 分发:构建期把 @tintinweb/pi-subagents bundle 成单文件
|
|
9
9
|
* `dist/runtime/pi-subagents.bundle.js`(pi-* 包 external),运行时仅向 external
|
|
10
|
-
* (用户 pi CLI)通过 `pi --extension/-e`
|
|
11
|
-
*
|
|
10
|
+
* (用户 pi CLI)通过 `pi --extension/-e` 显式注入。可只读用户 ~/.pi/agent
|
|
11
|
+
* 的非密钥设置/扩展元数据以避免重复注册 FATAL;不得读、复制或写 auth.json,
|
|
12
|
+
* 也不得写入 ~/.pi。
|
|
12
13
|
*
|
|
13
14
|
* 版本门槛:pi-subagents peerDependency 要求 @earendil-works/pi-* >= 0.80.0;
|
|
14
15
|
* external 的 pi 版本低于 0.80 时不注入(降级为无 subagent 能力)。
|
|
@@ -184,16 +185,49 @@ export function probeExternalPiVersion(piCommand, env) {
|
|
|
184
185
|
* 用户 pi 是否已自行安装 pi-subagents(settings.json packages 或包目录)。
|
|
185
186
|
* 已装时 Larkin 不再 -e 注入,避免同名工具(Agent/get_subagent_result/steer_subagent)
|
|
186
187
|
* 重复注册导致 pi 扩展加载 FATAL(pi 对 duplicate tool registration 是硬失败)。
|
|
188
|
+
*
|
|
189
|
+
* Owner 边界:只读非密钥兼容信息(settings.json / package 清单 / 扩展源码标记)。
|
|
190
|
+
* 不得打开或写入用户 Pi home 里的 auth.json,也不得往 ~/.pi 写任何文件。
|
|
187
191
|
*/
|
|
188
192
|
const BOUNDED_WAIT_CAPABILITY = "larkin-pi-subagents-bounded-wait-v1";
|
|
189
193
|
const SUPERVISED_COMMAND_CAPABILITY = "larkin-pi-supervised-command-v1";
|
|
194
|
+
function assertRegularFile(file) {
|
|
195
|
+
const stat = fs.lstatSync(file);
|
|
196
|
+
if (stat.isSymbolicLink() || !stat.isFile() || stat.nlink !== 1) {
|
|
197
|
+
throw new Error("user Pi path is not a regular file");
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
function readRegularFile(file) {
|
|
201
|
+
assertRegularFile(file);
|
|
202
|
+
return fs.readFileSync(file, "utf8");
|
|
203
|
+
}
|
|
204
|
+
function isRealpathInside(root, candidate) {
|
|
205
|
+
const rootReal = fs.realpathSync(root);
|
|
206
|
+
const candidateReal = fs.realpathSync(candidate);
|
|
207
|
+
const prefix = rootReal.endsWith(path.sep) ? rootReal : `${rootReal}${path.sep}`;
|
|
208
|
+
return candidateReal.startsWith(prefix);
|
|
209
|
+
}
|
|
210
|
+
/** 只读包根内的单链接常规文件;绝对路径、逃逸、符号链接、硬链接一律视为不可验证。 */
|
|
211
|
+
function readContainedPackageEntry(packageRoot, entry) {
|
|
212
|
+
if (path.isAbsolute(entry))
|
|
213
|
+
throw new Error("absolute Pi extension entry");
|
|
214
|
+
const root = path.resolve(packageRoot);
|
|
215
|
+
const resolved = path.resolve(root, entry);
|
|
216
|
+
const lexicalPrefix = root.endsWith(path.sep) ? root : `${root}${path.sep}`;
|
|
217
|
+
if (resolved !== root && !resolved.startsWith(lexicalPrefix))
|
|
218
|
+
throw new Error("Pi extension entry escaped package root");
|
|
219
|
+
assertRegularFile(resolved);
|
|
220
|
+
if (!isRealpathInside(root, resolved))
|
|
221
|
+
throw new Error("Pi extension entry escaped package root");
|
|
222
|
+
return fs.readFileSync(resolved, "utf8");
|
|
223
|
+
}
|
|
190
224
|
function userPiSubagentsWaitCapability(env) {
|
|
191
225
|
const agentDir = env.PI_CODING_AGENT_DIR || path.join(env.HOME || process.env.HOME || "", ".pi", "agent");
|
|
192
226
|
try {
|
|
193
227
|
let configured = false;
|
|
194
228
|
const settingsFile = path.join(agentDir, "settings.json");
|
|
195
229
|
if (fs.existsSync(settingsFile)) {
|
|
196
|
-
const settings = JSON.parse(
|
|
230
|
+
const settings = JSON.parse(readRegularFile(settingsFile));
|
|
197
231
|
const packages = settings?.packages;
|
|
198
232
|
if (Array.isArray(packages)) {
|
|
199
233
|
configured = packages.some((entry) => typeof entry === "string" && /pi-subagents/i.test(entry));
|
|
@@ -210,7 +244,7 @@ function userPiSubagentsWaitCapability(env) {
|
|
|
210
244
|
return "absent";
|
|
211
245
|
if (!installedRoot)
|
|
212
246
|
return "unbounded";
|
|
213
|
-
const packageManifest = JSON.parse(
|
|
247
|
+
const packageManifest = JSON.parse(readRegularFile(path.join(installedRoot, "package.json")));
|
|
214
248
|
const extensions = packageManifest.pi?.extensions;
|
|
215
249
|
if (!Array.isArray(extensions) || extensions.length === 0
|
|
216
250
|
|| !extensions.every((entry) => typeof entry === "string" && entry.length > 0)) {
|
|
@@ -218,20 +252,14 @@ function userPiSubagentsWaitCapability(env) {
|
|
|
218
252
|
}
|
|
219
253
|
// Pi executes the entries declared by the package manifest. Do not trust a
|
|
220
254
|
// marker in a build artifact when the manifest points Pi at another file.
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
const source = fs.readFileSync(file, "utf8");
|
|
225
|
-
return source.includes(BOUNDED_WAIT_CAPABILITY) && source.includes(SUPERVISED_COMMAND_CAPABILITY);
|
|
226
|
-
}
|
|
227
|
-
catch {
|
|
228
|
-
return false;
|
|
229
|
-
}
|
|
255
|
+
const hasCapability = extensions.every((entry) => {
|
|
256
|
+
const source = readContainedPackageEntry(installedRoot, entry);
|
|
257
|
+
return source.includes(BOUNDED_WAIT_CAPABILITY) && source.includes(SUPERVISED_COMMAND_CAPABILITY);
|
|
230
258
|
});
|
|
231
259
|
return hasCapability ? "bounded" : "unbounded";
|
|
232
260
|
}
|
|
233
261
|
catch {
|
|
234
|
-
// Unreadable user extension state is not safe to treat as bounded.
|
|
262
|
+
// Unreadable or escaped user extension state is not safe to treat as bounded.
|
|
235
263
|
return "unbounded";
|
|
236
264
|
}
|
|
237
265
|
}
|
|
@@ -5,7 +5,6 @@ import * as path from "node:path";
|
|
|
5
5
|
import { createInterface } from "node:readline";
|
|
6
6
|
import { isPiThinkingLevel } from "./pi-model-catalog.mjs";
|
|
7
7
|
import { PiRpcClient } from "./pi-rpc-client.mjs";
|
|
8
|
-
import { recordPiRuntimeArtifactProvenance } from "./pi-artifact-provenance.mjs";
|
|
9
8
|
import { traceProcessBoundary } from "../platform/process-boundary-trace.mjs";
|
|
10
9
|
import { resolvePiSubagentExtensionArg } from "./pi-subagent-injection.mjs";
|
|
11
10
|
import { extractCanonicalPiSubagentNotification, ledgerStatusFromPiNotificationStatus, } from "./pi-subagents-notification.mjs";
|
|
@@ -14,9 +13,12 @@ import { effectivePiStateDir, extractBackgroundPiSubagentDispatch } from "./pi-s
|
|
|
14
13
|
import { resolvePiBashTimeoutExtensionArg } from "./pi-bash-timeout-injection.mjs";
|
|
15
14
|
import { classifyPiMissingCredentialRejection, classifyRuntimePrerequisite, missingProviderCredentialReadiness, probeNativeRuntimeReadiness, providerAuthenticationFailureReadiness, RuntimePrerequisiteError, } from "./runtime-readiness.mjs";
|
|
16
15
|
import { buildStrictProviderErrorInput, classifyStrictProviderError, INTERNAL_CONTEXT_WINDOW_PROJECTION_REASON, } from "./provider-error-classifier.mjs";
|
|
17
|
-
import { assertEffectivePiCompactionSettings,
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
import { assertEffectivePiCompactionSettings, MINIMUM_PI_VERSION, calculatePiCompactionSettings, parsePiExecutableVersion, projectPiSettingsFile, readOwnedPiSettings, verifyPiCapabilities, writeOwnedPiSettings, } from "./pi-compaction-recovery.mjs";
|
|
17
|
+
/** External pi must see the user's own agent dir; Larkin never injects or honors PI_CODING_AGENT_DIR. */
|
|
18
|
+
function stripPiCodingAgentDir(env) {
|
|
19
|
+
const next = { ...env };
|
|
20
|
+
delete next.PI_CODING_AGENT_DIR;
|
|
21
|
+
return next;
|
|
20
22
|
}
|
|
21
23
|
class EventSession {
|
|
22
24
|
listeners = new Set();
|
|
@@ -977,19 +979,12 @@ class PiRpcBackend {
|
|
|
977
979
|
this.sessionFile = state.sessionFile;
|
|
978
980
|
if (typeof state.sessionId === "string" && state.sessionId)
|
|
979
981
|
this.sessionId = state.sessionId;
|
|
980
|
-
//
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
reserveTokens: handshake.reserveTokens,
|
|
987
|
-
keepRecentTokens: handshake.keepRecentTokens,
|
|
988
|
-
} });
|
|
989
|
-
return;
|
|
990
|
-
}
|
|
991
|
-
if (state.autoCompactionEnabled !== true)
|
|
992
|
-
throw new Error("Pi native compaction must be enabled");
|
|
982
|
+
// Prompt 只信 live get_state:有 handshake 就必须精确匹配,没有则只要求 compaction 仍开启。
|
|
983
|
+
verifyPiCapabilities({
|
|
984
|
+
distribution: "external", version: this.policy.version,
|
|
985
|
+
contextWindow: state.model?.contextWindow, autoCompactionEnabled: state.autoCompactionEnabled,
|
|
986
|
+
compactRpc: true, compactionCapabilities: state.compactionCapabilities,
|
|
987
|
+
});
|
|
993
988
|
}
|
|
994
989
|
getState() { return this.client.request("get_state"); }
|
|
995
990
|
dispose() { return this.client.close(); }
|
|
@@ -1017,13 +1012,11 @@ export function resolvePiProcessExtensionArgs(input, resolvers = {}) {
|
|
|
1017
1012
|
}
|
|
1018
1013
|
async function createPiRpcBackend(input, dependencies, spawn, productionSpawn = true) {
|
|
1019
1014
|
const stateRoot = effectivePiStateDir(input);
|
|
1020
|
-
const mergedEnv = { ...globalThis.process.env, ...dependencies.env, ...input.env, NO_COLOR: "1" };
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
prepareOwnedPiDirectory(ownedPiDirectory);
|
|
1026
|
-
traceProcessBoundary(mergedEnv, "pi-rpc:child-env", { configDir: mergedEnv.LARKIN_CONFIG_DIR, agentId: input.agentId, targetDir: ownedPiDirectory, childEnvConfigDir: mergedEnv.LARKIN_CONFIG_DIR || null, childEnvHome: mergedEnv.HOME || null });
|
|
1015
|
+
const mergedEnv = stripPiCodingAgentDir({ ...globalThis.process.env, ...dependencies.env, ...input.env, NO_COLOR: "1" });
|
|
1016
|
+
if (mergedEnv.LARKIN_PI_DISTRIBUTION === "builtin")
|
|
1017
|
+
delete mergedEnv.LARKIN_PI_DISTRIBUTION;
|
|
1018
|
+
const projectSettings = projectPiSettingsFile(input.workspaceDir);
|
|
1019
|
+
traceProcessBoundary(mergedEnv, "pi-rpc:child-env", { configDir: mergedEnv.LARKIN_CONFIG_DIR, agentId: input.agentId, targetDir: projectSettings, childEnvConfigDir: mergedEnv.LARKIN_CONFIG_DIR || null, childEnvHome: mergedEnv.HOME || null });
|
|
1027
1020
|
const runtimeDir = path.join(stateRoot, "runtime");
|
|
1028
1021
|
fs.mkdirSync(runtimeDir, { recursive: true, mode: 0o700 });
|
|
1029
1022
|
const promptFile = path.join(runtimeDir, "pi-standing-prompt.md");
|
|
@@ -1033,22 +1026,20 @@ async function createPiRpcBackend(input, dependencies, spawn, productionSpawn =
|
|
|
1033
1026
|
const requestedEffort = input.reasoningEffort?.trim() || undefined;
|
|
1034
1027
|
if (requestedEffort && !isPiThinkingLevel(requestedEffort))
|
|
1035
1028
|
throw new Error(`Unknown Pi thinking level: ${requestedEffort}`);
|
|
1036
|
-
|
|
1029
|
+
// `--approve` 让 RPC 在无 UI 时仍加载工作区 `.pi/settings.json`(否则 settings.json 会触发 trust gate 并被忽略)。
|
|
1030
|
+
const args = ["--mode", "rpc", "--approve", "--session-dir", session.sessionDir, "--append-system-prompt", promptFile,
|
|
1037
1031
|
...(session.sessionFile ? ["--session", session.sessionFile] : []),
|
|
1038
1032
|
...(requestedModel ? ["--model", requestedModel] : []),
|
|
1039
1033
|
...(requestedEffort ? ["--thinking", requestedEffort] : [])];
|
|
1040
|
-
if (mergedEnv.LARKIN_PI_DISTRIBUTION === "builtin")
|
|
1041
|
-
delete mergedEnv.LARKIN_PI_DISTRIBUTION;
|
|
1042
1034
|
const command = dependencies.piCommand ?? dependencies.env?.LARKIN_PI_COMMAND ?? process.env.LARKIN_PI_COMMAND ?? "pi";
|
|
1043
1035
|
const commandPrefix = dependencies.piCommandArgs ?? [];
|
|
1044
1036
|
const commandArgs = [...commandPrefix, ...args];
|
|
1045
|
-
|
|
1046
|
-
const childEnv = { ...mergedEnv };
|
|
1037
|
+
const childEnv = stripPiCodingAgentDir(mergedEnv);
|
|
1047
1038
|
const reportedVersion = productionSpawn
|
|
1048
1039
|
? parsePiExecutableVersion(String(spawnSync(command, [...commandPrefix, "--version"], {
|
|
1049
1040
|
cwd: input.workspaceDir, env: childEnv, encoding: "utf8", timeout: 5_000,
|
|
1050
1041
|
}).stdout || ""))
|
|
1051
|
-
:
|
|
1042
|
+
: MINIMUM_PI_VERSION;
|
|
1052
1043
|
const extensionInput = { piCommand: command, env: childEnv, platform: process.platform };
|
|
1053
1044
|
// 假 spawn 不需要真实 extension 路径;默认 resolver 会 spawnSync(`pi --version`)。
|
|
1054
1045
|
const extensionArgs = dependencies.resolvePiProcessExtensionArgs
|
|
@@ -1057,21 +1048,16 @@ async function createPiRpcBackend(input, dependencies, spawn, productionSpawn =
|
|
|
1057
1048
|
commandArgs.push(...extensionArgs);
|
|
1058
1049
|
let probedModel = null;
|
|
1059
1050
|
if (productionSpawn) {
|
|
1060
|
-
// Pi
|
|
1061
|
-
// requested model first, then write the matching 15% reserve before the
|
|
1062
|
-
// real session starts; no prompt, provider request, package, or extension
|
|
1063
|
-
// is loaded by the probe.
|
|
1051
|
+
// 隔离探测用户自己的 Pi home(无 PI_CODING_AGENT_DIR),再把 15% reserve 写到工作区项目设置。
|
|
1064
1052
|
probedModel = await discoverEffectivePiContextWindow(input, command, commandPrefix, requestedModel, childEnv, spawn, dependencies.piRpcClientOptions);
|
|
1065
|
-
writeOwnedPiSettings(
|
|
1053
|
+
writeOwnedPiSettings(input.workspaceDir, calculatePiCompactionSettings(probedModel.contextWindow));
|
|
1066
1054
|
}
|
|
1067
|
-
const artifactEntriesBeforeSpawn = new Set(fs.readdirSync(ownedPiDirectory));
|
|
1068
|
-
const artifactSpawnBoundary = Date.now();
|
|
1069
1055
|
const child = spawn(command, commandArgs, {
|
|
1070
1056
|
cwd: input.workspaceDir,
|
|
1071
1057
|
env: childEnv,
|
|
1072
1058
|
stdio: ["pipe", "pipe", "pipe"],
|
|
1073
1059
|
});
|
|
1074
|
-
traceProcessBoundary(mergedEnv, "pi-rpc:child-spawned", { configDir: mergedEnv.LARKIN_CONFIG_DIR, agentId: input.agentId, targetDir:
|
|
1060
|
+
traceProcessBoundary(mergedEnv, "pi-rpc:child-spawned", { configDir: mergedEnv.LARKIN_CONFIG_DIR, agentId: input.agentId, targetDir: projectSettings, childPid: child.pid ?? null });
|
|
1075
1061
|
const client = new PiRpcClient(child, dependencies.piRpcClientOptions);
|
|
1076
1062
|
try {
|
|
1077
1063
|
const [state, available] = await Promise.all([
|
|
@@ -1086,17 +1072,18 @@ async function createPiRpcBackend(input, dependencies, spawn, productionSpawn =
|
|
|
1086
1072
|
|| state.model?.contextWindow !== probedModel.contextWindow) {
|
|
1087
1073
|
throw new Error("Pi model or context window changed between the isolated probe and runtime startup");
|
|
1088
1074
|
}
|
|
1089
|
-
const ownedSettings = readOwnedPiSettings(
|
|
1075
|
+
const ownedSettings = readOwnedPiSettings(input.workspaceDir);
|
|
1090
1076
|
assertEffectivePiCompactionSettings({ contextWindow: state.model?.contextWindow, compaction: {
|
|
1091
1077
|
enabled: state.autoCompactionEnabled,
|
|
1092
1078
|
reserveTokens: ownedSettings.compaction?.reserveTokens,
|
|
1093
1079
|
keepRecentTokens: ownedSettings.compaction?.keepRecentTokens,
|
|
1094
1080
|
} });
|
|
1095
|
-
const
|
|
1096
|
-
|
|
1081
|
+
const policySource = verifyPiCapabilities({
|
|
1082
|
+
distribution: "external", version: reportedVersion,
|
|
1097
1083
|
contextWindow: state.model?.contextWindow, autoCompactionEnabled: state.autoCompactionEnabled,
|
|
1098
|
-
|
|
1099
|
-
|
|
1084
|
+
compactRpc: true, compactionCapabilities: state.compactionCapabilities,
|
|
1085
|
+
});
|
|
1086
|
+
process.stderr.write(`[runtime] Pi compaction policy source=${policySource} agent=${input.agentId} model=${effectiveModel} contextWindow=${state.model?.contextWindow}\n`);
|
|
1100
1087
|
}
|
|
1101
1088
|
// 严格对账(Owner 决策):模型必须来自 pi 的权威可用列表,不做后缀猜测——
|
|
1102
1089
|
// 配置的模型与 pi 可用列表不符即报错,并在错误里列出可用模型。
|
|
@@ -1110,9 +1097,8 @@ async function createPiRpcBackend(input, dependencies, spawn, productionSpawn =
|
|
|
1110
1097
|
}
|
|
1111
1098
|
if (requestedEffort && state.thinkingLevel !== requestedEffort)
|
|
1112
1099
|
throw new Error(`Pi thinking level ${requestedEffort} was not accepted by effective model ${effectiveModel || "unknown"}`);
|
|
1113
|
-
recordPiRuntimeArtifactProvenance(ownedPiDirectory, artifactEntriesBeforeSpawn, artifactSpawnBoundary);
|
|
1114
1100
|
return new PiRpcBackend(client, state, productionSpawn ? {
|
|
1115
|
-
model: effectiveModel, contextWindow: state.model?.contextWindow,
|
|
1101
|
+
model: effectiveModel, contextWindow: state.model?.contextWindow, version: reportedVersion,
|
|
1116
1102
|
} : undefined);
|
|
1117
1103
|
}
|
|
1118
1104
|
catch (error) {
|
|
@@ -53,6 +53,7 @@ exports.probeNativeRuntimeReadiness = probeNativeRuntimeReadiness;
|
|
|
53
53
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
54
54
|
const node_path_1 = __importDefault(require("node:path"));
|
|
55
55
|
const node_child_process_1 = require("node:child_process");
|
|
56
|
+
const pi_compaction_recovery_js_1 = require("./pi-compaction-recovery.cjs");
|
|
56
57
|
function safeProviderId(value) {
|
|
57
58
|
const provider = typeof value === "string" ? value.trim() : "";
|
|
58
59
|
return /^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$/.test(provider) ? provider : null;
|
|
@@ -191,6 +192,14 @@ function classifyRuntimePrerequisite(runtime, error, executable) {
|
|
|
191
192
|
runtime, state: "unauthenticated", ...(executable ? { executable } : {}), reason,
|
|
192
193
|
nextAction: runtimeLoginNextAction(runtime),
|
|
193
194
|
};
|
|
195
|
+
if (/older than the minimum|not a supported version(?: line)?|version output must contain exactly one/i.test(reason)) {
|
|
196
|
+
return {
|
|
197
|
+
runtime, state: "incompatible", ...(executable ? { executable } : {}), reason,
|
|
198
|
+
nextAction: runtime === "pi"
|
|
199
|
+
? `Upgrade pi to ${pi_compaction_recovery_js_1.MINIMUM_PI_VERSION} or newer`
|
|
200
|
+
: `Upgrade ${runtime}, then retry.`,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
194
203
|
if (/protocol (?:version )?(?:mismatch|unsupported|incompatible)|unsupported (?:rpc|protocol|schema)|schema (?:mismatch|incompatible)|requires (?:a )?newer version/i.test(reason)) {
|
|
195
204
|
return {
|
|
196
205
|
runtime, state: "incompatible", ...(executable ? { executable } : {}), reason,
|
|
@@ -228,6 +237,8 @@ function executableVersion(executable, env, commandArgs = []) {
|
|
|
228
237
|
/** Resolve and handshake through each runtime's structured native control protocol. */
|
|
229
238
|
async function probeNativeRuntimeReadiness(options) {
|
|
230
239
|
const env = { ...process.env, ...options.env };
|
|
240
|
+
if (options.runtime === "pi")
|
|
241
|
+
delete env.PI_CODING_AGENT_DIR;
|
|
231
242
|
const command = selectedCommand(options);
|
|
232
243
|
const executable = resolveRuntimeExecutable(command, env);
|
|
233
244
|
if (!executable) {
|
|
@@ -238,7 +249,19 @@ async function probeNativeRuntimeReadiness(options) {
|
|
|
238
249
|
nextAction: runtimeInstallNextAction(options.runtime),
|
|
239
250
|
};
|
|
240
251
|
}
|
|
241
|
-
|
|
252
|
+
let version = executableVersion(executable, env, options.commandArgs);
|
|
253
|
+
if (options.runtime === "pi") {
|
|
254
|
+
try {
|
|
255
|
+
const probe = (0, node_child_process_1.spawnSync)(executable, [...options.commandArgs ?? [], "--version"], {
|
|
256
|
+
env, encoding: "utf8", timeout: 5_000, maxBuffer: 64 * 1024,
|
|
257
|
+
});
|
|
258
|
+
version = (0, pi_compaction_recovery_js_1.parsePiExecutableVersion)(String(probe.stdout || ""));
|
|
259
|
+
}
|
|
260
|
+
catch (error) {
|
|
261
|
+
const classified = classifyRuntimePrerequisite("pi", error, executable);
|
|
262
|
+
return { ...classified, executable, ...(version ? { version } : {}) };
|
|
263
|
+
}
|
|
264
|
+
}
|
|
242
265
|
try {
|
|
243
266
|
if (options.runtime === "pi") {
|
|
244
267
|
const { discoverPiModelCatalog } = await Promise.resolve().then(() => __importStar(require("./pi-model-catalog.cjs")));
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { MINIMUM_PI_VERSION, parsePiExecutableVersion } from "./pi-compaction-recovery.mjs";
|
|
4
5
|
export function safeProviderId(value) {
|
|
5
6
|
const provider = typeof value === "string" ? value.trim() : "";
|
|
6
7
|
return /^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$/.test(provider) ? provider : null;
|
|
@@ -138,6 +139,14 @@ export function classifyRuntimePrerequisite(runtime, error, executable) {
|
|
|
138
139
|
runtime, state: "unauthenticated", ...(executable ? { executable } : {}), reason,
|
|
139
140
|
nextAction: runtimeLoginNextAction(runtime),
|
|
140
141
|
};
|
|
142
|
+
if (/older than the minimum|not a supported version(?: line)?|version output must contain exactly one/i.test(reason)) {
|
|
143
|
+
return {
|
|
144
|
+
runtime, state: "incompatible", ...(executable ? { executable } : {}), reason,
|
|
145
|
+
nextAction: runtime === "pi"
|
|
146
|
+
? `Upgrade pi to ${MINIMUM_PI_VERSION} or newer`
|
|
147
|
+
: `Upgrade ${runtime}, then retry.`,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
141
150
|
if (/protocol (?:version )?(?:mismatch|unsupported|incompatible)|unsupported (?:rpc|protocol|schema)|schema (?:mismatch|incompatible)|requires (?:a )?newer version/i.test(reason)) {
|
|
142
151
|
return {
|
|
143
152
|
runtime, state: "incompatible", ...(executable ? { executable } : {}), reason,
|
|
@@ -175,6 +184,8 @@ function executableVersion(executable, env, commandArgs = []) {
|
|
|
175
184
|
/** Resolve and handshake through each runtime's structured native control protocol. */
|
|
176
185
|
export async function probeNativeRuntimeReadiness(options) {
|
|
177
186
|
const env = { ...process.env, ...options.env };
|
|
187
|
+
if (options.runtime === "pi")
|
|
188
|
+
delete env.PI_CODING_AGENT_DIR;
|
|
178
189
|
const command = selectedCommand(options);
|
|
179
190
|
const executable = resolveRuntimeExecutable(command, env);
|
|
180
191
|
if (!executable) {
|
|
@@ -185,7 +196,19 @@ export async function probeNativeRuntimeReadiness(options) {
|
|
|
185
196
|
nextAction: runtimeInstallNextAction(options.runtime),
|
|
186
197
|
};
|
|
187
198
|
}
|
|
188
|
-
|
|
199
|
+
let version = executableVersion(executable, env, options.commandArgs);
|
|
200
|
+
if (options.runtime === "pi") {
|
|
201
|
+
try {
|
|
202
|
+
const probe = spawnSync(executable, [...options.commandArgs ?? [], "--version"], {
|
|
203
|
+
env, encoding: "utf8", timeout: 5_000, maxBuffer: 64 * 1024,
|
|
204
|
+
});
|
|
205
|
+
version = parsePiExecutableVersion(String(probe.stdout || ""));
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
const classified = classifyRuntimePrerequisite("pi", error, executable);
|
|
209
|
+
return { ...classified, executable, ...(version ? { version } : {}) };
|
|
210
|
+
}
|
|
211
|
+
}
|
|
189
212
|
try {
|
|
190
213
|
if (options.runtime === "pi") {
|
|
191
214
|
const { discoverPiModelCatalog } = await import("./pi-model-catalog.mjs");
|
package/package.json
CHANGED
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
import crypto from "node:crypto";
|
|
2
|
-
import fs from "node:fs";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { exactMode } from "../platform/secure-metadata.mjs";
|
|
5
|
-
export const PI_RUNTIME_ARTIFACT_MANIFEST = ".larkin-pi-owned-artifacts.json";
|
|
6
|
-
export const PI_RUNTIME_ARTIFACT_NAMES = ["models-store.json", "npm"];
|
|
7
|
-
const MAX_FILE_BYTES = 256 * 1024 * 1024;
|
|
8
|
-
const MAX_ENTRIES = 4096;
|
|
9
|
-
function digest(value) {
|
|
10
|
-
return crypto.createHash("sha256").update(value).digest("hex");
|
|
11
|
-
}
|
|
12
|
-
function assertSafeEntry(file, label) {
|
|
13
|
-
const stat = fs.lstatSync(file);
|
|
14
|
-
if (stat.isSymbolicLink())
|
|
15
|
-
throw new Error(`${label} must not be a symlink`);
|
|
16
|
-
if (typeof process.getuid === "function" && stat.uid !== process.getuid())
|
|
17
|
-
throw new Error(`${label} owner is unsafe`);
|
|
18
|
-
return stat;
|
|
19
|
-
}
|
|
20
|
-
function identity(file, label) {
|
|
21
|
-
const stat = assertSafeEntry(file, label);
|
|
22
|
-
const mode = stat.mode & 0o777;
|
|
23
|
-
if (stat.isFile()) {
|
|
24
|
-
if (stat.size > MAX_FILE_BYTES)
|
|
25
|
-
throw new Error(`${label} is too large`);
|
|
26
|
-
const bytes = fs.readFileSync(file);
|
|
27
|
-
return { kind: "file", mode, device: String(stat.dev), inode: String(stat.ino), bytes: bytes.length, digest: digest(bytes) };
|
|
28
|
-
}
|
|
29
|
-
if (!stat.isDirectory())
|
|
30
|
-
throw new Error(`${label} has an unsupported type`);
|
|
31
|
-
const entries = fs.readdirSync(file).sort();
|
|
32
|
-
if (entries.length > MAX_ENTRIES)
|
|
33
|
-
throw new Error(`${label} has too many entries`);
|
|
34
|
-
const children = entries.map((name) => {
|
|
35
|
-
if (name === "." || name === ".." || name.includes("/"))
|
|
36
|
-
throw new Error(`${label} contains an unsafe entry`);
|
|
37
|
-
const child = identity(path.join(file, name), `${label} child`);
|
|
38
|
-
return [name, child.kind, child.mode, child.device, child.inode, child.bytes, child.digest];
|
|
39
|
-
});
|
|
40
|
-
return {
|
|
41
|
-
kind: "directory", mode, device: String(stat.dev), inode: String(stat.ino),
|
|
42
|
-
bytes: children.reduce((sum, child) => sum + Number(child[5]), 0), digest: digest(JSON.stringify(children)),
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
function sameIdentity(actual, expected) {
|
|
46
|
-
return actual.kind === expected.kind && actual.mode === expected.mode
|
|
47
|
-
&& actual.device === expected.device && actual.inode === expected.inode
|
|
48
|
-
&& actual.bytes === expected.bytes && actual.digest === expected.digest;
|
|
49
|
-
}
|
|
50
|
-
function readManifest(file) {
|
|
51
|
-
try {
|
|
52
|
-
const stat = assertSafeEntry(file, "Pi artifact provenance manifest");
|
|
53
|
-
if (!stat.isFile() || !exactMode(stat, 0o600) || stat.size > 1024 * 1024)
|
|
54
|
-
throw new Error("Pi artifact provenance manifest is unsafe");
|
|
55
|
-
const value = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
56
|
-
if (value?.version !== 1 || (value.owner !== "larkin-pi" && value.owner !== "larkin-builtin-pi") || !value.directory || !value.artifacts)
|
|
57
|
-
throw new Error("Pi artifact provenance manifest is invalid");
|
|
58
|
-
return value;
|
|
59
|
-
}
|
|
60
|
-
catch (error) {
|
|
61
|
-
if (error.code === "ENOENT")
|
|
62
|
-
return null;
|
|
63
|
-
throw error;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
function writeManifest(file, value) {
|
|
67
|
-
const temp = `${file}.tmp-${process.pid}`;
|
|
68
|
-
const fd = fs.openSync(temp, "wx", 0o600);
|
|
69
|
-
try {
|
|
70
|
-
fs.writeFileSync(fd, `${JSON.stringify(value, null, 2)}\n`);
|
|
71
|
-
fs.fsyncSync(fd);
|
|
72
|
-
}
|
|
73
|
-
finally {
|
|
74
|
-
fs.closeSync(fd);
|
|
75
|
-
}
|
|
76
|
-
fs.renameSync(temp, file);
|
|
77
|
-
}
|
|
78
|
-
/** Record only allowed entries that were absent at the runtime spawn boundary. */
|
|
79
|
-
export function recordPiRuntimeArtifactProvenance(targetDir, beforeEntries, boundaryAt = Date.now()) {
|
|
80
|
-
const target = assertSafeEntry(targetDir, "Pi provider target");
|
|
81
|
-
if (!target.isDirectory() || !exactMode(target, 0o700))
|
|
82
|
-
throw new Error("Pi provider target is unsafe");
|
|
83
|
-
const manifestFile = path.join(targetDir, PI_RUNTIME_ARTIFACT_MANIFEST);
|
|
84
|
-
const existing = readManifest(manifestFile);
|
|
85
|
-
const manifest = existing ?? {
|
|
86
|
-
version: 1, owner: "larkin-pi", directory: { device: String(target.dev), inode: String(target.ino) },
|
|
87
|
-
recordedAt: new Date(boundaryAt).toISOString(), artifacts: {},
|
|
88
|
-
};
|
|
89
|
-
if (manifest.directory.device !== String(target.dev) || manifest.directory.inode !== String(target.ino))
|
|
90
|
-
throw new Error("Pi artifact provenance target identity changed");
|
|
91
|
-
let changed = false;
|
|
92
|
-
for (const [name, expected] of Object.entries(manifest.artifacts)) {
|
|
93
|
-
if (!PI_RUNTIME_ARTIFACT_NAMES.includes(name) || name.includes("/") || !expected)
|
|
94
|
-
throw new Error("Pi artifact provenance entry is invalid");
|
|
95
|
-
const actual = identity(path.join(targetDir, name), `Pi artifact ${name}`);
|
|
96
|
-
const sameOwner = actual.kind === expected.kind && actual.mode === expected.mode
|
|
97
|
-
&& actual.device === expected.device && actual.inode === expected.inode;
|
|
98
|
-
if (!sameOwner)
|
|
99
|
-
throw new Error(`Pi artifact ${name} provenance identity changed`);
|
|
100
|
-
if (!sameIdentity(actual, expected)) {
|
|
101
|
-
manifest.artifacts[name] = actual;
|
|
102
|
-
changed = true;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
for (const name of PI_RUNTIME_ARTIFACT_NAMES) {
|
|
106
|
-
if (beforeEntries.has(name) || manifest.artifacts[name])
|
|
107
|
-
continue;
|
|
108
|
-
const file = path.join(targetDir, name);
|
|
109
|
-
if (!fs.existsSync(file))
|
|
110
|
-
continue;
|
|
111
|
-
const actual = identity(file, `Pi artifact ${name}`);
|
|
112
|
-
const stat = fs.lstatSync(file);
|
|
113
|
-
if (stat.birthtimeMs + 1 < boundaryAt && stat.mtimeMs + 1 < boundaryAt)
|
|
114
|
-
throw new Error(`Pi artifact ${name} predates runtime provenance boundary`);
|
|
115
|
-
manifest.artifacts[name] = actual;
|
|
116
|
-
changed = true;
|
|
117
|
-
}
|
|
118
|
-
if (changed)
|
|
119
|
-
writeManifest(manifestFile, manifest);
|
|
120
|
-
}
|
|
121
|
-
/** Return the names that are both explicitly attested and unchanged. */
|
|
122
|
-
export function attestedPiRuntimeArtifactNames(targetDir) {
|
|
123
|
-
const target = assertSafeEntry(targetDir, "Pi provider target");
|
|
124
|
-
if (!target.isDirectory())
|
|
125
|
-
throw new Error("Pi provider target is unsafe");
|
|
126
|
-
const manifest = readManifest(path.join(targetDir, PI_RUNTIME_ARTIFACT_MANIFEST));
|
|
127
|
-
if (!manifest || manifest.directory.device !== String(target.dev) || manifest.directory.inode !== String(target.ino))
|
|
128
|
-
return new Set();
|
|
129
|
-
const names = new Set();
|
|
130
|
-
for (const [name, expected] of Object.entries(manifest.artifacts)) {
|
|
131
|
-
if (!PI_RUNTIME_ARTIFACT_NAMES.includes(name) || !expected)
|
|
132
|
-
continue;
|
|
133
|
-
const actual = identity(path.join(targetDir, name), `Pi artifact ${name}`);
|
|
134
|
-
if (!sameIdentity(actual, expected))
|
|
135
|
-
throw new Error(`Pi artifact ${name} provenance changed`);
|
|
136
|
-
names.add(name);
|
|
137
|
-
}
|
|
138
|
-
return names;
|
|
139
|
-
}
|