@abelxiaoxing/cadence 1.0.10 → 1.0.12

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.
@@ -15,11 +15,15 @@ import {
15
15
  } from "node:fs";
16
16
  import path from "node:path";
17
17
  import {
18
+ type AtomicVerificationContract,
18
19
  type CandidateFailure,
19
20
  diffWritePaths,
20
21
  isValidRelativePath,
22
+ type StructuredVerificationContract,
21
23
  type VerificationContract,
22
24
  validateVerificationContract,
25
+ verificationInputPaths,
26
+ verificationSteps,
23
27
  } from "./contracts.ts";
24
28
  import {
25
29
  type Bound,
@@ -28,6 +32,10 @@ import {
28
32
  isCurrent,
29
33
  snapshotDirManifest,
30
34
  } from "./file-snapshot.ts";
35
+ import {
36
+ type VerificationRunnerBinding,
37
+ validateVerificationCapability,
38
+ } from "./verification-capability.ts";
31
39
 
32
40
  export type { VerificationContract } from "./contracts.ts";
33
41
 
@@ -377,6 +385,7 @@ function validateInput(input: CandidatePreflightInput): {
377
385
  targets: string[];
378
386
  lockPath: string;
379
387
  dependencyPath: string;
388
+ verification: StructuredVerificationContract;
380
389
  } {
381
390
  if (!Buffer.isBuffer(input.diff) || input.diff.length === 0)
382
391
  reject("artifact", "invalid-diff-bytes");
@@ -489,12 +498,16 @@ function validateInput(input: CandidatePreflightInput): {
489
498
  }
490
499
  if (targets.some((target) => !entries.has(target)))
491
500
  reject("artifact", "target-baseline-missing");
492
- for (let index = 3; index < input.verification.argv.length; index++) {
493
- const testPath = input.verification.argv[index];
494
- if (testPath === undefined || !entries.has(testPath))
501
+ for (const verificationPath of verificationInputPaths(verification.value)) {
502
+ if (!entries.has(verificationPath))
495
503
  reject("artifact", "verification-baseline-missing");
496
504
  }
497
- return { targets, lockPath, dependencyPath };
505
+ return {
506
+ targets,
507
+ lockPath,
508
+ dependencyPath,
509
+ verification: verification.value,
510
+ };
498
511
  }
499
512
 
500
513
  function verifyBaselineAt(root: string, baseline: BaselineEntry[]): void {
@@ -655,11 +668,18 @@ function sandboxArgs(
655
668
  checkout: string,
656
669
  dependencySource: string,
657
670
  dependencyIsDirectory: boolean,
658
- bunExecutable: string,
671
+ bunExecutable: string | undefined,
672
+ runnerPlan: SandboxRunnerPlan,
659
673
  invocation: string[],
660
674
  reportSource?: string,
661
675
  ): string[] {
662
676
  const candidate = "/candidate";
677
+ const sandboxPath = [
678
+ ...runnerPlan.pathEntries,
679
+ "/usr/bin",
680
+ "/usr/local/bin",
681
+ "/bin",
682
+ ].join(":");
663
683
  const args = [
664
684
  "--unshare-net",
665
685
  "--unshare-pid",
@@ -674,7 +694,7 @@ function sandboxArgs(
674
694
  "--clearenv",
675
695
  "--setenv",
676
696
  "PATH",
677
- "/usr/bin:/usr/local/bin:/bin",
697
+ sandboxPath,
678
698
  "--setenv",
679
699
  "HOME",
680
700
  "/nonexistent",
@@ -685,7 +705,15 @@ function sandboxArgs(
685
705
  for (const systemPath of ["/usr", "/bin", "/lib", "/lib64", "/etc"]) {
686
706
  if (existsSync(systemPath)) args.push("--ro-bind", systemPath, systemPath);
687
707
  }
688
- args.push("--ro-bind", bunExecutable, SANDBOX_BUN_TARGET);
708
+ if (bunExecutable) {
709
+ args.push("--ro-bind", bunExecutable, SANDBOX_BUN_TARGET);
710
+ }
711
+ if (runnerPlan.mounts.length > 0) {
712
+ args.push("--dir", "/cadence", "--dir", "/cadence/runners");
713
+ for (const mount of runnerPlan.mounts) {
714
+ args.push("--dir", mount.target, "--ro-bind", mount.source, mount.target);
715
+ }
716
+ }
689
717
  const dependencyDestination = path.posix.join(candidate, "node_modules");
690
718
  const localDependency = path.join(checkout, "node_modules");
691
719
  rmSync(localDependency, { recursive: true, force: true });
@@ -712,6 +740,59 @@ function sandboxArgs(
712
740
  return args;
713
741
  }
714
742
 
743
+ interface SandboxRunnerPlan {
744
+ mounts: Array<{ source: string; target: string }>;
745
+ pathEntries: string[];
746
+ }
747
+
748
+ function pathWithin(root: string, candidate: string): boolean {
749
+ return candidate === root || candidate.startsWith(`${root}${path.sep}`);
750
+ }
751
+
752
+ function sandboxRunnerPlan(
753
+ bindings: readonly VerificationRunnerBinding[],
754
+ ): SandboxRunnerPlan {
755
+ const relevant = bindings.filter((binding) => binding.command !== "bun");
756
+ const sources = [
757
+ ...new Set(
758
+ relevant.flatMap((binding) =>
759
+ binding.mountSource ? [binding.mountSource] : [],
760
+ ),
761
+ ),
762
+ ]
763
+ .sort(
764
+ (left, right) => left.length - right.length || left.localeCompare(right),
765
+ )
766
+ .filter(
767
+ (source, index, all) =>
768
+ !all.some(
769
+ (candidate, candidateIndex) =>
770
+ candidateIndex !== index &&
771
+ candidate.length < source.length &&
772
+ pathWithin(candidate, source),
773
+ ),
774
+ );
775
+ const mounts = sources.map((source, index) => ({
776
+ source,
777
+ target: `/cadence/runners/${index}`,
778
+ }));
779
+ const pathEntries = relevant.map((binding) => {
780
+ if (!binding.mountSource) return path.dirname(binding.executablePath);
781
+ const mount = mounts.find((candidate) =>
782
+ pathWithin(candidate.source, binding.executablePath),
783
+ );
784
+ if (!mount) throw new Error("verification runner mount plan is incomplete");
785
+ const relative = path.relative(
786
+ mount.source,
787
+ path.dirname(binding.executablePath),
788
+ );
789
+ return relative
790
+ ? path.posix.join(mount.target, ...relative.split(path.sep))
791
+ : mount.target;
792
+ });
793
+ return { mounts, pathEntries: [...new Set(pathEntries)] };
794
+ }
795
+
715
796
  interface StructuredTestReport {
716
797
  total: number;
717
798
  failed: number;
@@ -1037,44 +1118,44 @@ function rejectUnapprovedDependencies(
1037
1118
  }
1038
1119
 
1039
1120
  function normalizeTarget(
1040
- input: CandidatePreflightInput,
1121
+ verification: AtomicVerificationContract,
1041
1122
  result: CommandResult,
1042
1123
  secrets: string[],
1043
1124
  report?: StructuredTestReport,
1044
1125
  ): CandidatePreflightResult {
1045
- const staticCheck = isStaticCheck(input.verification);
1126
+ const vitest = verification.kind === "vitest";
1046
1127
  const output = `${result.stdout}\n${result.stderr}`;
1047
- const count = staticCheck ? 0 : (report?.total ?? 0);
1128
+ const count = vitest ? (report?.total ?? 0) : 0;
1048
1129
  const identityMatch =
1049
- input.verification.classification !== "expected-red" ||
1050
- (staticCheck
1051
- ? output.includes(input.verification.expectedFailure ?? "")
1052
- : (report?.failedAssertions.some((failure) =>
1053
- failure.includes(input.verification.expectedFailure ?? ""),
1054
- ) ?? false));
1130
+ verification.classification !== "expected-red" ||
1131
+ (vitest
1132
+ ? (report?.failedAssertions.some((failure) =>
1133
+ failure.includes(verification.expectedFailure ?? ""),
1134
+ ) ?? false)
1135
+ : output.includes(verification.expectedFailure ?? ""));
1055
1136
  const loadable =
1056
- staticCheck ||
1137
+ !vitest ||
1057
1138
  ((report?.fileErrors.length ?? 0) === 0 && !LOAD_FAILURE.test(output));
1058
- const enoughTests = staticCheck || count >= input.verification.minTests;
1139
+ const enoughTests = !vitest || count >= verification.minTests;
1059
1140
  const accepted =
1060
- input.verification.classification === "expected-red"
1141
+ verification.classification === "expected-red"
1061
1142
  ? result.status !== 0 &&
1062
- (staticCheck || (report?.failed ?? 0) > 0) &&
1143
+ (!vitest || (report?.failed ?? 0) > 0) &&
1063
1144
  enoughTests &&
1064
1145
  identityMatch &&
1065
1146
  loadable
1066
1147
  : result.status === 0 &&
1067
- (staticCheck || report?.success === true) &&
1148
+ (!vitest || report?.success === true) &&
1068
1149
  enoughTests &&
1069
1150
  loadable;
1070
1151
  const details = {
1071
- commandId: input.verification.id,
1152
+ commandId: verification.id,
1072
1153
  exitCode: result.status ?? 1,
1073
1154
  testCount: count,
1074
1155
  identityMatch,
1075
1156
  };
1076
1157
  if (
1077
- input.verification.classification === "expected-red" &&
1158
+ verification.classification === "expected-red" &&
1078
1159
  result.status === 0 &&
1079
1160
  enoughTests &&
1080
1161
  loadable
@@ -1092,20 +1173,74 @@ function normalizeTarget(
1092
1173
  }
1093
1174
  return {
1094
1175
  ok: true,
1095
- classification: input.verification.classification,
1176
+ classification: verification.classification,
1096
1177
  targets: [],
1097
1178
  ...details,
1098
1179
  checkoutRemoved: true,
1099
1180
  };
1100
1181
  }
1101
1182
 
1102
- function isStaticCheck(verification: VerificationContract): boolean {
1103
- return (
1104
- verification.argv.length === 3 &&
1105
- verification.argv[0] === "bun" &&
1106
- verification.argv[1] === "run" &&
1107
- verification.argv[2] === "check"
1108
- );
1183
+ function packageScriptInvocation(
1184
+ packageManager: string,
1185
+ script: string,
1186
+ args: string[],
1187
+ ): string[] {
1188
+ switch (packageManager) {
1189
+ case "npm":
1190
+ case "pnpm":
1191
+ return [packageManager, "run", script, "--", ...args];
1192
+ case "yarn":
1193
+ return [packageManager, "run", script, ...args];
1194
+ default:
1195
+ return ["bun", "run", script, ...args];
1196
+ }
1197
+ }
1198
+
1199
+ function executableInvocation(
1200
+ runner: Exclude<
1201
+ AtomicVerificationContract,
1202
+ { kind: "package-script" }
1203
+ >["runner"],
1204
+ args: string[],
1205
+ ): string[] {
1206
+ switch (runner.kind) {
1207
+ case "package-script":
1208
+ return packageScriptInvocation(
1209
+ runner.packageManager,
1210
+ runner.script,
1211
+ args,
1212
+ );
1213
+ case "local-binary":
1214
+ return [
1215
+ path.posix.join("/candidate/node_modules/.bin", runner.executable),
1216
+ ...args,
1217
+ ];
1218
+ case "npx":
1219
+ return ["npx", "--no-install", runner.executable, ...args];
1220
+ case "node":
1221
+ return ["node", runner.script, ...args];
1222
+ }
1223
+ }
1224
+
1225
+ function verificationInvocation(
1226
+ verification: AtomicVerificationContract,
1227
+ report: boolean,
1228
+ ): string[] {
1229
+ const reporter = report
1230
+ ? ["--reporter=json", `--outputFile=${SANDBOX_REPORT_TARGET}`]
1231
+ : [];
1232
+ if (verification.kind === "package-script") {
1233
+ return packageScriptInvocation(
1234
+ verification.packageManager,
1235
+ verification.script,
1236
+ verification.args,
1237
+ );
1238
+ }
1239
+ const args =
1240
+ verification.kind === "vitest"
1241
+ ? [...verification.testFiles, ...verification.args, ...reporter]
1242
+ : verification.args;
1243
+ return executableInvocation(verification.runner, args);
1109
1244
  }
1110
1245
 
1111
1246
  function resolveDependencySource(
@@ -1297,29 +1432,53 @@ export async function preflightCandidate(
1297
1432
  await requireRegularGitModes(dependencies, checkout, input.signal);
1298
1433
  scanCheckout(checkout);
1299
1434
 
1300
- for (const testPath of input.verification.argv.slice(3)) {
1301
- const stat = lstatSync(safePath(checkout, testPath), {
1435
+ for (const verificationPath of verificationInputPaths(
1436
+ validated.verification,
1437
+ )) {
1438
+ const stat = lstatSync(safePath(checkout, verificationPath), {
1302
1439
  throwIfNoEntry: false,
1303
1440
  });
1304
1441
  if (!stat?.isFile() || stat.isSymbolicLink())
1305
1442
  reject("artifact", "verification-input-unavailable");
1306
1443
  }
1307
1444
 
1445
+ const capability = validateVerificationCapability(
1446
+ checkout,
1447
+ validated.verification,
1448
+ { dependencyOwner: root },
1449
+ );
1450
+ if (!capability.ok) {
1451
+ if (capability.diagnostic.kind === "verification-adapter") {
1452
+ reject("verification-adapter", capability.diagnostic.code);
1453
+ }
1454
+ reject("artifact", "invalid-verification-contract");
1455
+ }
1456
+
1308
1457
  let bwrap: string;
1309
- let bunExecutable: string;
1310
1458
  try {
1311
1459
  bwrap = dependencies.realpath(dependencies.bwrapPath);
1312
- bunExecutable = resolveBunExecutable(dependencies);
1313
1460
  } catch {
1314
1461
  reject("environment", "bubblewrap-or-dependency-unavailable");
1315
1462
  }
1463
+ const runnerPlan = sandboxRunnerPlan(capability.runnerBindings);
1464
+ const sandboxSecrets = [
1465
+ root,
1466
+ checkout,
1467
+ ...runnerPlan.mounts.map((mount) => mount.source),
1468
+ ];
1469
+ let bunExecutable =
1470
+ capability.runnerBindings.some((binding) => binding.command === "bun") ||
1471
+ dependencyManifestChanged
1472
+ ? resolveBunExecutable(dependencies)
1473
+ : undefined;
1316
1474
 
1317
1475
  const probeArgs = sandboxArgs(
1318
1476
  checkout,
1319
1477
  dependencySource,
1320
1478
  input.dependencyTarget.kind === "dir",
1321
1479
  bunExecutable,
1322
- ["bun", "--version"],
1480
+ runnerPlan,
1481
+ ["/bin/true"],
1323
1482
  );
1324
1483
  result = await run(
1325
1484
  dependencies,
@@ -1333,15 +1492,17 @@ export async function preflightCandidate(
1333
1492
  if (result.error || result.status !== 0)
1334
1493
  reject("environment", "sandbox-runtime-unavailable", {
1335
1494
  exitCode: result.status ?? 1,
1336
- excerpt: commandExcerpt(result, [root, checkout]),
1495
+ excerpt: commandExcerpt(result, sandboxSecrets),
1337
1496
  });
1338
1497
 
1339
1498
  if (dependencyManifestChanged) {
1499
+ bunExecutable ??= resolveBunExecutable(dependencies);
1340
1500
  const dependencyArgs = sandboxArgs(
1341
1501
  checkout,
1342
1502
  dependencySource,
1343
1503
  input.dependencyTarget.kind === "dir",
1344
1504
  bunExecutable,
1505
+ runnerPlan,
1345
1506
  [
1346
1507
  "bun",
1347
1508
  "install",
@@ -1362,81 +1523,67 @@ export async function preflightCandidate(
1362
1523
  if (input.signal?.aborted) reject("cancelled", "cancelled");
1363
1524
  if (result.error || result.status === null)
1364
1525
  reject("environment", "bubblewrap-launch-failed", {
1365
- excerpt: commandExcerpt(result, [root, checkout]),
1526
+ excerpt: commandExcerpt(result, sandboxSecrets),
1366
1527
  });
1367
1528
  if (result.status !== 0) {
1368
1529
  reject("approval-boundary", "unapproved-dependency-change");
1369
1530
  }
1370
1531
  }
1371
1532
 
1372
- if (!isStaticCheck(input.verification)) {
1373
- const checkArgs = sandboxArgs(
1533
+ const steps = verificationSteps(validated.verification);
1534
+ for (const [index, step] of steps.entries()) {
1535
+ const structuredReportPath =
1536
+ step.kind === "vitest"
1537
+ ? path.join(temp, `verification-report-${index}.json`)
1538
+ : undefined;
1539
+ if (structuredReportPath) {
1540
+ writeFileSync(structuredReportPath, "", { mode: 0o600 });
1541
+ }
1542
+ const targetArgs = sandboxArgs(
1374
1543
  checkout,
1375
1544
  dependencySource,
1376
1545
  input.dependencyTarget.kind === "dir",
1377
1546
  bunExecutable,
1378
- ["bun", "run", "check"],
1547
+ runnerPlan,
1548
+ verificationInvocation(step, structuredReportPath !== undefined),
1549
+ structuredReportPath,
1379
1550
  );
1380
1551
  result = await run(
1381
1552
  dependencies,
1382
1553
  checkout,
1383
1554
  bwrap,
1384
- checkArgs,
1555
+ targetArgs,
1385
1556
  undefined,
1386
1557
  input.signal,
1387
1558
  );
1388
1559
  if (input.signal?.aborted) reject("cancelled", "cancelled");
1389
- if (result.error || result.status === null)
1560
+ requireCurrentHost(input);
1561
+ if (result.error || result.status === null) {
1390
1562
  reject("environment", "bubblewrap-launch-failed", {
1391
- excerpt: commandExcerpt(result, [root, checkout]),
1392
- });
1393
- if (result.status !== 0)
1394
- reject("artifact", "candidate-check-failed", {
1395
- commandId: input.verification.id,
1396
- exitCode: result.status,
1397
- excerpt: commandExcerpt(result, [root, checkout]),
1563
+ excerpt: commandExcerpt(result, sandboxSecrets),
1398
1564
  });
1565
+ }
1566
+ const report = structuredReportPath
1567
+ ? structuredTestReport(structuredReportPath)
1568
+ : undefined;
1569
+ const normalized = normalizeTarget(step, result, sandboxSecrets, report);
1570
+ if (!normalized.ok) {
1571
+ outcome =
1572
+ index < steps.length - 1
1573
+ ? failed("artifact", "candidate-check-failed", {
1574
+ commandId: step.id,
1575
+ exitCode: normalized.exitCode,
1576
+ testCount: normalized.testCount,
1577
+ identityMatch: normalized.identityMatch,
1578
+ excerpt: normalized.excerpt,
1579
+ })
1580
+ : normalized;
1581
+ break;
1582
+ }
1583
+ if (index === steps.length - 1) {
1584
+ outcome = { ...normalized, targets };
1585
+ }
1399
1586
  }
1400
-
1401
- const structuredReportPath = isStaticCheck(input.verification)
1402
- ? undefined
1403
- : path.join(temp, "verification-report.json");
1404
- if (structuredReportPath)
1405
- writeFileSync(structuredReportPath, "", { mode: 0o600 });
1406
- const targetInvocation = structuredReportPath
1407
- ? [
1408
- ...input.verification.argv,
1409
- "--reporter=json",
1410
- `--outputFile=${SANDBOX_REPORT_TARGET}`,
1411
- ]
1412
- : input.verification.argv;
1413
- const targetArgs = sandboxArgs(
1414
- checkout,
1415
- dependencySource,
1416
- input.dependencyTarget.kind === "dir",
1417
- bunExecutable,
1418
- targetInvocation,
1419
- structuredReportPath,
1420
- );
1421
- result = await run(
1422
- dependencies,
1423
- checkout,
1424
- bwrap,
1425
- targetArgs,
1426
- undefined,
1427
- input.signal,
1428
- );
1429
- if (input.signal?.aborted) reject("cancelled", "cancelled");
1430
- requireCurrentHost(input);
1431
- if (result.error || result.status === null)
1432
- reject("environment", "bubblewrap-launch-failed", {
1433
- excerpt: commandExcerpt(result, [root, checkout]),
1434
- });
1435
- const report = structuredReportPath
1436
- ? structuredTestReport(structuredReportPath)
1437
- : undefined;
1438
- const normalized = normalizeTarget(input, result, [root, checkout], report);
1439
- outcome = normalized.ok ? { ...normalized, targets } : normalized;
1440
1587
  } catch (error) {
1441
1588
  if (input.signal?.aborted) outcome = failed("cancelled", "cancelled");
1442
1589
  else if (error instanceof PreflightFailure)
@@ -10,7 +10,7 @@ import {
10
10
  import { Type } from "typebox";
11
11
  import type { ChildFailure, DiffResult, EvidenceResult } from "./contracts.ts";
12
12
  import { EmptyResourceLoader } from "./empty-resource-loader.ts";
13
- import { createScopedTools } from "./scoped-tools.ts";
13
+ import { createScopedTools, TOOL_LIMITS } from "./scoped-tools.ts";
14
14
  import {
15
15
  createSubmitTool,
16
16
  type FinalCategory,
@@ -120,8 +120,30 @@ function wrapScopedTools(
120
120
  description: scoped.description,
121
121
  parameters:
122
122
  scoped.name === "grep"
123
- ? Type.Object({ path: Type.String(), pattern: Type.String() })
124
- : Type.Object({ path: Type.String() }),
123
+ ? Type.Object({
124
+ path: Type.Optional(Type.String()),
125
+ pattern: Type.String(),
126
+ })
127
+ : scoped.name === "find"
128
+ ? Type.Object({
129
+ path: Type.Optional(Type.String()),
130
+ pattern: Type.String(),
131
+ limit: Type.Optional(
132
+ Type.Integer({
133
+ minimum: 1,
134
+ maximum: TOOL_LIMITS.maxEntries,
135
+ }),
136
+ ),
137
+ })
138
+ : scoped.name === "read"
139
+ ? Type.Object({
140
+ path: Type.String(),
141
+ offset: Type.Optional(Type.Integer({ minimum: 1 })),
142
+ limit: Type.Optional(Type.Integer({ minimum: 1 })),
143
+ })
144
+ : Type.Object({
145
+ path: Type.Optional(Type.String()),
146
+ }),
125
147
  async execute(_id, params) {
126
148
  const result = await scoped.execute(
127
149
  params as Record<string, unknown>,
@@ -185,6 +207,7 @@ function safeChildError(failure: ChildFailure): string {
185
207
  return "child phase cancelled";
186
208
  case "stale":
187
209
  case "approval-boundary":
210
+ case "verification-adapter":
188
211
  case "result-limit":
189
212
  return "child result rejected";
190
213
  }