owlrunkit 0.15.0 → 0.16.0
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 +100 -0
- package/README.md +274 -66
- package/integrations/codex/skills/owlcoda-runkit/SKILL.md +223 -117
- package/integrations/codex/skills/owlcoda-runkit/assets/templates/config.json +3 -3
- package/integrations/codex/skills/owlcoda-runkit/assets/templates/ready-for-commit-request.json +2 -2
- package/package.json +2 -2
- package/packages/attest/distribution-manifest-v1.json +10 -0
- package/packages/attest/src/quick.mjs +10 -0
- package/registry-release-policy-v1.json +8 -2
- package/sbom.spdx.json +5 -5
- package/scripts/runkit-contract/assurance-router.mjs +119 -0
- package/scripts/runkit-contract/core-contract.mjs +250 -115
- package/scripts/runkit-contract/core-successor.mjs +2043 -0
- package/scripts/runkit-contract/deployment-workflow.mjs +2040 -0
- package/scripts/runkit-contract/finalize.mjs +242 -21
- package/scripts/runkit-contract/fleet-discovery.mjs +456 -0
- package/scripts/runkit-contract/formal-workflow.mjs +850 -0
- package/scripts/runkit-contract/human-status.mjs +553 -0
- package/scripts/runkit-contract/inspect-presentation.mjs +25 -8
- package/scripts/runkit-contract/install-codex-skill.mjs +73 -30
- package/scripts/runkit-contract/lease-lifecycle.mjs +8 -2
- package/scripts/runkit-contract/lifecycle-orchestration.mjs +135 -12
- package/scripts/runkit-contract/onboarding-doctor.mjs +43 -90
- package/scripts/runkit-contract/owner-authority-trust.mjs +230 -0
- package/scripts/runkit-contract/owner-deployment-decision.mjs +385 -0
- package/scripts/runkit-contract/profile-onboarding.mjs +841 -13
- package/scripts/runkit-contract/project-cli-resolver.mjs +701 -0
- package/scripts/runkit-contract/project-control-parser.mjs +830 -0
- package/scripts/runkit-contract/project-control-state.mjs +28 -0
- package/scripts/runkit-contract/provenance-common.mjs +19 -2
- package/scripts/runkit-contract/ready-for-commit.mjs +330 -18
- package/scripts/runkit-contract/remote-deployment.mjs +1596 -0
- package/scripts/runkit-contract/remote-process-adapter.mjs +177 -0
- package/scripts/runkit-contract/runkit-bootstrap.mjs +131 -0
- package/scripts/runkit-contract/runkit-cli.mjs +1097 -21
- package/scripts/runkit-contract/schemas/assurance-route-v1.schema.json +26 -0
- package/scripts/runkit-contract/schemas/core-successor-batch-receipt-v1.schema.json +126 -0
- package/scripts/runkit-contract/schemas/core-successor-plan-v1.schema.json +155 -0
- package/scripts/runkit-contract/schemas/core-successor-project-success-receipt-v1.schema.json +72 -0
- package/scripts/runkit-contract/schemas/deployment-execute-request-v1.schema.json +63 -0
- package/scripts/runkit-contract/schemas/deployment-lineage-v1.schema.json +292 -0
- package/scripts/runkit-contract/schemas/deployment-prepare-receipt-v1.schema.json +140 -0
- package/scripts/runkit-contract/schemas/fleet-discovery-v1.schema.json +31 -0
- package/scripts/runkit-contract/schemas/fleet-manifest-v1.schema.json +20 -0
- package/scripts/runkit-contract/schemas/fleet-root-registry-v1.schema.json +34 -0
- package/scripts/runkit-contract/schemas/human-status-v1.schema.json +102 -0
- package/scripts/runkit-contract/schemas/owner-authority-trust-v1.schema.json +66 -0
- package/scripts/runkit-contract/schemas/owner-deployment-authority-v1.schema.json +46 -0
- package/scripts/runkit-contract/schemas/owner-deployment-authority-v2.schema.json +94 -0
- package/scripts/runkit-contract/schemas/owner-deployment-decision-v1.schema.json +102 -0
- package/scripts/runkit-contract/schemas/owner-migration-authority-v1.schema.json +33 -0
- package/scripts/runkit-contract/schemas/owner-migration-authority-v2.schema.json +67 -0
- package/scripts/runkit-contract/schemas/profile-apply-receipt-v1.schema.json +32 -0
- package/scripts/runkit-contract/schemas/profile-detection-v2.schema.json +145 -0
- package/scripts/runkit-contract/schemas/ready-for-commit-v2.schema.json +37 -0
- package/scripts/runkit-contract/schemas/remote-deployment-manifest-v1.schema.json +379 -0
- package/scripts/runkit-contract/schemas/remote-deployment-result-v1.schema.json +153 -0
- package/scripts/runkit-contract/schemas/remote-deployment-stage-journal-v1.schema.json +183 -0
- package/scripts/runkit-contract/schemas/remote-target-v1.schema.json +32 -0
- package/scripts/runkit-contract/schemas/source-candidate-v2.schema.json +152 -0
- package/scripts/runkit-contract/schemas/verification-receipt-v2.schema.json +13 -0
- package/scripts/runkit-contract/snapshot.mjs +1 -1
- package/scripts/runkit-contract/source-candidate.mjs +1382 -0
- package/scripts/runkit-contract/ssh-remote-adapter.mjs +1184 -0
- package/scripts/runkit-contract/sync-core016-release-identity.mjs +161 -0
- package/scripts/runkit-contract/templates/assurance-request.json +13 -0
- package/scripts/runkit-contract/templates/core-successor-owner-authority.json +15 -0
- package/scripts/runkit-contract/templates/deployment-execute-request.json +39 -0
- package/scripts/runkit-contract/templates/deployment-owner-authority.json +23 -0
- package/scripts/runkit-contract/templates/deployment-owner-decision.json +26 -0
- package/scripts/runkit-contract/templates/fleet-manifest.json +8 -0
- package/scripts/runkit-contract/templates/fleet-root-registry.json +8 -0
- package/scripts/runkit-contract/templates/owner-authority-trust.json +16 -0
- package/scripts/runkit-contract/templates/ready-for-commit-request.json +2 -2
- package/scripts/runkit-contract/templates/remote-deployment-manifest.json +108 -0
- package/scripts/runkit-contract/verification-receipt-gate.mjs +28 -0
|
@@ -1,8 +1,14 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import {
|
|
2
3
|
existsSync,
|
|
3
4
|
lstatSync,
|
|
5
|
+
linkSync,
|
|
6
|
+
mkdirSync,
|
|
7
|
+
readFileSync,
|
|
4
8
|
readdirSync,
|
|
5
9
|
realpathSync,
|
|
10
|
+
rmSync,
|
|
11
|
+
writeFileSync,
|
|
6
12
|
} from "node:fs";
|
|
7
13
|
import path from "node:path";
|
|
8
14
|
|
|
@@ -36,6 +42,355 @@ function resolveRegularExecutable(name) {
|
|
|
36
42
|
return null;
|
|
37
43
|
}
|
|
38
44
|
|
|
45
|
+
function canonicalJson(value) {
|
|
46
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
|
|
47
|
+
if (value && typeof value === "object") {
|
|
48
|
+
return `{${Object.entries(value)
|
|
49
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
50
|
+
.map(([key, nested]) => `${JSON.stringify(key)}:${canonicalJson(nested)}`)
|
|
51
|
+
.join(",")}}`;
|
|
52
|
+
}
|
|
53
|
+
const encoded = JSON.stringify(value);
|
|
54
|
+
if (encoded === undefined) throw new Error("profile_detection_noncanonical_value");
|
|
55
|
+
return encoded;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function sha256(value) {
|
|
59
|
+
return createHash("sha256").update(value).digest("hex");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function boundedRegularFile(root, relativePath) {
|
|
63
|
+
safeRelativePath(relativePath, "profile detection input");
|
|
64
|
+
const absolute = path.resolve(root, relativePath);
|
|
65
|
+
const relative = path.relative(root, absolute);
|
|
66
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
67
|
+
throw new Error(`profile_detection_input_outside_workspace:${relativePath}`);
|
|
68
|
+
}
|
|
69
|
+
const stat = lstatSync(absolute);
|
|
70
|
+
const resolved = realpathSync(absolute);
|
|
71
|
+
if (
|
|
72
|
+
stat.isSymbolicLink()
|
|
73
|
+
|| !stat.isFile()
|
|
74
|
+
|| resolved !== absolute
|
|
75
|
+
) {
|
|
76
|
+
throw new Error(`profile_detection_input_not_regular:${relativePath}`);
|
|
77
|
+
}
|
|
78
|
+
return absolute;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function insideRoot(root, candidate) {
|
|
82
|
+
const relative = path.relative(root, candidate);
|
|
83
|
+
return relative !== ".."
|
|
84
|
+
&& !relative.startsWith(`..${path.sep}`)
|
|
85
|
+
&& !path.isAbsolute(relative);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function ensureDirectoryChain(root, relativeDirectory) {
|
|
89
|
+
let current = root;
|
|
90
|
+
for (const segment of relativeDirectory.split("/")) {
|
|
91
|
+
current = path.join(current, segment);
|
|
92
|
+
try {
|
|
93
|
+
lstatSync(current);
|
|
94
|
+
} catch (error) {
|
|
95
|
+
if (error?.code !== "ENOENT") throw error;
|
|
96
|
+
mkdirSync(current);
|
|
97
|
+
}
|
|
98
|
+
const stat = lstatSync(current);
|
|
99
|
+
const resolved = realpathSync(current);
|
|
100
|
+
if (
|
|
101
|
+
stat.isSymbolicLink()
|
|
102
|
+
|| !stat.isDirectory()
|
|
103
|
+
|| resolved !== current
|
|
104
|
+
|| !insideRoot(root, resolved)
|
|
105
|
+
) {
|
|
106
|
+
throw new Error("profile_apply_directory_untrusted");
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return current;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function regularExecutable(value) {
|
|
113
|
+
if (typeof value !== "string" || !path.isAbsolute(value)) return null;
|
|
114
|
+
try {
|
|
115
|
+
const resolved = realpathSync(value);
|
|
116
|
+
const stat = lstatSync(resolved);
|
|
117
|
+
return stat.isFile() && !stat.isSymbolicLink() ? resolved : null;
|
|
118
|
+
} catch {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function toolIdentity(executable, argvPrefix, version) {
|
|
124
|
+
const launcher = regularExecutable(argvPrefix[0] ?? executable);
|
|
125
|
+
if (!launcher || typeof version !== "string" || !/^[0-9]+\.[0-9]+\.[0-9]+/.test(version)) {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
version,
|
|
130
|
+
executableSha256: sha256(readFileSync(executable)),
|
|
131
|
+
launcherSha256: sha256(readFileSync(launcher)),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function defaultProjectNpm(root, manifest) {
|
|
136
|
+
try {
|
|
137
|
+
if (
|
|
138
|
+
typeof manifest.packageManager === "string"
|
|
139
|
+
&& !manifest.packageManager.startsWith("npm@")
|
|
140
|
+
) return null;
|
|
141
|
+
const lockPath = boundedRegularFile(root, "package-lock.json");
|
|
142
|
+
const lock = JSON.parse(readFileSync(lockPath, "utf8"));
|
|
143
|
+
if (!Number.isInteger(lock.lockfileVersion)) return null;
|
|
144
|
+
const executable = regularExecutable(process.execPath);
|
|
145
|
+
if (!executable) return null;
|
|
146
|
+
const npmEntry = path.join(
|
|
147
|
+
path.dirname(executable),
|
|
148
|
+
process.platform === "win32" ? "npm.cmd" : "npm",
|
|
149
|
+
);
|
|
150
|
+
const npmCli = regularExecutable(npmEntry);
|
|
151
|
+
if (!npmCli || process.platform === "win32") return null;
|
|
152
|
+
const npmManifestPath = path.resolve(npmCli, "..", "..", "package.json");
|
|
153
|
+
const npmManifest = JSON.parse(readFileSync(npmManifestPath, "utf8"));
|
|
154
|
+
if (
|
|
155
|
+
npmManifest.name !== "npm"
|
|
156
|
+
|| typeof npmManifest.version !== "string"
|
|
157
|
+
|| (
|
|
158
|
+
typeof manifest.packageManager === "string"
|
|
159
|
+
&& manifest.packageManager.startsWith("npm@")
|
|
160
|
+
&& manifest.packageManager.slice(4) !== npmManifest.version
|
|
161
|
+
)
|
|
162
|
+
) {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
const identity = toolIdentity(executable, [npmCli], npmManifest.version);
|
|
166
|
+
if (!identity) return null;
|
|
167
|
+
return {
|
|
168
|
+
status: "resolved",
|
|
169
|
+
confidence: "high",
|
|
170
|
+
source: "project_lockfile",
|
|
171
|
+
packageManager: "npm",
|
|
172
|
+
executable,
|
|
173
|
+
argvPrefix: [npmCli],
|
|
174
|
+
inputFiles: ["package.json", "package-lock.json"],
|
|
175
|
+
issueCodes: [],
|
|
176
|
+
...identity,
|
|
177
|
+
};
|
|
178
|
+
} catch {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function selectedPackageManager(root, manifest) {
|
|
184
|
+
const declared = typeof manifest.packageManager === "string"
|
|
185
|
+
? /^(npm|pnpm|yarn|bun)@([0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?)$/
|
|
186
|
+
.exec(manifest.packageManager)
|
|
187
|
+
: null;
|
|
188
|
+
const lockfiles = [
|
|
189
|
+
{ manager: "npm", refs: ["package-lock.json"] },
|
|
190
|
+
{ manager: "pnpm", refs: ["pnpm-lock.yaml"] },
|
|
191
|
+
{ manager: "yarn", refs: ["yarn.lock"] },
|
|
192
|
+
{ manager: "bun", refs: ["bun.lock", "bun.lockb"] },
|
|
193
|
+
].flatMap(entry => entry.refs
|
|
194
|
+
.filter(ref => existsSync(path.join(root, ref)))
|
|
195
|
+
.map(ref => ({ manager: entry.manager, ref })));
|
|
196
|
+
if (lockfiles.length !== 1) return {
|
|
197
|
+
manager: declared?.[1] ?? null,
|
|
198
|
+
version: declared?.[2] ?? null,
|
|
199
|
+
lockfile: null,
|
|
200
|
+
};
|
|
201
|
+
if (declared && declared[1] !== lockfiles[0].manager) {
|
|
202
|
+
return {
|
|
203
|
+
manager: declared[1],
|
|
204
|
+
version: declared[2],
|
|
205
|
+
lockfile: null,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
return {
|
|
209
|
+
manager: declared?.[1] ?? lockfiles[0].manager,
|
|
210
|
+
version: declared?.[2] ?? null,
|
|
211
|
+
lockfile: lockfiles[0].ref,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function defaultProjectInstalledManager(root, manifest) {
|
|
216
|
+
const selected = selectedPackageManager(root, manifest);
|
|
217
|
+
if (
|
|
218
|
+
selected.manager === null
|
|
219
|
+
|| selected.manager === "npm"
|
|
220
|
+
|| selected.version === null
|
|
221
|
+
|| selected.lockfile === null
|
|
222
|
+
) return null;
|
|
223
|
+
try {
|
|
224
|
+
boundedRegularFile(root, selected.lockfile);
|
|
225
|
+
const requestedPackageRoot = path.join(
|
|
226
|
+
root,
|
|
227
|
+
"node_modules",
|
|
228
|
+
selected.manager,
|
|
229
|
+
);
|
|
230
|
+
const packageRoot = realpathSync(requestedPackageRoot);
|
|
231
|
+
if (!insideRoot(root, packageRoot)) return null;
|
|
232
|
+
const managerManifestPath = path.join(packageRoot, "package.json");
|
|
233
|
+
const managerManifestStat = lstatSync(managerManifestPath);
|
|
234
|
+
if (
|
|
235
|
+
managerManifestStat.isSymbolicLink()
|
|
236
|
+
|| !managerManifestStat.isFile()
|
|
237
|
+
|| realpathSync(managerManifestPath) !== managerManifestPath
|
|
238
|
+
) return null;
|
|
239
|
+
const managerManifest = JSON.parse(readFileSync(managerManifestPath, "utf8"));
|
|
240
|
+
if (
|
|
241
|
+
managerManifest.name !== selected.manager
|
|
242
|
+
|| managerManifest.version !== selected.version
|
|
243
|
+
) return null;
|
|
244
|
+
const declaredBin = typeof managerManifest.bin === "string"
|
|
245
|
+
? managerManifest.bin
|
|
246
|
+
: managerManifest.bin?.[selected.manager];
|
|
247
|
+
if (typeof declaredBin !== "string" || declaredBin.length === 0) return null;
|
|
248
|
+
safeRelativePath(declaredBin, "project package manager bin");
|
|
249
|
+
const launcher = realpathSync(path.join(packageRoot, declaredBin));
|
|
250
|
+
const launcherStat = lstatSync(launcher);
|
|
251
|
+
if (
|
|
252
|
+
!insideRoot(root, launcher)
|
|
253
|
+
|| launcherStat.isSymbolicLink()
|
|
254
|
+
|| !launcherStat.isFile()
|
|
255
|
+
) return null;
|
|
256
|
+
const executable = regularExecutable(process.execPath);
|
|
257
|
+
if (!executable) return null;
|
|
258
|
+
const identity = toolIdentity(executable, [launcher], selected.version);
|
|
259
|
+
if (!identity) return null;
|
|
260
|
+
const managerManifestRef = path.relative(root, managerManifestPath)
|
|
261
|
+
.split(path.sep)
|
|
262
|
+
.join("/");
|
|
263
|
+
const launcherRef = path.relative(root, launcher)
|
|
264
|
+
.split(path.sep)
|
|
265
|
+
.join("/");
|
|
266
|
+
return {
|
|
267
|
+
status: "resolved",
|
|
268
|
+
confidence: "high",
|
|
269
|
+
source: "project_install",
|
|
270
|
+
packageManager: selected.manager,
|
|
271
|
+
executable,
|
|
272
|
+
argvPrefix: [launcher],
|
|
273
|
+
inputFiles: [
|
|
274
|
+
"package.json",
|
|
275
|
+
selected.lockfile,
|
|
276
|
+
managerManifestRef,
|
|
277
|
+
launcherRef,
|
|
278
|
+
].sort(),
|
|
279
|
+
issueCodes: [],
|
|
280
|
+
...identity,
|
|
281
|
+
};
|
|
282
|
+
} catch {
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function resolveProjectTool(root, manifest, projectToolResolver) {
|
|
288
|
+
const selected = selectedPackageManager(root, manifest);
|
|
289
|
+
const packageManager = selected.manager ?? "npm";
|
|
290
|
+
if (typeof projectToolResolver === "function") {
|
|
291
|
+
try {
|
|
292
|
+
const resolved = projectToolResolver({
|
|
293
|
+
workspaceRoot: root,
|
|
294
|
+
tool: packageManager,
|
|
295
|
+
manifest: structuredClone(manifest),
|
|
296
|
+
});
|
|
297
|
+
const executable = regularExecutable(resolved?.executable);
|
|
298
|
+
const argvPrefix = Array.isArray(resolved?.argvPrefix)
|
|
299
|
+
&& resolved.argvPrefix.every(value => typeof value === "string" && value.length > 0)
|
|
300
|
+
? [...resolved.argvPrefix]
|
|
301
|
+
: null;
|
|
302
|
+
const inputFiles = Array.isArray(resolved?.inputFiles)
|
|
303
|
+
&& resolved.inputFiles.every(value => typeof value === "string" && value.length > 0)
|
|
304
|
+
? [...new Set(resolved.inputFiles)].sort()
|
|
305
|
+
: null;
|
|
306
|
+
const authoritativeSource = new Set([
|
|
307
|
+
"project_install",
|
|
308
|
+
"project_lockfile",
|
|
309
|
+
"project_toolchain",
|
|
310
|
+
]).has(resolved?.source);
|
|
311
|
+
const identity = executable && argvPrefix
|
|
312
|
+
? toolIdentity(executable, argvPrefix, resolved?.version)
|
|
313
|
+
: null;
|
|
314
|
+
const lockBound = inputFiles?.includes("package.json")
|
|
315
|
+
&& inputFiles.some(ref => [
|
|
316
|
+
"bun.lock",
|
|
317
|
+
"bun.lockb",
|
|
318
|
+
"package-lock.json",
|
|
319
|
+
"pnpm-lock.yaml",
|
|
320
|
+
"yarn.lock",
|
|
321
|
+
].includes(ref));
|
|
322
|
+
if (
|
|
323
|
+
resolved?.status === "resolved"
|
|
324
|
+
&& executable
|
|
325
|
+
&& argvPrefix
|
|
326
|
+
&& inputFiles
|
|
327
|
+
&& identity
|
|
328
|
+
) {
|
|
329
|
+
const high = resolved.confidence === "high"
|
|
330
|
+
&& authoritativeSource
|
|
331
|
+
&& lockBound;
|
|
332
|
+
return {
|
|
333
|
+
status: "resolved",
|
|
334
|
+
confidence: high ? "high" : "review_required",
|
|
335
|
+
source: String(resolved.source ?? "injected_resolver"),
|
|
336
|
+
packageManager,
|
|
337
|
+
executable,
|
|
338
|
+
argvPrefix,
|
|
339
|
+
inputFiles,
|
|
340
|
+
issueCodes: high ? [] : ["project_tool_resolution_not_authoritative"],
|
|
341
|
+
...identity,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
return {
|
|
345
|
+
status: "invalid",
|
|
346
|
+
confidence: "review_required",
|
|
347
|
+
source: "injected_resolver",
|
|
348
|
+
packageManager,
|
|
349
|
+
executable: null,
|
|
350
|
+
argvPrefix: [],
|
|
351
|
+
inputFiles: [],
|
|
352
|
+
version: null,
|
|
353
|
+
executableSha256: null,
|
|
354
|
+
launcherSha256: null,
|
|
355
|
+
issueCodes: ["project_tool_resolution_invalid"],
|
|
356
|
+
};
|
|
357
|
+
} catch {
|
|
358
|
+
return {
|
|
359
|
+
status: "invalid",
|
|
360
|
+
confidence: "review_required",
|
|
361
|
+
source: "injected_resolver",
|
|
362
|
+
packageManager,
|
|
363
|
+
executable: null,
|
|
364
|
+
argvPrefix: [],
|
|
365
|
+
inputFiles: [],
|
|
366
|
+
version: null,
|
|
367
|
+
executableSha256: null,
|
|
368
|
+
launcherSha256: null,
|
|
369
|
+
issueCodes: ["project_tool_resolution_failed"],
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const installedManager = defaultProjectInstalledManager(root, manifest);
|
|
375
|
+
if (installedManager) return installedManager;
|
|
376
|
+
const projectNpm = defaultProjectNpm(root, manifest);
|
|
377
|
+
if (projectNpm) return projectNpm;
|
|
378
|
+
const executable = resolveRegularExecutable(packageManager);
|
|
379
|
+
return {
|
|
380
|
+
status: executable ? "resolved" : "unavailable",
|
|
381
|
+
confidence: "review_required",
|
|
382
|
+
source: executable ? "path_fallback" : "unavailable",
|
|
383
|
+
packageManager,
|
|
384
|
+
executable,
|
|
385
|
+
argvPrefix: [],
|
|
386
|
+
inputFiles: [],
|
|
387
|
+
version: null,
|
|
388
|
+
executableSha256: executable ? sha256(readFileSync(executable)) : null,
|
|
389
|
+
launcherSha256: null,
|
|
390
|
+
issueCodes: ["project_tool_resolution_not_authoritative"],
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
39
394
|
function command(id, script, executable) {
|
|
40
395
|
return {
|
|
41
396
|
id,
|
|
@@ -45,12 +400,20 @@ function command(id, script, executable) {
|
|
|
45
400
|
};
|
|
46
401
|
}
|
|
47
402
|
|
|
48
|
-
function workspaceCommand(
|
|
403
|
+
function workspaceCommand(
|
|
404
|
+
id,
|
|
405
|
+
workspacePath,
|
|
406
|
+
script,
|
|
407
|
+
executable,
|
|
408
|
+
useWorkspaceCwd = false,
|
|
409
|
+
) {
|
|
49
410
|
return {
|
|
50
411
|
id,
|
|
51
|
-
cwd: ".",
|
|
412
|
+
cwd: useWorkspaceCwd ? workspacePath : ".",
|
|
52
413
|
executable,
|
|
53
|
-
argv:
|
|
414
|
+
argv: useWorkspaceCwd
|
|
415
|
+
? ["run", script]
|
|
416
|
+
: ["run", "--workspace", workspacePath, script],
|
|
54
417
|
};
|
|
55
418
|
}
|
|
56
419
|
|
|
@@ -155,7 +518,12 @@ function profileIdForWorkspace(workspacePath) {
|
|
|
155
518
|
return `workspace-${workspacePath.replace(/[^A-Za-z0-9._-]+/g, "-")}`;
|
|
156
519
|
}
|
|
157
520
|
|
|
158
|
-
function workspaceProfiles(
|
|
521
|
+
function workspaceProfiles(
|
|
522
|
+
workspaces,
|
|
523
|
+
npmExecutable,
|
|
524
|
+
packageManager = "npm",
|
|
525
|
+
useWorkspaceCwd = false,
|
|
526
|
+
) {
|
|
159
527
|
return workspaces
|
|
160
528
|
.map((workspace) => {
|
|
161
529
|
const qualityScripts = workspace.scripts.filter((script) =>
|
|
@@ -170,10 +538,11 @@ function workspaceProfiles(workspaces, npmExecutable) {
|
|
|
170
538
|
requiresProfileIds: [],
|
|
171
539
|
commands: npmExecutable
|
|
172
540
|
? qualityScripts.map((script) => workspaceCommand(
|
|
173
|
-
|
|
541
|
+
`${packageManager}-${profileId}-${script}`,
|
|
174
542
|
workspace.path,
|
|
175
543
|
script,
|
|
176
544
|
npmExecutable,
|
|
545
|
+
useWorkspaceCwd,
|
|
177
546
|
))
|
|
178
547
|
: [],
|
|
179
548
|
};
|
|
@@ -181,17 +550,32 @@ function workspaceProfiles(workspaces, npmExecutable) {
|
|
|
181
550
|
.filter(Boolean);
|
|
182
551
|
}
|
|
183
552
|
|
|
184
|
-
function profileCandidates(
|
|
553
|
+
function profileCandidates(
|
|
554
|
+
root,
|
|
555
|
+
manifest,
|
|
556
|
+
npmExecutable,
|
|
557
|
+
workspaces,
|
|
558
|
+
packageManager = "npm",
|
|
559
|
+
useWorkspaceCwd = false,
|
|
560
|
+
includeLegacyDatabaseProfile = false,
|
|
561
|
+
) {
|
|
185
562
|
const scripts = new Set(detectedScripts(manifest));
|
|
186
563
|
const candidates = [];
|
|
187
564
|
if (
|
|
188
|
-
|
|
189
|
-
|
|
565
|
+
includeLegacyDatabaseProfile
|
|
566
|
+
&& (
|
|
567
|
+
existsSync(path.join(root, "prisma"))
|
|
568
|
+
|| [...scripts].some((script) => /^(db:|prisma)/.test(script))
|
|
569
|
+
)
|
|
190
570
|
) {
|
|
191
571
|
const commands = [...scripts]
|
|
192
572
|
.filter((script) => /^(db:|prisma)/.test(script))
|
|
193
573
|
.map((script) => npmExecutable
|
|
194
|
-
? command(
|
|
574
|
+
? command(
|
|
575
|
+
`${packageManager}-${script.replaceAll(":", "-")}`,
|
|
576
|
+
script,
|
|
577
|
+
npmExecutable,
|
|
578
|
+
)
|
|
195
579
|
: null)
|
|
196
580
|
.filter(Boolean);
|
|
197
581
|
candidates.push({
|
|
@@ -209,7 +593,9 @@ function profileCandidates(root, manifest, npmExecutable, workspaces) {
|
|
|
209
593
|
) {
|
|
210
594
|
const commands = ["build", "lint", "test", "typecheck"]
|
|
211
595
|
.filter((script) => scripts.has(script))
|
|
212
|
-
.map((script) => npmExecutable
|
|
596
|
+
.map((script) => npmExecutable
|
|
597
|
+
? command(`${packageManager}-${script}`, script, npmExecutable)
|
|
598
|
+
: null)
|
|
213
599
|
.filter(Boolean);
|
|
214
600
|
candidates.push({
|
|
215
601
|
id: "node-quality",
|
|
@@ -226,7 +612,7 @@ function profileCandidates(root, manifest, npmExecutable, workspaces) {
|
|
|
226
612
|
|| scripts.has("e2e")
|
|
227
613
|
) {
|
|
228
614
|
const commands = scripts.has("e2e") && npmExecutable
|
|
229
|
-
? [command(
|
|
615
|
+
? [command(`${packageManager}-e2e`, "e2e", npmExecutable)]
|
|
230
616
|
: [];
|
|
231
617
|
candidates.push({
|
|
232
618
|
id: "web-e2e",
|
|
@@ -237,7 +623,12 @@ function profileCandidates(root, manifest, npmExecutable, workspaces) {
|
|
|
237
623
|
commands,
|
|
238
624
|
});
|
|
239
625
|
}
|
|
240
|
-
candidates.push(...workspaceProfiles(
|
|
626
|
+
candidates.push(...workspaceProfiles(
|
|
627
|
+
workspaces,
|
|
628
|
+
npmExecutable,
|
|
629
|
+
packageManager,
|
|
630
|
+
useWorkspaceCwd,
|
|
631
|
+
));
|
|
241
632
|
return candidates.sort((left, right) => left.id.localeCompare(right.id));
|
|
242
633
|
}
|
|
243
634
|
|
|
@@ -246,7 +637,15 @@ export function detectProfiles({ workspaceRoot } = {}) {
|
|
|
246
637
|
const manifest = readWorkspaceJsonBounded(root, "package.json");
|
|
247
638
|
const npmExecutable = resolveRegularExecutable("npm");
|
|
248
639
|
const discovered = discoverWorkspaces(root, manifest);
|
|
249
|
-
const candidates = profileCandidates(
|
|
640
|
+
const candidates = profileCandidates(
|
|
641
|
+
root,
|
|
642
|
+
manifest,
|
|
643
|
+
npmExecutable,
|
|
644
|
+
discovered.workspaces,
|
|
645
|
+
"npm",
|
|
646
|
+
false,
|
|
647
|
+
true,
|
|
648
|
+
);
|
|
250
649
|
const primaryCandidates = candidates.filter((profile) => profile.role === "primary");
|
|
251
650
|
const warnings = primaryCandidates.length > 1
|
|
252
651
|
? [`ambiguous_primary_profile:${primaryCandidates.map((profile) => profile.id).join(",")}`]
|
|
@@ -277,6 +676,435 @@ export function detectProfiles({ workspaceRoot } = {}) {
|
|
|
277
676
|
};
|
|
278
677
|
}
|
|
279
678
|
|
|
679
|
+
function detectedInputManifest(root, workspaces, toolBinding) {
|
|
680
|
+
const refs = new Set([
|
|
681
|
+
"package.json",
|
|
682
|
+
...[
|
|
683
|
+
"bun.lock",
|
|
684
|
+
"bun.lockb",
|
|
685
|
+
"package-lock.json",
|
|
686
|
+
"pnpm-lock.yaml",
|
|
687
|
+
"yarn.lock",
|
|
688
|
+
].filter(ref => existsSync(path.join(root, ref))),
|
|
689
|
+
...workspaces.map(workspace => `${workspace.path}/package.json`),
|
|
690
|
+
...toolBinding.inputFiles,
|
|
691
|
+
]);
|
|
692
|
+
return [...refs]
|
|
693
|
+
.sort()
|
|
694
|
+
.map((ref) => {
|
|
695
|
+
const absolute = boundedRegularFile(root, ref);
|
|
696
|
+
return {
|
|
697
|
+
path: ref,
|
|
698
|
+
sha256: sha256(readFileSync(absolute)),
|
|
699
|
+
};
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
function bindCommandsToProjectTool(profiles, toolBinding) {
|
|
704
|
+
return profiles.map(profile => ({
|
|
705
|
+
...profile,
|
|
706
|
+
commands: profile.commands.map(entry => ({
|
|
707
|
+
...entry,
|
|
708
|
+
executable: toolBinding.executable,
|
|
709
|
+
argv: [...toolBinding.argvPrefix, ...entry.argv],
|
|
710
|
+
})),
|
|
711
|
+
}));
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
function proposedProfileDocument(candidates, inputManifest) {
|
|
715
|
+
const actionable = candidates.filter(profile => profile.commands.length > 0);
|
|
716
|
+
const selectedPrimary = actionable.find(profile => profile.id === "node-quality")
|
|
717
|
+
?? actionable[0]
|
|
718
|
+
?? null;
|
|
719
|
+
const rootSurfacePaths = inputManifest
|
|
720
|
+
.map(entry => entry.path)
|
|
721
|
+
.filter(ref => !ref.includes("/"));
|
|
722
|
+
return {
|
|
723
|
+
schemaVersion: "OwlCodaRunKitProfilesV1",
|
|
724
|
+
profiles: candidates.map(profile => ({
|
|
725
|
+
...profile,
|
|
726
|
+
paths: [...new Set([
|
|
727
|
+
...profile.paths,
|
|
728
|
+
...(profile.id === selectedPrimary?.id ? rootSurfacePaths : []),
|
|
729
|
+
])].sort(),
|
|
730
|
+
primary: profile.id === selectedPrimary?.id,
|
|
731
|
+
})),
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
function profilesFileHash(root) {
|
|
736
|
+
const ref = path.join(root, ".owlcoda", "runkit", "profiles.json");
|
|
737
|
+
if (!existsSync(ref)) return null;
|
|
738
|
+
return sha256(readFileSync(boundedRegularFile(
|
|
739
|
+
root,
|
|
740
|
+
path.join(".owlcoda", "runkit", "profiles.json"),
|
|
741
|
+
)));
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
function exactRegularFileBytes(absolutePath, expectedBytes) {
|
|
745
|
+
try {
|
|
746
|
+
const stat = lstatSync(absolutePath);
|
|
747
|
+
return !stat.isSymbolicLink()
|
|
748
|
+
&& stat.isFile()
|
|
749
|
+
&& realpathSync(absolutePath) === absolutePath
|
|
750
|
+
&& readFileSync(absolutePath, "utf8") === expectedBytes;
|
|
751
|
+
} catch {
|
|
752
|
+
return false;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
function commitCreateOnlyStagedFile(stagedPath, finalPath, expectedBytes) {
|
|
757
|
+
if (!exactRegularFileBytes(stagedPath, expectedBytes)) {
|
|
758
|
+
throw new Error("profile_apply_staged_file_invalid");
|
|
759
|
+
}
|
|
760
|
+
try {
|
|
761
|
+
linkSync(stagedPath, finalPath);
|
|
762
|
+
} catch (error) {
|
|
763
|
+
if (
|
|
764
|
+
error?.code !== "EEXIST"
|
|
765
|
+
|| !exactRegularFileBytes(finalPath, expectedBytes)
|
|
766
|
+
) {
|
|
767
|
+
throw error;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
rmSync(stagedPath, { force: true });
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
function recoverProfileApplyTransaction({
|
|
774
|
+
root,
|
|
775
|
+
transactionRoot,
|
|
776
|
+
profilesPath,
|
|
777
|
+
profilesBytes,
|
|
778
|
+
receiptAbsolutePath,
|
|
779
|
+
receiptBytes,
|
|
780
|
+
receipt,
|
|
781
|
+
}) {
|
|
782
|
+
const transactionStat = lstatSync(transactionRoot);
|
|
783
|
+
if (
|
|
784
|
+
transactionStat.isSymbolicLink()
|
|
785
|
+
|| !transactionStat.isDirectory()
|
|
786
|
+
|| realpathSync(transactionRoot) !== transactionRoot
|
|
787
|
+
|| !insideRoot(root, transactionRoot)
|
|
788
|
+
) {
|
|
789
|
+
throw new Error("profile_apply_transaction_directory_untrusted");
|
|
790
|
+
}
|
|
791
|
+
const entries = readdirSync(transactionRoot).sort();
|
|
792
|
+
if (entries.some((entry) => !["profiles.json", "receipt.json"].includes(entry))) {
|
|
793
|
+
throw new Error("profile_apply_transaction_contents_untrusted");
|
|
794
|
+
}
|
|
795
|
+
const stagedProfiles = path.join(transactionRoot, "profiles.json");
|
|
796
|
+
const stagedReceipt = path.join(transactionRoot, "receipt.json");
|
|
797
|
+
const currentProfilesSha256 = profilesFileHash(root);
|
|
798
|
+
const receiptExists = existsSync(receiptAbsolutePath);
|
|
799
|
+
if (
|
|
800
|
+
currentProfilesSha256 === null
|
|
801
|
+
&& !exactRegularFileBytes(stagedProfiles, profilesBytes)
|
|
802
|
+
) {
|
|
803
|
+
throw new Error("profile_apply_transaction_staged_profile_mismatch");
|
|
804
|
+
}
|
|
805
|
+
if (
|
|
806
|
+
currentProfilesSha256 !== null
|
|
807
|
+
&& currentProfilesSha256 !== receipt.appliedProfilesSha256
|
|
808
|
+
) {
|
|
809
|
+
throw new Error("profile_apply_transaction_profile_mismatch");
|
|
810
|
+
}
|
|
811
|
+
if (
|
|
812
|
+
currentProfilesSha256 !== null
|
|
813
|
+
&& existsSync(stagedProfiles)
|
|
814
|
+
&& !exactRegularFileBytes(stagedProfiles, profilesBytes)
|
|
815
|
+
) {
|
|
816
|
+
throw new Error("profile_apply_transaction_staged_profile_mismatch");
|
|
817
|
+
}
|
|
818
|
+
if (
|
|
819
|
+
receiptExists
|
|
820
|
+
? !exactRegularFileBytes(receiptAbsolutePath, receiptBytes)
|
|
821
|
+
: !exactRegularFileBytes(stagedReceipt, receiptBytes)
|
|
822
|
+
) {
|
|
823
|
+
throw new Error("profile_apply_transaction_receipt_mismatch");
|
|
824
|
+
}
|
|
825
|
+
let profilesCommittedByRecovery = false;
|
|
826
|
+
try {
|
|
827
|
+
if (currentProfilesSha256 === null) {
|
|
828
|
+
commitCreateOnlyStagedFile(stagedProfiles, profilesPath, profilesBytes);
|
|
829
|
+
profilesCommittedByRecovery = true;
|
|
830
|
+
} else {
|
|
831
|
+
rmSync(stagedProfiles, { force: true });
|
|
832
|
+
}
|
|
833
|
+
if (receiptExists) {
|
|
834
|
+
rmSync(stagedReceipt, { force: true });
|
|
835
|
+
} else {
|
|
836
|
+
commitCreateOnlyStagedFile(
|
|
837
|
+
stagedReceipt,
|
|
838
|
+
receiptAbsolutePath,
|
|
839
|
+
receiptBytes,
|
|
840
|
+
);
|
|
841
|
+
}
|
|
842
|
+
} catch (error) {
|
|
843
|
+
if (
|
|
844
|
+
profilesCommittedByRecovery
|
|
845
|
+
&& profilesFileHash(root) === receipt.appliedProfilesSha256
|
|
846
|
+
) {
|
|
847
|
+
try {
|
|
848
|
+
linkSync(profilesPath, stagedProfiles);
|
|
849
|
+
} catch (linkError) {
|
|
850
|
+
if (
|
|
851
|
+
linkError?.code !== "EEXIST"
|
|
852
|
+
|| !exactRegularFileBytes(stagedProfiles, profilesBytes)
|
|
853
|
+
) {
|
|
854
|
+
throw linkError;
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
rmSync(profilesPath);
|
|
858
|
+
}
|
|
859
|
+
throw error;
|
|
860
|
+
}
|
|
861
|
+
rmSync(transactionRoot, { recursive: true });
|
|
862
|
+
return receipt;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
export function detectProfilesV2({
|
|
866
|
+
workspaceRoot,
|
|
867
|
+
projectToolResolver,
|
|
868
|
+
} = {}) {
|
|
869
|
+
const root = resolveBoundedWorkspaceRoot(workspaceRoot);
|
|
870
|
+
const manifest = readWorkspaceJsonBounded(root, "package.json");
|
|
871
|
+
const discovered = discoverWorkspaces(root, manifest);
|
|
872
|
+
const toolBinding = resolveProjectTool(root, manifest, projectToolResolver);
|
|
873
|
+
const rawCandidates = profileCandidates(
|
|
874
|
+
root,
|
|
875
|
+
manifest,
|
|
876
|
+
toolBinding.executable,
|
|
877
|
+
discovered.workspaces,
|
|
878
|
+
toolBinding.packageManager,
|
|
879
|
+
true,
|
|
880
|
+
);
|
|
881
|
+
const candidates = bindCommandsToProjectTool(rawCandidates, toolBinding);
|
|
882
|
+
let inputManifest = [];
|
|
883
|
+
const issueCodes = [
|
|
884
|
+
...toolBinding.issueCodes,
|
|
885
|
+
...discovered.warnings,
|
|
886
|
+
];
|
|
887
|
+
try {
|
|
888
|
+
inputManifest = detectedInputManifest(root, discovered.workspaces, toolBinding);
|
|
889
|
+
} catch (error) {
|
|
890
|
+
issueCodes.push(error instanceof Error ? error.message : String(error));
|
|
891
|
+
}
|
|
892
|
+
const proposedProfiles = proposedProfileDocument(candidates, inputManifest);
|
|
893
|
+
issueCodes.push(...validateProfileDocument(proposedProfiles));
|
|
894
|
+
const qualityIssues = [...new Set(issueCodes)].sort();
|
|
895
|
+
const confidence = qualityIssues.length === 0 && toolBinding.confidence === "high"
|
|
896
|
+
? "high"
|
|
897
|
+
: "review_required";
|
|
898
|
+
const currentProfilesSha256 = profilesFileHash(root);
|
|
899
|
+
const applyStatus = currentProfilesSha256 !== null
|
|
900
|
+
? "profiles_already_exists"
|
|
901
|
+
: confidence === "high"
|
|
902
|
+
? "ready_to_apply"
|
|
903
|
+
: "review_required";
|
|
904
|
+
const body = {
|
|
905
|
+
schemaVersion: "OwlCodaRunKitProfileDetectionV2",
|
|
906
|
+
status: "profiles_detected",
|
|
907
|
+
dryRun: true,
|
|
908
|
+
writesPerformed: 0,
|
|
909
|
+
confidence,
|
|
910
|
+
applyStatus,
|
|
911
|
+
issueCodes: qualityIssues,
|
|
912
|
+
proposedProfiles,
|
|
913
|
+
candidates,
|
|
914
|
+
primaryProfileId: proposedProfiles.profiles.find(profile => profile.primary)?.id ?? null,
|
|
915
|
+
inputManifest,
|
|
916
|
+
toolBinding: {
|
|
917
|
+
status: toolBinding.status,
|
|
918
|
+
confidence: toolBinding.confidence,
|
|
919
|
+
source: toolBinding.source,
|
|
920
|
+
packageManager: toolBinding.packageManager,
|
|
921
|
+
executable: toolBinding.executable,
|
|
922
|
+
argvPrefix: [...toolBinding.argvPrefix],
|
|
923
|
+
version: toolBinding.version,
|
|
924
|
+
executableSha256: toolBinding.executableSha256,
|
|
925
|
+
launcherSha256: toolBinding.launcherSha256,
|
|
926
|
+
},
|
|
927
|
+
currentProfilesSha256,
|
|
928
|
+
detectedScripts: detectedScripts(manifest),
|
|
929
|
+
detectedWorkspaces: discovered.workspaces,
|
|
930
|
+
authorizationGranted: false,
|
|
931
|
+
};
|
|
932
|
+
return {
|
|
933
|
+
...body,
|
|
934
|
+
detectionSha256: sha256(canonicalJson(body)),
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
function blockedApply(detection, issueCodes, currentProfilesSha256 = null) {
|
|
939
|
+
return {
|
|
940
|
+
schemaVersion: "OwlCodaRunKitProfilesApplyReceiptV1",
|
|
941
|
+
status: "profiles_apply_blocked",
|
|
942
|
+
writesPerformed: 0,
|
|
943
|
+
detectionSha256: detection?.detectionSha256 ?? null,
|
|
944
|
+
currentProfilesSha256,
|
|
945
|
+
issueCodes: [...new Set(issueCodes)].sort(),
|
|
946
|
+
authorizationGranted: false,
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
function verifyDetectionInputs(root, detection) {
|
|
951
|
+
const issues = [];
|
|
952
|
+
const { detectionSha256, ...body } = detection;
|
|
953
|
+
if (
|
|
954
|
+
detection.schemaVersion !== "OwlCodaRunKitProfileDetectionV2"
|
|
955
|
+
|| detection.status !== "profiles_detected"
|
|
956
|
+
|| !/^[a-f0-9]{64}$/.test(detectionSha256 ?? "")
|
|
957
|
+
|| sha256(canonicalJson(body)) !== detectionSha256
|
|
958
|
+
) {
|
|
959
|
+
issues.push("profile_detection_invalid");
|
|
960
|
+
}
|
|
961
|
+
if (detection.confidence !== "high" || detection.applyStatus !== "ready_to_apply") {
|
|
962
|
+
issues.push("profile_detection_not_ready_to_apply");
|
|
963
|
+
}
|
|
964
|
+
issues.push(...validateProfileDocument(detection.proposedProfiles));
|
|
965
|
+
const executable = regularExecutable(detection.toolBinding?.executable);
|
|
966
|
+
const launcher = regularExecutable(detection.toolBinding?.argvPrefix?.[0]);
|
|
967
|
+
if (
|
|
968
|
+
!executable
|
|
969
|
+
|| !launcher
|
|
970
|
+
|| sha256(readFileSync(executable)) !== detection.toolBinding.executableSha256
|
|
971
|
+
|| sha256(readFileSync(launcher)) !== detection.toolBinding.launcherSha256
|
|
972
|
+
) {
|
|
973
|
+
issues.push("profile_detection_tool_drift");
|
|
974
|
+
}
|
|
975
|
+
if (!Array.isArray(detection.inputManifest)) {
|
|
976
|
+
issues.push("profile_detection_invalid");
|
|
977
|
+
} else {
|
|
978
|
+
for (const entry of detection.inputManifest) {
|
|
979
|
+
try {
|
|
980
|
+
const actual = sha256(readFileSync(boundedRegularFile(root, entry.path)));
|
|
981
|
+
if (actual !== entry.sha256) issues.push(`profile_detection_input_drift:${entry.path}`);
|
|
982
|
+
} catch {
|
|
983
|
+
issues.push(`profile_detection_input_unavailable:${entry.path}`);
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
return [...new Set(issues)].sort();
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
export function applyDetectedProfilesV2({
|
|
991
|
+
workspaceRoot,
|
|
992
|
+
detection,
|
|
993
|
+
} = {}) {
|
|
994
|
+
const root = resolveBoundedWorkspaceRoot(workspaceRoot);
|
|
995
|
+
let currentProfilesSha256;
|
|
996
|
+
try {
|
|
997
|
+
currentProfilesSha256 = profilesFileHash(root);
|
|
998
|
+
} catch {
|
|
999
|
+
return blockedApply(detection, ["profile_apply_directory_untrusted"]);
|
|
1000
|
+
}
|
|
1001
|
+
const issues = verifyDetectionInputs(root, detection ?? {});
|
|
1002
|
+
if (issues.length > 0) return blockedApply(detection, issues);
|
|
1003
|
+
|
|
1004
|
+
const receiptPath = path.join(
|
|
1005
|
+
".owlcoda",
|
|
1006
|
+
"runkit",
|
|
1007
|
+
"profile-apply-receipts",
|
|
1008
|
+
`${detection.detectionSha256}.json`,
|
|
1009
|
+
);
|
|
1010
|
+
let runtimeRoot;
|
|
1011
|
+
let receiptRoot;
|
|
1012
|
+
try {
|
|
1013
|
+
runtimeRoot = ensureDirectoryChain(root, ".owlcoda/runkit");
|
|
1014
|
+
receiptRoot = ensureDirectoryChain(
|
|
1015
|
+
root,
|
|
1016
|
+
".owlcoda/runkit/profile-apply-receipts",
|
|
1017
|
+
);
|
|
1018
|
+
} catch {
|
|
1019
|
+
return blockedApply(detection, ["profile_apply_directory_untrusted"]);
|
|
1020
|
+
}
|
|
1021
|
+
const profilesPath = path.join(runtimeRoot, "profiles.json");
|
|
1022
|
+
const receiptAbsolutePath = path.join(
|
|
1023
|
+
receiptRoot,
|
|
1024
|
+
`${detection.detectionSha256}.json`,
|
|
1025
|
+
);
|
|
1026
|
+
const profilesBytes = `${JSON.stringify(detection.proposedProfiles, null, 2)}\n`;
|
|
1027
|
+
const receipt = {
|
|
1028
|
+
schemaVersion: "OwlCodaRunKitProfilesApplyReceiptV1",
|
|
1029
|
+
status: "profiles_applied",
|
|
1030
|
+
writesPerformed: 2,
|
|
1031
|
+
detectionSha256: detection.detectionSha256,
|
|
1032
|
+
beforeProfilesSha256: null,
|
|
1033
|
+
appliedProfilesSha256: sha256(profilesBytes),
|
|
1034
|
+
receiptPath,
|
|
1035
|
+
issueCodes: [],
|
|
1036
|
+
authorizationGranted: false,
|
|
1037
|
+
};
|
|
1038
|
+
const receiptBytes = `${JSON.stringify(receipt, null, 2)}\n`;
|
|
1039
|
+
const transactionRoot = path.join(
|
|
1040
|
+
runtimeRoot,
|
|
1041
|
+
`.profile-apply-${detection.detectionSha256}.lock`,
|
|
1042
|
+
);
|
|
1043
|
+
if (existsSync(transactionRoot)) {
|
|
1044
|
+
try {
|
|
1045
|
+
return recoverProfileApplyTransaction({
|
|
1046
|
+
root,
|
|
1047
|
+
transactionRoot,
|
|
1048
|
+
profilesPath,
|
|
1049
|
+
profilesBytes,
|
|
1050
|
+
receiptAbsolutePath,
|
|
1051
|
+
receiptBytes,
|
|
1052
|
+
receipt,
|
|
1053
|
+
});
|
|
1054
|
+
} catch {
|
|
1055
|
+
return blockedApply(
|
|
1056
|
+
detection,
|
|
1057
|
+
["profile_apply_transaction_recovery_invalid"],
|
|
1058
|
+
currentProfilesSha256,
|
|
1059
|
+
);
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
if (currentProfilesSha256 !== null) {
|
|
1063
|
+
return blockedApply(detection, ["profiles_already_exists"], currentProfilesSha256);
|
|
1064
|
+
}
|
|
1065
|
+
try {
|
|
1066
|
+
mkdirSync(transactionRoot);
|
|
1067
|
+
} catch (error) {
|
|
1068
|
+
if (error?.code === "EEXIST") {
|
|
1069
|
+
return blockedApply(detection, ["profile_apply_transaction_active"]);
|
|
1070
|
+
}
|
|
1071
|
+
throw error;
|
|
1072
|
+
}
|
|
1073
|
+
let profilesCommitted = false;
|
|
1074
|
+
try {
|
|
1075
|
+
const current = profilesFileHash(root);
|
|
1076
|
+
if (current !== null) {
|
|
1077
|
+
return blockedApply(detection, ["profiles_already_exists"], current);
|
|
1078
|
+
}
|
|
1079
|
+
if (existsSync(receiptAbsolutePath)) {
|
|
1080
|
+
return blockedApply(detection, ["profile_apply_receipt_already_exists"]);
|
|
1081
|
+
}
|
|
1082
|
+
const stagedProfiles = path.join(transactionRoot, "profiles.json");
|
|
1083
|
+
const stagedReceipt = path.join(transactionRoot, "receipt.json");
|
|
1084
|
+
writeFileSync(stagedProfiles, profilesBytes, { flag: "wx" });
|
|
1085
|
+
writeFileSync(stagedReceipt, receiptBytes, { flag: "wx" });
|
|
1086
|
+
commitCreateOnlyStagedFile(stagedProfiles, profilesPath, profilesBytes);
|
|
1087
|
+
profilesCommitted = true;
|
|
1088
|
+
commitCreateOnlyStagedFile(
|
|
1089
|
+
stagedReceipt,
|
|
1090
|
+
receiptAbsolutePath,
|
|
1091
|
+
receiptBytes,
|
|
1092
|
+
);
|
|
1093
|
+
return receipt;
|
|
1094
|
+
} catch {
|
|
1095
|
+
if (
|
|
1096
|
+
profilesCommitted
|
|
1097
|
+
&& existsSync(profilesPath)
|
|
1098
|
+
&& profilesFileHash(root) === receipt.appliedProfilesSha256
|
|
1099
|
+
) {
|
|
1100
|
+
rmSync(profilesPath);
|
|
1101
|
+
}
|
|
1102
|
+
return blockedApply(detection, ["profile_apply_transaction_failed"]);
|
|
1103
|
+
} finally {
|
|
1104
|
+
rmSync(transactionRoot, { recursive: true, force: true });
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
|
|
280
1108
|
function exactKeys(value, allowed) {
|
|
281
1109
|
return value
|
|
282
1110
|
&& typeof value === "object"
|