owlrunkit 0.17.2 → 0.18.1
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 +64 -0
- package/README.md +117 -14
- package/docs/architecture/OWLCODA_RUNKIT_017_AGENT_NATIVE_PRODUCT_DEFINITION.md +2 -3
- package/docs/architecture/OWLCODA_RUNKIT_018_PROJECT_DRIVER_PRODUCT_DEFINITION.md +165 -0
- package/docs/architecture/OWLCODA_RUNKIT_USAGE_MODE_SELECTION_PRODUCT_DECISION_20260811.md +142 -0
- package/integrations/codex/skills/owlcoda-runkit/SKILL.md +127 -23
- package/integrations/codex/skills/owlcoda-runkit/assets/templates/config.json +3 -3
- 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 +5 -5
- package/sbom.spdx.json +5 -5
- package/scripts/runkit-contract/assurance-router.mjs +28 -20
- package/scripts/runkit-contract/core-contract.mjs +7 -1
- package/scripts/runkit-contract/inspect-presentation.mjs +38 -5
- package/scripts/runkit-contract/install-codex-skill.mjs +60 -1
- package/scripts/runkit-contract/onboarding-doctor.mjs +19 -1
- package/scripts/runkit-contract/profile-onboarding.mjs +597 -22
- package/scripts/runkit-contract/project-cli-resolver.mjs +6 -0
- package/scripts/runkit-contract/quick-attest.mjs +1 -0
- package/scripts/runkit-contract/quick-metrics.mjs +43 -5
- package/scripts/runkit-contract/quick-verify.mjs +15 -4
- package/scripts/runkit-contract/runkit-cli.mjs +747 -20
- package/scripts/runkit-contract/schemas/profile-reconcile-plan-v1.schema.json +105 -0
- package/scripts/runkit-contract/schemas/profile-reconcile-receipt-v1.schema.json +60 -0
- package/scripts/runkit-contract/schemas/team-project-event-v1.schema.json +16 -1
- package/scripts/runkit-contract/schemas/team-project-status-v1.schema.json +34 -1
- package/scripts/runkit-contract/schemas/team-project-successor-receipt-v1.schema.json +204 -0
- package/scripts/runkit-contract/schemas/usage-mode-recommendation-v1.schema.json +136 -0
- package/scripts/runkit-contract/team-project-successor.mjs +1176 -0
- package/scripts/runkit-contract/team-project.mjs +1082 -43
- package/scripts/runkit-contract/templates/assurance-request.json +6 -0
- package/scripts/runkit-contract/usage-mode.mjs +229 -0
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
readFileSync,
|
|
8
8
|
readdirSync,
|
|
9
9
|
realpathSync,
|
|
10
|
+
renameSync,
|
|
10
11
|
rmSync,
|
|
11
12
|
writeFileSync,
|
|
12
13
|
} from "node:fs";
|
|
@@ -550,6 +551,48 @@ function workspaceProfiles(
|
|
|
550
551
|
.filter(Boolean);
|
|
551
552
|
}
|
|
552
553
|
|
|
554
|
+
function existingProfileSurface(root, relativePath, profilePath) {
|
|
555
|
+
const absolute = path.join(root, relativePath);
|
|
556
|
+
try {
|
|
557
|
+
const stat = lstatSync(absolute);
|
|
558
|
+
return stat.isDirectory()
|
|
559
|
+
&& !stat.isSymbolicLink()
|
|
560
|
+
&& realpathSync(absolute) === path.resolve(absolute)
|
|
561
|
+
? profilePath
|
|
562
|
+
: null;
|
|
563
|
+
} catch {
|
|
564
|
+
return null;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function existingProfileFile(root, candidates) {
|
|
569
|
+
for (const relativePath of candidates) {
|
|
570
|
+
try {
|
|
571
|
+
const absolute = path.join(root, relativePath);
|
|
572
|
+
const stat = lstatSync(absolute);
|
|
573
|
+
if (
|
|
574
|
+
stat.isFile()
|
|
575
|
+
&& !stat.isSymbolicLink()
|
|
576
|
+
&& realpathSync(absolute) === path.resolve(absolute)
|
|
577
|
+
) {
|
|
578
|
+
return relativePath;
|
|
579
|
+
}
|
|
580
|
+
} catch {
|
|
581
|
+
// Continue to the next supported filename.
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
return null;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function realOrManifestSurface(root, directorySurfaces) {
|
|
588
|
+
const paths = directorySurfaces
|
|
589
|
+
.map(([relativePath, profilePath]) => (
|
|
590
|
+
existingProfileSurface(root, relativePath, profilePath)
|
|
591
|
+
))
|
|
592
|
+
.filter(Boolean);
|
|
593
|
+
return paths.length > 0 ? paths : ["package.json"];
|
|
594
|
+
}
|
|
595
|
+
|
|
553
596
|
function profileCandidates(
|
|
554
597
|
root,
|
|
555
598
|
manifest,
|
|
@@ -580,7 +623,7 @@ function profileCandidates(
|
|
|
580
623
|
.filter(Boolean);
|
|
581
624
|
candidates.push({
|
|
582
625
|
id: "database",
|
|
583
|
-
paths: ["prisma/**"],
|
|
626
|
+
paths: realOrManifestSurface(root, [["prisma", "prisma/**"]]),
|
|
584
627
|
role: "primary",
|
|
585
628
|
primary: false,
|
|
586
629
|
requiresProfileIds: [],
|
|
@@ -599,7 +642,10 @@ function profileCandidates(
|
|
|
599
642
|
.filter(Boolean);
|
|
600
643
|
candidates.push({
|
|
601
644
|
id: "node-quality",
|
|
602
|
-
paths:
|
|
645
|
+
paths: realOrManifestSurface(root, [
|
|
646
|
+
["src", "src/**"],
|
|
647
|
+
["tests", "tests/**"],
|
|
648
|
+
]),
|
|
603
649
|
role: "primary",
|
|
604
650
|
primary: false,
|
|
605
651
|
requiresProfileIds: [],
|
|
@@ -614,9 +660,17 @@ function profileCandidates(
|
|
|
614
660
|
const commands = scripts.has("e2e") && npmExecutable
|
|
615
661
|
? [command(`${packageManager}-e2e`, "e2e", npmExecutable)]
|
|
616
662
|
: [];
|
|
663
|
+
const configPath = existingProfileFile(root, [
|
|
664
|
+
"playwright.config.ts",
|
|
665
|
+
"playwright.config.js",
|
|
666
|
+
]);
|
|
667
|
+
const paths = [
|
|
668
|
+
configPath,
|
|
669
|
+
existingProfileSurface(root, "tests/e2e", "tests/e2e/**"),
|
|
670
|
+
].filter(Boolean);
|
|
617
671
|
candidates.push({
|
|
618
672
|
id: "web-e2e",
|
|
619
|
-
paths: ["
|
|
673
|
+
paths: paths.length > 0 ? paths : ["package.json"],
|
|
620
674
|
role: "primary",
|
|
621
675
|
primary: false,
|
|
622
676
|
requiresProfileIds: [],
|
|
@@ -753,6 +807,34 @@ function exactRegularFileBytes(absolutePath, expectedBytes) {
|
|
|
753
807
|
}
|
|
754
808
|
}
|
|
755
809
|
|
|
810
|
+
function readRegularFileInsideRoot(root, absolutePath) {
|
|
811
|
+
const stat = lstatSync(absolutePath);
|
|
812
|
+
const resolved = realpathSync(absolutePath);
|
|
813
|
+
if (
|
|
814
|
+
stat.isSymbolicLink()
|
|
815
|
+
|| !stat.isFile()
|
|
816
|
+
|| resolved !== absolutePath
|
|
817
|
+
|| !insideRoot(root, resolved)
|
|
818
|
+
) {
|
|
819
|
+
throw new Error("profile_transaction_file_untrusted");
|
|
820
|
+
}
|
|
821
|
+
return readFileSync(absolutePath, "utf8");
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
function trustedDirectoryInsideRoot(root, absolutePath) {
|
|
825
|
+
const stat = lstatSync(absolutePath);
|
|
826
|
+
const resolved = realpathSync(absolutePath);
|
|
827
|
+
if (
|
|
828
|
+
stat.isSymbolicLink()
|
|
829
|
+
|| !stat.isDirectory()
|
|
830
|
+
|| resolved !== absolutePath
|
|
831
|
+
|| !insideRoot(root, resolved)
|
|
832
|
+
) {
|
|
833
|
+
throw new Error("profile_transaction_directory_untrusted");
|
|
834
|
+
}
|
|
835
|
+
return resolved;
|
|
836
|
+
}
|
|
837
|
+
|
|
756
838
|
function commitCreateOnlyStagedFile(stagedPath, finalPath, expectedBytes) {
|
|
757
839
|
if (!exactRegularFileBytes(stagedPath, expectedBytes)) {
|
|
758
840
|
throw new Error("profile_apply_staged_file_invalid");
|
|
@@ -947,7 +1029,11 @@ function blockedApply(detection, issueCodes, currentProfilesSha256 = null) {
|
|
|
947
1029
|
};
|
|
948
1030
|
}
|
|
949
1031
|
|
|
950
|
-
function verifyDetectionInputs(
|
|
1032
|
+
function verifyDetectionInputs(
|
|
1033
|
+
root,
|
|
1034
|
+
detection,
|
|
1035
|
+
{ allowedApplyStatuses = ["ready_to_apply"] } = {},
|
|
1036
|
+
) {
|
|
951
1037
|
const issues = [];
|
|
952
1038
|
const { detectionSha256, ...body } = detection;
|
|
953
1039
|
if (
|
|
@@ -958,7 +1044,10 @@ function verifyDetectionInputs(root, detection) {
|
|
|
958
1044
|
) {
|
|
959
1045
|
issues.push("profile_detection_invalid");
|
|
960
1046
|
}
|
|
961
|
-
if (
|
|
1047
|
+
if (
|
|
1048
|
+
detection.confidence !== "high"
|
|
1049
|
+
|| !allowedApplyStatuses.includes(detection.applyStatus)
|
|
1050
|
+
) {
|
|
962
1051
|
issues.push("profile_detection_not_ready_to_apply");
|
|
963
1052
|
}
|
|
964
1053
|
issues.push(...validateProfileDocument(detection.proposedProfiles));
|
|
@@ -1105,6 +1194,461 @@ export function applyDetectedProfilesV2({
|
|
|
1105
1194
|
}
|
|
1106
1195
|
}
|
|
1107
1196
|
|
|
1197
|
+
function mergeDetectedProfiles(currentProfiles, detectedProfiles) {
|
|
1198
|
+
const profiles = currentProfiles.profiles.map((profile) => structuredClone(profile));
|
|
1199
|
+
const byId = new Map(profiles.map((profile) => [profile.id, profile]));
|
|
1200
|
+
const addedProfileIds = [];
|
|
1201
|
+
const updatedProfiles = [];
|
|
1202
|
+
for (const detected of [...detectedProfiles.profiles]
|
|
1203
|
+
.sort((left, right) => left.id.localeCompare(right.id))) {
|
|
1204
|
+
const current = byId.get(detected.id);
|
|
1205
|
+
if (!current) {
|
|
1206
|
+
const impact = resolveProfileImpactDetailed({
|
|
1207
|
+
changedPaths: detected.paths.map(representativePath),
|
|
1208
|
+
profiles,
|
|
1209
|
+
});
|
|
1210
|
+
const primaryCoverageComplete = impact.uncoveredPaths.length === 0
|
|
1211
|
+
&& !impact.warnings.some((warning) => warning.startsWith("supporting_only_match:"));
|
|
1212
|
+
if (primaryCoverageComplete) continue;
|
|
1213
|
+
const added = { ...structuredClone(detected), primary: false };
|
|
1214
|
+
profiles.push(added);
|
|
1215
|
+
byId.set(added.id, added);
|
|
1216
|
+
addedProfileIds.push(added.id);
|
|
1217
|
+
continue;
|
|
1218
|
+
}
|
|
1219
|
+
const addedPaths = detected.paths
|
|
1220
|
+
.filter((value) => !current.paths.includes(value))
|
|
1221
|
+
.sort();
|
|
1222
|
+
current.paths = [...new Set([...current.paths, ...detected.paths])].sort();
|
|
1223
|
+
const commands = new Map(current.commands.map((entry) => [entry.id, entry]));
|
|
1224
|
+
const addedCommandIds = [];
|
|
1225
|
+
const updatedCommandIds = [];
|
|
1226
|
+
for (const detectedCommand of detected.commands) {
|
|
1227
|
+
const currentCommand = commands.get(detectedCommand.id);
|
|
1228
|
+
if (!currentCommand) {
|
|
1229
|
+
current.commands.push(structuredClone(detectedCommand));
|
|
1230
|
+
commands.set(detectedCommand.id, detectedCommand);
|
|
1231
|
+
addedCommandIds.push(detectedCommand.id);
|
|
1232
|
+
} else if (canonicalJson(currentCommand) !== canonicalJson(detectedCommand)) {
|
|
1233
|
+
const index = current.commands.findIndex((entry) => entry.id === detectedCommand.id);
|
|
1234
|
+
current.commands[index] = structuredClone(detectedCommand);
|
|
1235
|
+
commands.set(detectedCommand.id, detectedCommand);
|
|
1236
|
+
updatedCommandIds.push(detectedCommand.id);
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
current.commands.sort((left, right) => left.id.localeCompare(right.id));
|
|
1240
|
+
if (
|
|
1241
|
+
addedPaths.length > 0
|
|
1242
|
+
|| addedCommandIds.length > 0
|
|
1243
|
+
|| updatedCommandIds.length > 0
|
|
1244
|
+
) {
|
|
1245
|
+
updatedProfiles.push({
|
|
1246
|
+
profileId: current.id,
|
|
1247
|
+
addedPaths,
|
|
1248
|
+
addedCommandIds: addedCommandIds.sort(),
|
|
1249
|
+
updatedCommandIds: updatedCommandIds.sort(),
|
|
1250
|
+
});
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
return {
|
|
1254
|
+
proposedProfiles: {
|
|
1255
|
+
schemaVersion: "OwlCodaRunKitProfilesV1",
|
|
1256
|
+
profiles,
|
|
1257
|
+
},
|
|
1258
|
+
addedProfileIds,
|
|
1259
|
+
updatedProfiles,
|
|
1260
|
+
};
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
function blockedReconcile(
|
|
1264
|
+
detection,
|
|
1265
|
+
issueCodes,
|
|
1266
|
+
currentProfilesSha256 = null,
|
|
1267
|
+
dryRun = true,
|
|
1268
|
+
) {
|
|
1269
|
+
return {
|
|
1270
|
+
schemaVersion: "OwlCodaRunKitProfilesReconcilePlanV1",
|
|
1271
|
+
status: "profiles_reconcile_blocked",
|
|
1272
|
+
valid: false,
|
|
1273
|
+
exitCode: 2,
|
|
1274
|
+
dryRun,
|
|
1275
|
+
writesPerformed: 0,
|
|
1276
|
+
detectionSha256: detection?.detectionSha256 ?? null,
|
|
1277
|
+
currentProfilesSha256,
|
|
1278
|
+
issueCodes: [...new Set(issueCodes)].sort(),
|
|
1279
|
+
authorizationGranted: false,
|
|
1280
|
+
};
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
function validateReconcileReceipt(receipt, reconcileSha256) {
|
|
1284
|
+
const hash = /^[a-f0-9]{64}$/;
|
|
1285
|
+
const receiptKeys = [
|
|
1286
|
+
"schemaVersion",
|
|
1287
|
+
"status",
|
|
1288
|
+
"exitCode",
|
|
1289
|
+
"writesPerformed",
|
|
1290
|
+
"dryRun",
|
|
1291
|
+
"reconcileSha256",
|
|
1292
|
+
"detectionSha256",
|
|
1293
|
+
"beforeProfilesSha256",
|
|
1294
|
+
"appliedProfilesSha256",
|
|
1295
|
+
"addedProfileIds",
|
|
1296
|
+
"updatedProfiles",
|
|
1297
|
+
"receiptPath",
|
|
1298
|
+
"issueCodes",
|
|
1299
|
+
"authorizationGranted",
|
|
1300
|
+
];
|
|
1301
|
+
const receiptPath = path.join(
|
|
1302
|
+
".owlcoda",
|
|
1303
|
+
"runkit",
|
|
1304
|
+
"profile-reconcile-receipts",
|
|
1305
|
+
`${reconcileSha256}.json`,
|
|
1306
|
+
);
|
|
1307
|
+
if (
|
|
1308
|
+
!exactKeys(receipt, receiptKeys)
|
|
1309
|
+
|| receiptKeys.some((key) => !Object.hasOwn(receipt, key))
|
|
1310
|
+
|| receipt.schemaVersion !== "OwlCodaRunKitProfilesReconcileReceiptV1"
|
|
1311
|
+
|| receipt.status !== "profiles_reconciled"
|
|
1312
|
+
|| receipt.exitCode !== 0
|
|
1313
|
+
|| receipt.writesPerformed !== 2
|
|
1314
|
+
|| receipt.dryRun !== false
|
|
1315
|
+
|| receipt.reconcileSha256 !== reconcileSha256
|
|
1316
|
+
|| !hash.test(receipt.detectionSha256 ?? "")
|
|
1317
|
+
|| !hash.test(receipt.beforeProfilesSha256 ?? "")
|
|
1318
|
+
|| !hash.test(receipt.appliedProfilesSha256 ?? "")
|
|
1319
|
+
|| !Array.isArray(receipt.addedProfileIds)
|
|
1320
|
+
|| receipt.addedProfileIds.some((value) => !safeIdentifier(value))
|
|
1321
|
+
|| !Array.isArray(receipt.updatedProfiles)
|
|
1322
|
+
|| receipt.updatedProfiles.some((update) => (
|
|
1323
|
+
!exactKeys(update, [
|
|
1324
|
+
"profileId",
|
|
1325
|
+
"addedPaths",
|
|
1326
|
+
"addedCommandIds",
|
|
1327
|
+
"updatedCommandIds",
|
|
1328
|
+
])
|
|
1329
|
+
|| !safeIdentifier(update.profileId)
|
|
1330
|
+
|| !Array.isArray(update.addedPaths)
|
|
1331
|
+
|| update.addedPaths.some((value) => typeof value !== "string")
|
|
1332
|
+
|| !Array.isArray(update.addedCommandIds)
|
|
1333
|
+
|| update.addedCommandIds.some((value) => !safeIdentifier(value))
|
|
1334
|
+
|| !Array.isArray(update.updatedCommandIds)
|
|
1335
|
+
|| update.updatedCommandIds.some((value) => !safeIdentifier(value))
|
|
1336
|
+
))
|
|
1337
|
+
|| receipt.receiptPath !== receiptPath
|
|
1338
|
+
|| !Array.isArray(receipt.issueCodes)
|
|
1339
|
+
|| receipt.issueCodes.length !== 0
|
|
1340
|
+
|| receipt.authorizationGranted !== false
|
|
1341
|
+
) {
|
|
1342
|
+
throw new Error("profile_reconcile_transaction_receipt_invalid");
|
|
1343
|
+
}
|
|
1344
|
+
return receiptPath;
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
function recoverProfileReconcileTransaction(root) {
|
|
1348
|
+
const runtimeRoot = path.join(root, ".owlcoda", "runkit");
|
|
1349
|
+
if (!existsSync(runtimeRoot)) return null;
|
|
1350
|
+
trustedDirectoryInsideRoot(root, runtimeRoot);
|
|
1351
|
+
const transactionNames = readdirSync(runtimeRoot)
|
|
1352
|
+
.filter((entry) => entry.startsWith(".profile-reconcile-") && entry.endsWith(".lock"));
|
|
1353
|
+
if (transactionNames.length === 0) return null;
|
|
1354
|
+
if (
|
|
1355
|
+
transactionNames.length !== 1
|
|
1356
|
+
|| !/^\.profile-reconcile-[a-f0-9]{64}\.lock$/u.test(transactionNames[0])
|
|
1357
|
+
) {
|
|
1358
|
+
throw new Error("profile_reconcile_transaction_set_invalid");
|
|
1359
|
+
}
|
|
1360
|
+
const transactionName = transactionNames[0];
|
|
1361
|
+
const reconcileSha256 = transactionName.slice(
|
|
1362
|
+
".profile-reconcile-".length,
|
|
1363
|
+
-".lock".length,
|
|
1364
|
+
);
|
|
1365
|
+
const transactionRoot = path.join(runtimeRoot, transactionName);
|
|
1366
|
+
trustedDirectoryInsideRoot(root, transactionRoot);
|
|
1367
|
+
const entries = readdirSync(transactionRoot).sort();
|
|
1368
|
+
if (
|
|
1369
|
+
entries.some((entry) => ![
|
|
1370
|
+
"previous-profiles.json",
|
|
1371
|
+
"profiles.json",
|
|
1372
|
+
"receipt.json",
|
|
1373
|
+
].includes(entry))
|
|
1374
|
+
) {
|
|
1375
|
+
throw new Error("profile_reconcile_transaction_contents_untrusted");
|
|
1376
|
+
}
|
|
1377
|
+
const previousProfiles = path.join(transactionRoot, "previous-profiles.json");
|
|
1378
|
+
const stagedProfiles = path.join(transactionRoot, "profiles.json");
|
|
1379
|
+
const stagedReceipt = path.join(transactionRoot, "receipt.json");
|
|
1380
|
+
const receiptRoot = ensureDirectoryChain(
|
|
1381
|
+
root,
|
|
1382
|
+
".owlcoda/runkit/profile-reconcile-receipts",
|
|
1383
|
+
);
|
|
1384
|
+
const receiptAbsolutePath = path.join(receiptRoot, `${reconcileSha256}.json`);
|
|
1385
|
+
const receiptSource = existsSync(stagedReceipt) ? stagedReceipt : receiptAbsolutePath;
|
|
1386
|
+
const receipt = JSON.parse(readRegularFileInsideRoot(root, receiptSource));
|
|
1387
|
+
const receiptPath = validateReconcileReceipt(receipt, reconcileSha256);
|
|
1388
|
+
if (path.join(root, receiptPath) !== receiptAbsolutePath) {
|
|
1389
|
+
throw new Error("profile_reconcile_transaction_receipt_path_invalid");
|
|
1390
|
+
}
|
|
1391
|
+
const receiptBytes = `${JSON.stringify(receipt, null, 2)}\n`;
|
|
1392
|
+
if (!exactRegularFileBytes(receiptSource, receiptBytes)) {
|
|
1393
|
+
throw new Error("profile_reconcile_transaction_receipt_mismatch");
|
|
1394
|
+
}
|
|
1395
|
+
const previousBytes = readRegularFileInsideRoot(root, previousProfiles);
|
|
1396
|
+
if (sha256(previousBytes) !== receipt.beforeProfilesSha256) {
|
|
1397
|
+
throw new Error("profile_reconcile_transaction_preimage_mismatch");
|
|
1398
|
+
}
|
|
1399
|
+
const profilesPath = path.join(runtimeRoot, "profiles.json");
|
|
1400
|
+
const currentProfilesSha256 = profilesFileHash(root);
|
|
1401
|
+
if (
|
|
1402
|
+
currentProfilesSha256 !== receipt.beforeProfilesSha256
|
|
1403
|
+
&& currentProfilesSha256 !== receipt.appliedProfilesSha256
|
|
1404
|
+
) {
|
|
1405
|
+
throw new Error("profile_reconcile_transaction_profile_mismatch");
|
|
1406
|
+
}
|
|
1407
|
+
if (existsSync(stagedProfiles)) {
|
|
1408
|
+
const stagedProfilesBytes = readRegularFileInsideRoot(root, stagedProfiles);
|
|
1409
|
+
if (sha256(stagedProfilesBytes) !== receipt.appliedProfilesSha256) {
|
|
1410
|
+
throw new Error("profile_reconcile_transaction_staged_profile_mismatch");
|
|
1411
|
+
}
|
|
1412
|
+
} else if (currentProfilesSha256 !== receipt.appliedProfilesSha256) {
|
|
1413
|
+
throw new Error("profile_reconcile_transaction_staged_profile_missing");
|
|
1414
|
+
}
|
|
1415
|
+
if (
|
|
1416
|
+
existsSync(receiptAbsolutePath)
|
|
1417
|
+
&& !exactRegularFileBytes(receiptAbsolutePath, receiptBytes)
|
|
1418
|
+
) {
|
|
1419
|
+
throw new Error("profile_reconcile_transaction_committed_receipt_mismatch");
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
let profilesCommittedByRecovery = false;
|
|
1423
|
+
try {
|
|
1424
|
+
if (currentProfilesSha256 === receipt.beforeProfilesSha256) {
|
|
1425
|
+
renameSync(stagedProfiles, profilesPath);
|
|
1426
|
+
profilesCommittedByRecovery = true;
|
|
1427
|
+
}
|
|
1428
|
+
if (!existsSync(receiptAbsolutePath)) {
|
|
1429
|
+
commitCreateOnlyStagedFile(stagedReceipt, receiptAbsolutePath, receiptBytes);
|
|
1430
|
+
}
|
|
1431
|
+
} catch (error) {
|
|
1432
|
+
if (
|
|
1433
|
+
profilesCommittedByRecovery
|
|
1434
|
+
&& profilesFileHash(root) === receipt.appliedProfilesSha256
|
|
1435
|
+
) {
|
|
1436
|
+
renameSync(previousProfiles, profilesPath);
|
|
1437
|
+
}
|
|
1438
|
+
throw error;
|
|
1439
|
+
}
|
|
1440
|
+
rmSync(transactionRoot, { recursive: true });
|
|
1441
|
+
return receipt;
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
export function reconcileProfilesV2({
|
|
1445
|
+
workspaceRoot,
|
|
1446
|
+
apply = false,
|
|
1447
|
+
projectToolResolver,
|
|
1448
|
+
onAfterProfilesCommit,
|
|
1449
|
+
} = {}) {
|
|
1450
|
+
const root = resolveBoundedWorkspaceRoot(workspaceRoot);
|
|
1451
|
+
if (apply) {
|
|
1452
|
+
try {
|
|
1453
|
+
const recovered = recoverProfileReconcileTransaction(root);
|
|
1454
|
+
if (recovered !== null) return recovered;
|
|
1455
|
+
} catch {
|
|
1456
|
+
return blockedReconcile(
|
|
1457
|
+
null,
|
|
1458
|
+
["profile_reconcile_transaction_recovery_invalid"],
|
|
1459
|
+
null,
|
|
1460
|
+
false,
|
|
1461
|
+
);
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
let currentProfilesSha256;
|
|
1465
|
+
let currentProfiles;
|
|
1466
|
+
let currentProfilesBytes;
|
|
1467
|
+
try {
|
|
1468
|
+
currentProfilesSha256 = profilesFileHash(root);
|
|
1469
|
+
if (currentProfilesSha256 === null) {
|
|
1470
|
+
return blockedReconcile(null, ["profiles_missing"], null, !apply);
|
|
1471
|
+
}
|
|
1472
|
+
currentProfilesBytes = readFileSync(boundedRegularFile(
|
|
1473
|
+
root,
|
|
1474
|
+
path.join(".owlcoda", "runkit", "profiles.json"),
|
|
1475
|
+
), "utf8");
|
|
1476
|
+
currentProfiles = readWorkspaceJsonBounded(
|
|
1477
|
+
root,
|
|
1478
|
+
path.join(".owlcoda", "runkit", "profiles.json"),
|
|
1479
|
+
);
|
|
1480
|
+
const documentIssues = validateProfileDocument(currentProfiles);
|
|
1481
|
+
if (documentIssues.length > 0) {
|
|
1482
|
+
return blockedReconcile(null, documentIssues, currentProfilesSha256, !apply);
|
|
1483
|
+
}
|
|
1484
|
+
} catch {
|
|
1485
|
+
return blockedReconcile(
|
|
1486
|
+
null,
|
|
1487
|
+
["profile_reconcile_current_profiles_invalid"],
|
|
1488
|
+
null,
|
|
1489
|
+
!apply,
|
|
1490
|
+
);
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
const detection = detectProfilesV2({ workspaceRoot: root, projectToolResolver });
|
|
1494
|
+
const detectionIssues = verifyDetectionInputs(root, detection, {
|
|
1495
|
+
allowedApplyStatuses: ["profiles_already_exists"],
|
|
1496
|
+
});
|
|
1497
|
+
if (detection.currentProfilesSha256 !== currentProfilesSha256) {
|
|
1498
|
+
detectionIssues.push("profile_reconcile_detection_preimage_mismatch");
|
|
1499
|
+
}
|
|
1500
|
+
if (detectionIssues.length > 0) {
|
|
1501
|
+
return blockedReconcile(detection, detectionIssues, currentProfilesSha256, !apply);
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
const merged = mergeDetectedProfiles(currentProfiles, detection.proposedProfiles);
|
|
1505
|
+
const proposedIssues = validateProfileDocumentWithDetectedSurfaces(
|
|
1506
|
+
root,
|
|
1507
|
+
merged.proposedProfiles,
|
|
1508
|
+
);
|
|
1509
|
+
if (proposedIssues.length > 0) {
|
|
1510
|
+
return blockedReconcile(detection, proposedIssues, currentProfilesSha256, !apply);
|
|
1511
|
+
}
|
|
1512
|
+
const proposedProfilesBytes = `${JSON.stringify(merged.proposedProfiles, null, 2)}\n`;
|
|
1513
|
+
const proposedProfilesSha256 = sha256(proposedProfilesBytes);
|
|
1514
|
+
const planBody = {
|
|
1515
|
+
detectionSha256: detection.detectionSha256,
|
|
1516
|
+
currentProfilesSha256,
|
|
1517
|
+
proposedProfilesSha256,
|
|
1518
|
+
addedProfileIds: merged.addedProfileIds,
|
|
1519
|
+
updatedProfiles: merged.updatedProfiles,
|
|
1520
|
+
};
|
|
1521
|
+
const reconcileSha256 = sha256(canonicalJson(planBody));
|
|
1522
|
+
const plan = {
|
|
1523
|
+
schemaVersion: "OwlCodaRunKitProfilesReconcilePlanV1",
|
|
1524
|
+
status: proposedProfilesSha256 === currentProfilesSha256
|
|
1525
|
+
? "profiles_already_current"
|
|
1526
|
+
: "profiles_reconcile_ready",
|
|
1527
|
+
valid: true,
|
|
1528
|
+
exitCode: 0,
|
|
1529
|
+
dryRun: !apply,
|
|
1530
|
+
writesPerformed: 0,
|
|
1531
|
+
reconcileSha256,
|
|
1532
|
+
...planBody,
|
|
1533
|
+
proposedProfiles: merged.proposedProfiles,
|
|
1534
|
+
issueCodes: [],
|
|
1535
|
+
authorizationGranted: false,
|
|
1536
|
+
};
|
|
1537
|
+
if (!apply || proposedProfilesSha256 === currentProfilesSha256) return plan;
|
|
1538
|
+
|
|
1539
|
+
let runtimeRoot;
|
|
1540
|
+
let receiptRoot;
|
|
1541
|
+
try {
|
|
1542
|
+
runtimeRoot = ensureDirectoryChain(root, ".owlcoda/runkit");
|
|
1543
|
+
receiptRoot = ensureDirectoryChain(
|
|
1544
|
+
root,
|
|
1545
|
+
".owlcoda/runkit/profile-reconcile-receipts",
|
|
1546
|
+
);
|
|
1547
|
+
} catch {
|
|
1548
|
+
return blockedReconcile(
|
|
1549
|
+
detection,
|
|
1550
|
+
["profile_reconcile_directory_untrusted"],
|
|
1551
|
+
currentProfilesSha256,
|
|
1552
|
+
false,
|
|
1553
|
+
);
|
|
1554
|
+
}
|
|
1555
|
+
const profilesPath = path.join(runtimeRoot, "profiles.json");
|
|
1556
|
+
const receiptPath = path.join(
|
|
1557
|
+
".owlcoda",
|
|
1558
|
+
"runkit",
|
|
1559
|
+
"profile-reconcile-receipts",
|
|
1560
|
+
`${reconcileSha256}.json`,
|
|
1561
|
+
);
|
|
1562
|
+
const receiptAbsolutePath = path.join(receiptRoot, `${reconcileSha256}.json`);
|
|
1563
|
+
const receipt = {
|
|
1564
|
+
schemaVersion: "OwlCodaRunKitProfilesReconcileReceiptV1",
|
|
1565
|
+
status: "profiles_reconciled",
|
|
1566
|
+
exitCode: 0,
|
|
1567
|
+
writesPerformed: 2,
|
|
1568
|
+
dryRun: false,
|
|
1569
|
+
reconcileSha256,
|
|
1570
|
+
detectionSha256: detection.detectionSha256,
|
|
1571
|
+
beforeProfilesSha256: currentProfilesSha256,
|
|
1572
|
+
appliedProfilesSha256: proposedProfilesSha256,
|
|
1573
|
+
addedProfileIds: merged.addedProfileIds,
|
|
1574
|
+
updatedProfiles: merged.updatedProfiles,
|
|
1575
|
+
receiptPath,
|
|
1576
|
+
issueCodes: [],
|
|
1577
|
+
authorizationGranted: false,
|
|
1578
|
+
};
|
|
1579
|
+
const receiptBytes = `${JSON.stringify(receipt, null, 2)}\n`;
|
|
1580
|
+
const transactionRoot = path.join(
|
|
1581
|
+
runtimeRoot,
|
|
1582
|
+
`.profile-reconcile-${reconcileSha256}.lock`,
|
|
1583
|
+
);
|
|
1584
|
+
try {
|
|
1585
|
+
mkdirSync(transactionRoot);
|
|
1586
|
+
} catch (error) {
|
|
1587
|
+
return blockedReconcile(
|
|
1588
|
+
detection,
|
|
1589
|
+
[error?.code === "EEXIST"
|
|
1590
|
+
? "profile_reconcile_transaction_active"
|
|
1591
|
+
: "profile_reconcile_transaction_failed"],
|
|
1592
|
+
currentProfilesSha256,
|
|
1593
|
+
false,
|
|
1594
|
+
);
|
|
1595
|
+
}
|
|
1596
|
+
let profilesCommitted = false;
|
|
1597
|
+
let preserveTransaction = false;
|
|
1598
|
+
try {
|
|
1599
|
+
if (profilesFileHash(root) !== currentProfilesSha256) {
|
|
1600
|
+
throw new Error("profile_reconcile_preimage_changed");
|
|
1601
|
+
}
|
|
1602
|
+
if (existsSync(receiptAbsolutePath)) {
|
|
1603
|
+
throw new Error("profile_reconcile_receipt_already_exists");
|
|
1604
|
+
}
|
|
1605
|
+
const previousProfiles = path.join(transactionRoot, "previous-profiles.json");
|
|
1606
|
+
const stagedProfiles = path.join(transactionRoot, "profiles.json");
|
|
1607
|
+
const stagedReceipt = path.join(transactionRoot, "receipt.json");
|
|
1608
|
+
writeFileSync(previousProfiles, currentProfilesBytes, { flag: "wx" });
|
|
1609
|
+
writeFileSync(stagedProfiles, proposedProfilesBytes, { flag: "wx" });
|
|
1610
|
+
writeFileSync(stagedReceipt, receiptBytes, { flag: "wx" });
|
|
1611
|
+
if (!exactRegularFileBytes(stagedProfiles, proposedProfilesBytes)) {
|
|
1612
|
+
throw new Error("profile_reconcile_staged_profiles_invalid");
|
|
1613
|
+
}
|
|
1614
|
+
renameSync(stagedProfiles, profilesPath);
|
|
1615
|
+
profilesCommitted = true;
|
|
1616
|
+
onAfterProfilesCommit?.();
|
|
1617
|
+
commitCreateOnlyStagedFile(stagedReceipt, receiptAbsolutePath, receiptBytes);
|
|
1618
|
+
return receipt;
|
|
1619
|
+
} catch {
|
|
1620
|
+
let issueCode = "profile_reconcile_transaction_failed";
|
|
1621
|
+
if (
|
|
1622
|
+
profilesCommitted
|
|
1623
|
+
&& profilesFileHash(root) === proposedProfilesSha256
|
|
1624
|
+
) {
|
|
1625
|
+
const previousProfiles = path.join(transactionRoot, "previous-profiles.json");
|
|
1626
|
+
try {
|
|
1627
|
+
if (!exactRegularFileBytes(previousProfiles, currentProfilesBytes)) {
|
|
1628
|
+
throw new Error("profile_reconcile_previous_profiles_invalid");
|
|
1629
|
+
}
|
|
1630
|
+
renameSync(previousProfiles, profilesPath);
|
|
1631
|
+
} catch {
|
|
1632
|
+
preserveTransaction = true;
|
|
1633
|
+
issueCode = "profile_reconcile_transaction_rollback_failed";
|
|
1634
|
+
}
|
|
1635
|
+
} else if (profilesCommitted) {
|
|
1636
|
+
preserveTransaction = true;
|
|
1637
|
+
issueCode = "profile_reconcile_transaction_rollback_failed";
|
|
1638
|
+
}
|
|
1639
|
+
return blockedReconcile(
|
|
1640
|
+
detection,
|
|
1641
|
+
[issueCode],
|
|
1642
|
+
currentProfilesSha256,
|
|
1643
|
+
false,
|
|
1644
|
+
);
|
|
1645
|
+
} finally {
|
|
1646
|
+
if (!preserveTransaction) {
|
|
1647
|
+
rmSync(transactionRoot, { recursive: true, force: true });
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1108
1652
|
function exactKeys(value, allowed) {
|
|
1109
1653
|
return value
|
|
1110
1654
|
&& typeof value === "object"
|
|
@@ -1241,6 +1785,53 @@ function validateProfileDocument(config) {
|
|
|
1241
1785
|
return [...new Set(issues)].sort();
|
|
1242
1786
|
}
|
|
1243
1787
|
|
|
1788
|
+
function profileLauncherIssues(config) {
|
|
1789
|
+
const issues = [];
|
|
1790
|
+
for (const profile of config.profiles ?? []) {
|
|
1791
|
+
for (const command of profile.commands ?? []) {
|
|
1792
|
+
if (typeof command.executable !== "string" || !path.isAbsolute(command.executable)) {
|
|
1793
|
+
continue;
|
|
1794
|
+
}
|
|
1795
|
+
const identity = `${profile.id ?? "unknown"}:${command.id ?? "unknown"}`;
|
|
1796
|
+
try {
|
|
1797
|
+
const stat = lstatSync(command.executable);
|
|
1798
|
+
if (stat.isSymbolicLink()) {
|
|
1799
|
+
issues.push(`profile_launcher_symlink_rejected:${identity}`);
|
|
1800
|
+
} else if (
|
|
1801
|
+
!stat.isFile()
|
|
1802
|
+
|| realpathSync(command.executable) !== path.resolve(command.executable)
|
|
1803
|
+
) {
|
|
1804
|
+
issues.push(`profile_launcher_not_regular_file:${identity}`);
|
|
1805
|
+
}
|
|
1806
|
+
} catch {
|
|
1807
|
+
issues.push(`profile_launcher_unavailable:${identity}`);
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
return issues;
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
function validateProfileDocumentWithDetectedSurfaces(root, config) {
|
|
1815
|
+
const issues = validateProfileDocument(config);
|
|
1816
|
+
if (issues.length > 0) return issues;
|
|
1817
|
+
issues.push(...profileLauncherIssues(config));
|
|
1818
|
+
const detected = detectProfiles({ workspaceRoot: root });
|
|
1819
|
+
const detectedPaths = detected.candidates.flatMap((profile) =>
|
|
1820
|
+
profile.paths.map(representativePath));
|
|
1821
|
+
if (detectedPaths.length > 0) {
|
|
1822
|
+
const impact = resolveProfileImpactDetailed({
|
|
1823
|
+
changedPaths: detectedPaths,
|
|
1824
|
+
profiles: config.profiles,
|
|
1825
|
+
});
|
|
1826
|
+
issues.push(...impact.uncoveredPaths.map((value) =>
|
|
1827
|
+
`uncovered_detected_surface:${value}`));
|
|
1828
|
+
issues.push(...impact.warnings
|
|
1829
|
+
.filter((warning) => warning.startsWith("supporting_only_match:"))
|
|
1830
|
+
.map((warning) => `detected_surface_${warning}`));
|
|
1831
|
+
}
|
|
1832
|
+
return [...new Set(issues)].sort();
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1244
1835
|
export function validateProfiles({ workspaceRoot } = {}) {
|
|
1245
1836
|
const root = resolveBoundedWorkspaceRoot(workspaceRoot);
|
|
1246
1837
|
const issues = [];
|
|
@@ -1256,23 +1847,7 @@ export function validateProfiles({ workspaceRoot } = {}) {
|
|
|
1256
1847
|
.filter((id) => typeof id === "string")
|
|
1257
1848
|
.sort()
|
|
1258
1849
|
: [];
|
|
1259
|
-
issues.push(...
|
|
1260
|
-
if (issues.length === 0) {
|
|
1261
|
-
const detected = detectProfiles({ workspaceRoot: root });
|
|
1262
|
-
const detectedPaths = detected.candidates.flatMap((profile) =>
|
|
1263
|
-
profile.paths.map(representativePath));
|
|
1264
|
-
if (detectedPaths.length > 0) {
|
|
1265
|
-
const impact = resolveProfileImpactDetailed({
|
|
1266
|
-
changedPaths: detectedPaths,
|
|
1267
|
-
profiles: config.profiles,
|
|
1268
|
-
});
|
|
1269
|
-
issues.push(...impact.uncoveredPaths.map((value) =>
|
|
1270
|
-
`uncovered_detected_surface:${value}`));
|
|
1271
|
-
issues.push(...impact.warnings
|
|
1272
|
-
.filter((warning) => warning.startsWith("supporting_only_match:"))
|
|
1273
|
-
.map((warning) => `detected_surface_${warning}`));
|
|
1274
|
-
}
|
|
1275
|
-
}
|
|
1850
|
+
issues.push(...validateProfileDocumentWithDetectedSurfaces(root, config));
|
|
1276
1851
|
} catch (error) {
|
|
1277
1852
|
issues.push(error instanceof Error ? error.message : String(error));
|
|
1278
1853
|
}
|
|
@@ -18,6 +18,12 @@ const OFFICIAL_CLI_ENTRYPOINT =
|
|
|
18
18
|
const EXACT_VERSION = /^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?$/;
|
|
19
19
|
const SHA256_REF = /^sha256:[a-f0-9]{64}$/;
|
|
20
20
|
const TRUSTED_PRIOR_CORE_IDENTITIES = new Map([
|
|
21
|
+
["0.18.0", new Set([
|
|
22
|
+
"sha256:72c481dbafe06a9c56cd940d74a3fe109a82b73a3c43d0bb54923ea6c584c04e",
|
|
23
|
+
])],
|
|
24
|
+
["0.17.2", new Set([
|
|
25
|
+
"sha256:67b883b8a763253b873fb6047c7e7e01c81123aa5250a0db5feaabd13cc4d860",
|
|
26
|
+
])],
|
|
21
27
|
["0.17.1", new Set([
|
|
22
28
|
"sha256:5376f3736dd17c07598df8b655a6bbceb3b64b44f3e6630e69f966e420d82e26",
|
|
23
29
|
])],
|