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,108 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.traceProcessBoundary = traceProcessBoundary;
7
+ const node_fs_1 = __importDefault(require("node:fs"));
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ function inspect(role, file) {
10
+ try {
11
+ const stat = node_fs_1.default.lstatSync(file);
12
+ return {
13
+ role,
14
+ exists: true,
15
+ regularFile: stat.isFile(),
16
+ directory: stat.isDirectory(),
17
+ symlink: stat.isSymbolicLink(),
18
+ mode: stat.mode & 0o777,
19
+ };
20
+ }
21
+ catch (error) {
22
+ const code = error.code;
23
+ return { role, exists: false, ...(code === "ELOOP" ? { symlink: true } : {}) };
24
+ }
25
+ }
26
+ function errorCategory(error) {
27
+ const code = error && typeof error === "object" && "code" in error
28
+ ? String(error.code || "") : "";
29
+ if (code === "ENOENT")
30
+ return "missing";
31
+ if (code === "EACCES" || code === "EPERM")
32
+ return "permission";
33
+ if (code === "ELOOP")
34
+ return "symlink";
35
+ if (code === "EEXIST")
36
+ return "conflict";
37
+ if (code === "EINVAL" || code === "EBADMSG" || code === "ERR_INVALID_ARG_VALUE")
38
+ return "invalid";
39
+ return "runtime";
40
+ }
41
+ function safePhase(value) {
42
+ const phase = typeof value === "string" ? value : "unknown";
43
+ return /^[a-z0-9:_-]{1,80}$/i.test(phase) ? phase : "unknown";
44
+ }
45
+ function safeEpoch(value) {
46
+ if (typeof value !== "string" && typeof value !== "number" && !(value instanceof Date))
47
+ return null;
48
+ const text = value instanceof Date ? value.toISOString() : String(value);
49
+ const time = Date.parse(text);
50
+ return Number.isFinite(time) ? new Date(time).toISOString() : null;
51
+ }
52
+ /**
53
+ * Opt-in, content-free diagnostics for process-boundary startup investigations.
54
+ * The trace intentionally emits roles and metadata only: never a cwd, home,
55
+ * config/profile/target path, or exception text. Diagnostics are inert unless an
56
+ * absolute 0600 destination is explicitly supplied by a test or operator.
57
+ */
58
+ function traceProcessBoundary(env, phase, fields = {}) {
59
+ const file = env.LARKIN_PROCESS_BOUNDARY_TRACE_FILE;
60
+ if (!file || !node_path_1.default.isAbsolute(file) || /[\0\r\n]/.test(file))
61
+ return;
62
+ try {
63
+ const configRoot = fields.configDir || env.LARKIN_CONFIG_DIR || env.LARKIN_HOME || null;
64
+ const record = {
65
+ at: new Date().toISOString(),
66
+ epoch: safeEpoch(fields.epoch ?? env.LARKIN_DAEMON_EPOCH ?? new Date().toISOString()),
67
+ pid: process.pid,
68
+ ppid: process.ppid,
69
+ phase: safePhase(phase),
70
+ configuredRoots: {
71
+ configDir: Boolean(env.LARKIN_CONFIG_DIR || fields.configDir),
72
+ larkinHome: Boolean(env.LARKIN_HOME),
73
+ piAgentDir: Boolean(env.PI_CODING_AGENT_DIR),
74
+ },
75
+ pathRoles: [
76
+ ...(configRoot ? [inspect("config-root", configRoot)] : []),
77
+ ...(fields.targetDir ? [inspect("provider-target", fields.targetDir)] : []),
78
+ ],
79
+ };
80
+ if (typeof fields.agentId === "string" && /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(fields.agentId))
81
+ record.agentId = fields.agentId;
82
+ if (Number.isInteger(fields.childPid) && Number(fields.childPid) > 0)
83
+ record.childPid = Number(fields.childPid);
84
+ if (typeof fields.distribution === "string" && /^(builtin|external)$/.test(fields.distribution))
85
+ record.distribution = fields.distribution;
86
+ if (typeof fields.requested === "string" && /^(builtin|external|migration|startup)$/.test(fields.requested))
87
+ record.requested = fields.requested;
88
+ if (typeof fields.targetDirExisted === "boolean")
89
+ record.targetDirExisted = fields.targetDirExisted;
90
+ if (fields.error !== undefined)
91
+ record.errorCategory = errorCategory(fields.error);
92
+ const bytes = Buffer.from(`${JSON.stringify(record)}\n`);
93
+ const fd = node_fs_1.default.openSync(file, node_fs_1.default.constants.O_WRONLY | node_fs_1.default.constants.O_APPEND | node_fs_1.default.constants.O_CREAT | (node_fs_1.default.constants.O_NOFOLLOW || 0), 0o600);
94
+ try {
95
+ const stat = node_fs_1.default.fstatSync(fd);
96
+ if (!stat.isFile() || (typeof process.getuid === "function" && stat.uid !== process.getuid()) || (stat.mode & 0o077) !== 0)
97
+ return;
98
+ node_fs_1.default.writeSync(fd, bytes);
99
+ node_fs_1.default.fchmodSync(fd, 0o600);
100
+ }
101
+ finally {
102
+ node_fs_1.default.closeSync(fd);
103
+ }
104
+ }
105
+ catch {
106
+ // Diagnostics must never alter startup behavior.
107
+ }
108
+ }
@@ -0,0 +1,102 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ function inspect(role, file) {
4
+ try {
5
+ const stat = fs.lstatSync(file);
6
+ return {
7
+ role,
8
+ exists: true,
9
+ regularFile: stat.isFile(),
10
+ directory: stat.isDirectory(),
11
+ symlink: stat.isSymbolicLink(),
12
+ mode: stat.mode & 0o777,
13
+ };
14
+ }
15
+ catch (error) {
16
+ const code = error.code;
17
+ return { role, exists: false, ...(code === "ELOOP" ? { symlink: true } : {}) };
18
+ }
19
+ }
20
+ function errorCategory(error) {
21
+ const code = error && typeof error === "object" && "code" in error
22
+ ? String(error.code || "") : "";
23
+ if (code === "ENOENT")
24
+ return "missing";
25
+ if (code === "EACCES" || code === "EPERM")
26
+ return "permission";
27
+ if (code === "ELOOP")
28
+ return "symlink";
29
+ if (code === "EEXIST")
30
+ return "conflict";
31
+ if (code === "EINVAL" || code === "EBADMSG" || code === "ERR_INVALID_ARG_VALUE")
32
+ return "invalid";
33
+ return "runtime";
34
+ }
35
+ function safePhase(value) {
36
+ const phase = typeof value === "string" ? value : "unknown";
37
+ return /^[a-z0-9:_-]{1,80}$/i.test(phase) ? phase : "unknown";
38
+ }
39
+ function safeEpoch(value) {
40
+ if (typeof value !== "string" && typeof value !== "number" && !(value instanceof Date))
41
+ return null;
42
+ const text = value instanceof Date ? value.toISOString() : String(value);
43
+ const time = Date.parse(text);
44
+ return Number.isFinite(time) ? new Date(time).toISOString() : null;
45
+ }
46
+ /**
47
+ * Opt-in, content-free diagnostics for process-boundary startup investigations.
48
+ * The trace intentionally emits roles and metadata only: never a cwd, home,
49
+ * config/profile/target path, or exception text. Diagnostics are inert unless an
50
+ * absolute 0600 destination is explicitly supplied by a test or operator.
51
+ */
52
+ export function traceProcessBoundary(env, phase, fields = {}) {
53
+ const file = env.LARKIN_PROCESS_BOUNDARY_TRACE_FILE;
54
+ if (!file || !path.isAbsolute(file) || /[\0\r\n]/.test(file))
55
+ return;
56
+ try {
57
+ const configRoot = fields.configDir || env.LARKIN_CONFIG_DIR || env.LARKIN_HOME || null;
58
+ const record = {
59
+ at: new Date().toISOString(),
60
+ epoch: safeEpoch(fields.epoch ?? env.LARKIN_DAEMON_EPOCH ?? new Date().toISOString()),
61
+ pid: process.pid,
62
+ ppid: process.ppid,
63
+ phase: safePhase(phase),
64
+ configuredRoots: {
65
+ configDir: Boolean(env.LARKIN_CONFIG_DIR || fields.configDir),
66
+ larkinHome: Boolean(env.LARKIN_HOME),
67
+ piAgentDir: Boolean(env.PI_CODING_AGENT_DIR),
68
+ },
69
+ pathRoles: [
70
+ ...(configRoot ? [inspect("config-root", configRoot)] : []),
71
+ ...(fields.targetDir ? [inspect("provider-target", fields.targetDir)] : []),
72
+ ],
73
+ };
74
+ if (typeof fields.agentId === "string" && /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(fields.agentId))
75
+ record.agentId = fields.agentId;
76
+ if (Number.isInteger(fields.childPid) && Number(fields.childPid) > 0)
77
+ record.childPid = Number(fields.childPid);
78
+ if (typeof fields.distribution === "string" && /^(builtin|external)$/.test(fields.distribution))
79
+ record.distribution = fields.distribution;
80
+ if (typeof fields.requested === "string" && /^(builtin|external|migration|startup)$/.test(fields.requested))
81
+ record.requested = fields.requested;
82
+ if (typeof fields.targetDirExisted === "boolean")
83
+ record.targetDirExisted = fields.targetDirExisted;
84
+ if (fields.error !== undefined)
85
+ record.errorCategory = errorCategory(fields.error);
86
+ const bytes = Buffer.from(`${JSON.stringify(record)}\n`);
87
+ const fd = fs.openSync(file, fs.constants.O_WRONLY | fs.constants.O_APPEND | fs.constants.O_CREAT | (fs.constants.O_NOFOLLOW || 0), 0o600);
88
+ try {
89
+ const stat = fs.fstatSync(fd);
90
+ if (!stat.isFile() || (typeof process.getuid === "function" && stat.uid !== process.getuid()) || (stat.mode & 0o077) !== 0)
91
+ return;
92
+ fs.writeSync(fd, bytes);
93
+ fs.fchmodSync(fd, 0o600);
94
+ }
95
+ finally {
96
+ fs.closeSync(fd);
97
+ }
98
+ }
99
+ catch {
100
+ // Diagnostics must never alter startup behavior.
101
+ }
102
+ }
@@ -0,0 +1,147 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.PI_RUNTIME_ARTIFACT_NAMES = exports.PI_RUNTIME_ARTIFACT_MANIFEST = void 0;
7
+ exports.recordPiRuntimeArtifactProvenance = recordPiRuntimeArtifactProvenance;
8
+ exports.attestedPiRuntimeArtifactNames = attestedPiRuntimeArtifactNames;
9
+ const node_crypto_1 = __importDefault(require("node:crypto"));
10
+ const node_fs_1 = __importDefault(require("node:fs"));
11
+ const node_path_1 = __importDefault(require("node:path"));
12
+ const secure_metadata_js_1 = require("../platform/secure-metadata.cjs");
13
+ exports.PI_RUNTIME_ARTIFACT_MANIFEST = ".larkin-pi-owned-artifacts.json";
14
+ exports.PI_RUNTIME_ARTIFACT_NAMES = [".larkin-official-pi-package", "models-store.json", "npm"];
15
+ const MAX_FILE_BYTES = 256 * 1024 * 1024;
16
+ const MAX_ENTRIES = 4096;
17
+ function digest(value) {
18
+ return node_crypto_1.default.createHash("sha256").update(value).digest("hex");
19
+ }
20
+ function assertSafeEntry(file, label) {
21
+ const stat = node_fs_1.default.lstatSync(file);
22
+ if (stat.isSymbolicLink())
23
+ throw new Error(`${label} must not be a symlink`);
24
+ if (typeof process.getuid === "function" && stat.uid !== process.getuid())
25
+ throw new Error(`${label} owner is unsafe`);
26
+ return stat;
27
+ }
28
+ function identity(file, label) {
29
+ const stat = assertSafeEntry(file, label);
30
+ const mode = stat.mode & 0o777;
31
+ if (stat.isFile()) {
32
+ if (stat.size > MAX_FILE_BYTES)
33
+ throw new Error(`${label} is too large`);
34
+ const bytes = node_fs_1.default.readFileSync(file);
35
+ return { kind: "file", mode, device: String(stat.dev), inode: String(stat.ino), bytes: bytes.length, digest: digest(bytes) };
36
+ }
37
+ if (!stat.isDirectory())
38
+ throw new Error(`${label} has an unsupported type`);
39
+ const entries = node_fs_1.default.readdirSync(file).sort();
40
+ if (entries.length > MAX_ENTRIES)
41
+ throw new Error(`${label} has too many entries`);
42
+ const children = entries.map((name) => {
43
+ if (name === "." || name === ".." || name.includes("/"))
44
+ throw new Error(`${label} contains an unsafe entry`);
45
+ const child = identity(node_path_1.default.join(file, name), `${label} child`);
46
+ return [name, child.kind, child.mode, child.device, child.inode, child.bytes, child.digest];
47
+ });
48
+ return {
49
+ kind: "directory", mode, device: String(stat.dev), inode: String(stat.ino),
50
+ bytes: children.reduce((sum, child) => sum + Number(child[5]), 0), digest: digest(JSON.stringify(children)),
51
+ };
52
+ }
53
+ function sameIdentity(actual, expected) {
54
+ return actual.kind === expected.kind && actual.mode === expected.mode
55
+ && actual.device === expected.device && actual.inode === expected.inode
56
+ && actual.bytes === expected.bytes && actual.digest === expected.digest;
57
+ }
58
+ function readManifest(file) {
59
+ try {
60
+ const stat = assertSafeEntry(file, "Pi artifact provenance manifest");
61
+ if (!stat.isFile() || !(0, secure_metadata_js_1.exactMode)(stat, 0o600) || stat.size > 1024 * 1024)
62
+ throw new Error("Pi artifact provenance manifest is unsafe");
63
+ const value = JSON.parse(node_fs_1.default.readFileSync(file, "utf8"));
64
+ if (value?.version !== 1 || value.owner !== "larkin-builtin-pi" || !value.directory || !value.artifacts)
65
+ throw new Error("Pi artifact provenance manifest is invalid");
66
+ return value;
67
+ }
68
+ catch (error) {
69
+ if (error.code === "ENOENT")
70
+ return null;
71
+ throw error;
72
+ }
73
+ }
74
+ function writeManifest(file, value) {
75
+ const temp = `${file}.tmp-${process.pid}`;
76
+ const fd = node_fs_1.default.openSync(temp, "wx", 0o600);
77
+ try {
78
+ node_fs_1.default.writeFileSync(fd, `${JSON.stringify(value, null, 2)}\n`);
79
+ node_fs_1.default.fsyncSync(fd);
80
+ }
81
+ finally {
82
+ node_fs_1.default.closeSync(fd);
83
+ }
84
+ node_fs_1.default.renameSync(temp, file);
85
+ }
86
+ /** Record only allowed entries that were absent at the runtime spawn boundary. */
87
+ function recordPiRuntimeArtifactProvenance(targetDir, beforeEntries, boundaryAt = Date.now()) {
88
+ const target = assertSafeEntry(targetDir, "Pi provider target");
89
+ if (!target.isDirectory() || !(0, secure_metadata_js_1.exactMode)(target, 0o700))
90
+ throw new Error("Pi provider target is unsafe");
91
+ const manifestFile = node_path_1.default.join(targetDir, exports.PI_RUNTIME_ARTIFACT_MANIFEST);
92
+ const existing = readManifest(manifestFile);
93
+ const manifest = existing ?? {
94
+ version: 1, owner: "larkin-builtin-pi", directory: { device: String(target.dev), inode: String(target.ino) },
95
+ recordedAt: new Date(boundaryAt).toISOString(), artifacts: {},
96
+ };
97
+ if (manifest.directory.device !== String(target.dev) || manifest.directory.inode !== String(target.ino))
98
+ throw new Error("Pi artifact provenance target identity changed");
99
+ let changed = false;
100
+ for (const [name, expected] of Object.entries(manifest.artifacts)) {
101
+ if (!exports.PI_RUNTIME_ARTIFACT_NAMES.includes(name) || name.includes("/") || !expected)
102
+ throw new Error("Pi artifact provenance entry is invalid");
103
+ const actual = identity(node_path_1.default.join(targetDir, name), `Pi artifact ${name}`);
104
+ const sameOwner = actual.kind === expected.kind && actual.mode === expected.mode
105
+ && actual.device === expected.device && actual.inode === expected.inode;
106
+ if (!sameOwner)
107
+ throw new Error(`Pi artifact ${name} provenance identity changed`);
108
+ if (!sameIdentity(actual, expected)) {
109
+ manifest.artifacts[name] = actual;
110
+ changed = true;
111
+ }
112
+ }
113
+ for (const name of exports.PI_RUNTIME_ARTIFACT_NAMES) {
114
+ if (beforeEntries.has(name) || manifest.artifacts[name])
115
+ continue;
116
+ const file = node_path_1.default.join(targetDir, name);
117
+ if (!node_fs_1.default.existsSync(file))
118
+ continue;
119
+ const actual = identity(file, `Pi artifact ${name}`);
120
+ const stat = node_fs_1.default.lstatSync(file);
121
+ if (stat.birthtimeMs + 1 < boundaryAt && stat.mtimeMs + 1 < boundaryAt)
122
+ throw new Error(`Pi artifact ${name} predates runtime provenance boundary`);
123
+ manifest.artifacts[name] = actual;
124
+ changed = true;
125
+ }
126
+ if (changed)
127
+ writeManifest(manifestFile, manifest);
128
+ }
129
+ /** Return the names that are both explicitly attested and unchanged. */
130
+ function attestedPiRuntimeArtifactNames(targetDir) {
131
+ const target = assertSafeEntry(targetDir, "Pi provider target");
132
+ if (!target.isDirectory())
133
+ throw new Error("Pi provider target is unsafe");
134
+ const manifest = readManifest(node_path_1.default.join(targetDir, exports.PI_RUNTIME_ARTIFACT_MANIFEST));
135
+ if (!manifest || manifest.directory.device !== String(target.dev) || manifest.directory.inode !== String(target.ino))
136
+ return new Set();
137
+ const names = new Set();
138
+ for (const [name, expected] of Object.entries(manifest.artifacts)) {
139
+ if (!exports.PI_RUNTIME_ARTIFACT_NAMES.includes(name) || !expected)
140
+ continue;
141
+ const actual = identity(node_path_1.default.join(targetDir, name), `Pi artifact ${name}`);
142
+ if (!sameIdentity(actual, expected))
143
+ throw new Error(`Pi artifact ${name} provenance changed`);
144
+ names.add(name);
145
+ }
146
+ return names;
147
+ }
@@ -0,0 +1,139 @@
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 = [".larkin-official-pi-package", "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-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-builtin-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
+ }