@abelxiaoxing/cadence 1.0.12 → 1.0.13

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.
@@ -0,0 +1,66 @@
1
+ import { lstatSync } from "node:fs";
2
+ import path from "node:path";
3
+
4
+ import { isValidRelativePath } from "./contracts.ts";
5
+
6
+ export type SafePathObservation =
7
+ | { kind: "file" }
8
+ | { kind: "directory" }
9
+ | { kind: "absent" }
10
+ | {
11
+ kind: "unsafe";
12
+ reason:
13
+ | "invalid-path"
14
+ | "root-unavailable"
15
+ | "path-unavailable"
16
+ | "symlink"
17
+ | "parent-not-directory"
18
+ | "special-file";
19
+ };
20
+
21
+ export function observeSafePath(
22
+ root: string,
23
+ relative: string,
24
+ ): SafePathObservation {
25
+ if (!isValidRelativePath(relative)) {
26
+ return { kind: "unsafe", reason: "invalid-path" };
27
+ }
28
+ try {
29
+ const resolvedRoot = path.resolve(root);
30
+ const rootStat = lstatSync(resolvedRoot, { throwIfNoEntry: false });
31
+ if (!rootStat?.isDirectory() || rootStat.isSymbolicLink()) {
32
+ return { kind: "unsafe", reason: "root-unavailable" };
33
+ }
34
+ if (relative === ".") return { kind: "directory" };
35
+
36
+ let current = resolvedRoot;
37
+ const parts = relative.split("/");
38
+ for (const [index, part] of parts.entries()) {
39
+ current = path.join(current, part);
40
+ if (!current.startsWith(`${resolvedRoot}${path.sep}`)) {
41
+ return { kind: "unsafe", reason: "invalid-path" };
42
+ }
43
+ const stat = lstatSync(current, { throwIfNoEntry: false });
44
+ if (!stat) return { kind: "absent" };
45
+ if (stat.isSymbolicLink()) {
46
+ return { kind: "unsafe", reason: "symlink" };
47
+ }
48
+ if (index < parts.length - 1) {
49
+ if (!stat.isDirectory()) {
50
+ return { kind: "unsafe", reason: "parent-not-directory" };
51
+ }
52
+ continue;
53
+ }
54
+ if (stat.isFile()) return { kind: "file" };
55
+ if (stat.isDirectory()) return { kind: "directory" };
56
+ return { kind: "unsafe", reason: "special-file" };
57
+ }
58
+ } catch {
59
+ return { kind: "unsafe", reason: "path-unavailable" };
60
+ }
61
+ return { kind: "unsafe", reason: "invalid-path" };
62
+ }
63
+
64
+ export function isSafeRegularFile(root: string, relative: string): boolean {
65
+ return observeSafePath(root, relative).kind === "file";
66
+ }
@@ -4,6 +4,8 @@ import {
4
4
  type CandidateFailure,
5
5
  type DiffResult,
6
6
  type EvidenceResult,
7
+ type SubmitFinalCategory,
8
+ type SubmitSchemaState,
7
9
  validateDiffResult,
8
10
  validateEvidenceResult,
9
11
  } from "./contracts.ts";
@@ -74,12 +76,7 @@ const diffSchema = Type.Object({
74
76
  contractCompliant: Type.Literal(true),
75
77
  });
76
78
 
77
- export type FinalCategory =
78
- | "no-final-assistant"
79
- | "text-only"
80
- | "mixed"
81
- | "multiple-submit"
82
- | "single-submit-only";
79
+ export type FinalCategory = SubmitFinalCategory;
83
80
 
84
81
  export interface IdentityOutcome {
85
82
  request: boolean;
@@ -91,7 +88,7 @@ export interface IdentityOutcome {
91
88
  export interface SubmitClassification {
92
89
  finalCategory: FinalCategory;
93
90
  attempts: number;
94
- schema: "valid" | "invalid";
91
+ schema: SubmitSchemaState;
95
92
  identity: IdentityOutcome;
96
93
  }
97
94
 
@@ -105,7 +102,7 @@ export function createSubmitTool(input: {
105
102
  let submitted: EvidenceResult | DiffResult | undefined;
106
103
  let failure: CandidateFailure | undefined;
107
104
  let attempts = 0;
108
- let schema: "valid" | "invalid" = "invalid";
105
+ let schema: SubmitSchemaState = "not-submitted";
109
106
  const identity: IdentityOutcome = {
110
107
  request: true,
111
108
  role: true,
@@ -134,7 +131,11 @@ export function createSubmitTool(input: {
134
131
  identity.task = false;
135
132
  identity.phase = false;
136
133
  schema = "invalid";
137
- failure = { kind: "artifact", code: "invalid-structural-result" };
134
+ failure = {
135
+ kind: "artifact",
136
+ code: "invalid-structural-result",
137
+ stage: "structural-submit",
138
+ };
138
139
  throw new Error("submitted result is not an object");
139
140
  }
140
141
  if (value.id !== input.requestId) identity.request = false;
@@ -158,15 +159,27 @@ export function createSubmitTool(input: {
158
159
  const matches =
159
160
  identity.request && identity.role && identity.task && identity.phase;
160
161
  if (!validation.ok || !matches) {
161
- failure =
162
- validation.failure ??
163
- ({ kind: "artifact", code: "invalid-structural-result" } as const);
162
+ failure = validation.ok
163
+ ? {
164
+ kind: "artifact",
165
+ code: "structural-identity-mismatch",
166
+ stage: "structural-submit",
167
+ }
168
+ : (validation.failure ?? {
169
+ kind: "artifact",
170
+ code: "invalid-structural-result",
171
+ stage: "structural-submit",
172
+ });
164
173
  throw new Error(
165
174
  validation.reason ?? "submitted result identity does not match",
166
175
  );
167
176
  }
168
177
  if (submitted) {
169
- failure = { kind: "artifact", code: "invalid-structural-result" };
178
+ failure = {
179
+ kind: "artifact",
180
+ code: "invalid-structural-result",
181
+ stage: "structural-submit",
182
+ };
170
183
  throw new Error("duplicate structural submission");
171
184
  }
172
185
  submitted = structuredClone(value) as unknown as
@@ -2,12 +2,13 @@ import { lstatSync, readFileSync, realpathSync } from "node:fs";
2
2
  import path from "node:path";
3
3
  import {
4
4
  type AtomicVerificationContract,
5
- isValidRelativePath,
5
+ type StructuredVerificationContract,
6
6
  type VerificationAdapterCode,
7
7
  validateVerificationContract,
8
8
  verificationInputPaths,
9
9
  verificationSteps,
10
10
  } from "./contracts.ts";
11
+ import { isSafeRegularFile, observeSafePath } from "./safe-path.ts";
11
12
 
12
13
  export type VerificationCapabilityDiagnostic =
13
14
  | {
@@ -37,11 +38,6 @@ export interface VerificationRunnerBinding {
37
38
  mountSource?: string;
38
39
  }
39
40
 
40
- export interface VerificationReadiness {
41
- taskContractsExecutable: boolean;
42
- diagnostics: VerificationCapabilityDiagnostic[];
43
- }
44
-
45
41
  const SHELL_OPERATOR = /[;&|`$<>\n\r\0]/u;
46
42
  const SIMPLE_TOKEN =
47
43
  /^(?:[a-z0-9][a-z0-9._:@/=-]*|--?[a-z0-9][a-z0-9._:@/=-]*)$/iu;
@@ -88,18 +84,9 @@ function fileStatus(
88
84
  root: string,
89
85
  relative: string,
90
86
  ): "regular" | "absent" | "unsafe" {
91
- if (!isValidRelativePath(relative)) return "unsafe";
92
- const resolvedRoot = path.resolve(root);
93
- const absolute = path.resolve(resolvedRoot, relative);
94
- if (
95
- absolute === resolvedRoot ||
96
- !absolute.startsWith(`${resolvedRoot}${path.sep}`)
97
- ) {
98
- return "unsafe";
99
- }
100
- const stat = lstatSync(absolute, { throwIfNoEntry: false });
101
- if (!stat) return "absent";
102
- return stat.isFile() && !stat.isSymbolicLink() ? "regular" : "unsafe";
87
+ const observation = observeSafePath(root, relative);
88
+ if (observation.kind === "file") return "regular";
89
+ return observation.kind === "absent" ? "absent" : "unsafe";
103
90
  }
104
91
 
105
92
  function regularFile(root: string, relative: string): boolean {
@@ -287,7 +274,7 @@ function validatePackageScript(
287
274
  verificationId: string,
288
275
  packageManager: string,
289
276
  script: string,
290
- approvedCommand?: string,
277
+ approvedCommand: string,
291
278
  dependencyOwner = root,
292
279
  ): VerificationCapabilityResult {
293
280
  const packageRunner = resolveVerificationRunner(packageManager);
@@ -311,7 +298,7 @@ function validatePackageScript(
311
298
  `package.json script ${script} is missing`,
312
299
  );
313
300
  }
314
- if (approvedCommand !== undefined && actual !== approvedCommand) {
301
+ if (actual !== approvedCommand) {
315
302
  return adapterFailure(
316
303
  verificationId,
317
304
  "script-command-mismatch",
@@ -336,21 +323,7 @@ function validateAtomicCapability(
336
323
  root: string,
337
324
  step: AtomicVerificationContract,
338
325
  dependencyOwner: string,
339
- allowedMissingInputs: ReadonlySet<string>,
340
326
  ): VerificationCapabilityResult {
341
- for (const input of verificationInputPaths(step)) {
342
- const status = fileStatus(root, input);
343
- if (
344
- status !== "regular" &&
345
- !(status === "absent" && allowedMissingInputs.has(input))
346
- ) {
347
- return adapterFailure(
348
- step.id,
349
- "input-missing",
350
- `verification input ${input} is missing or unsafe`,
351
- );
352
- }
353
- }
354
327
  if (step.kind === "package-script") {
355
328
  return validatePackageScript(
356
329
  root,
@@ -419,8 +392,24 @@ export function validateVerificationCapability(
419
392
  value: unknown,
420
393
  options: {
421
394
  dependencyOwner?: string;
422
- allowedMissingInputs?: readonly string[];
423
395
  } = {},
396
+ ): VerificationCapabilityResult {
397
+ return validateCapability(root, value, options, true);
398
+ }
399
+
400
+ export function validateVerificationAdapterCapability(
401
+ root: string,
402
+ value: unknown,
403
+ options: { dependencyOwner?: string } = {},
404
+ ): VerificationCapabilityResult {
405
+ return validateCapability(root, value, options, false);
406
+ }
407
+
408
+ function validateCapability(
409
+ root: string,
410
+ value: unknown,
411
+ options: { dependencyOwner?: string },
412
+ requireInputAvailability: boolean,
424
413
  ): VerificationCapabilityResult {
425
414
  const validation = validateVerificationContract(value);
426
415
  if (!validation.ok) {
@@ -442,40 +431,36 @@ export function validateVerificationCapability(
442
431
  };
443
432
  }
444
433
  const dependencyOwner = options.dependencyOwner ?? root;
445
- const allowedMissingInputs = new Set(options.allowedMissingInputs ?? []);
434
+ return validateAcceptedVerificationCapability(
435
+ root,
436
+ validation.value,
437
+ dependencyOwner,
438
+ requireInputAvailability,
439
+ );
440
+ }
441
+
442
+ function validateAcceptedVerificationCapability(
443
+ root: string,
444
+ verification: StructuredVerificationContract,
445
+ dependencyOwner: string,
446
+ requireInputAvailability: boolean,
447
+ ): VerificationCapabilityResult {
448
+ if (requireInputAvailability) {
449
+ for (const input of verificationInputPaths(verification)) {
450
+ if (!isSafeRegularFile(root, input)) {
451
+ return adapterFailure(
452
+ verification.id,
453
+ "input-missing",
454
+ `verification input ${input} is missing or unsafe`,
455
+ );
456
+ }
457
+ }
458
+ }
446
459
  const runnerBindings: VerificationRunnerBinding[] = [];
447
- for (const step of verificationSteps(validation.value)) {
448
- const capability = validateAtomicCapability(
449
- root,
450
- step,
451
- dependencyOwner,
452
- allowedMissingInputs,
453
- );
460
+ for (const step of verificationSteps(verification)) {
461
+ const capability = validateAtomicCapability(root, step, dependencyOwner);
454
462
  if (!capability.ok) return capability;
455
463
  runnerBindings.push(...capability.runnerBindings);
456
464
  }
457
- return capabilitySuccess(validation.value.id, runnerBindings);
458
- }
459
-
460
- export function assessVerificationReadiness(
461
- root: string,
462
- contracts: readonly unknown[],
463
- options: {
464
- dependencyOwner?: string;
465
- allowedMissingInputs?: readonly string[];
466
- } = {},
467
- ): VerificationReadiness {
468
- const diagnostics = contracts
469
- .map((contract) => validateVerificationCapability(root, contract, options))
470
- .filter(
471
- (
472
- result,
473
- ): result is Extract<VerificationCapabilityResult, { ok: false }> =>
474
- !result.ok,
475
- )
476
- .map((result) => result.diagnostic);
477
- return {
478
- taskContractsExecutable: diagnostics.length === 0,
479
- diagnostics,
480
- };
465
+ return capabilitySuccess(verification.id, runnerBindings);
481
466
  }
package/src/worker.ts CHANGED
@@ -3,6 +3,7 @@
3
3
  import type {
4
4
  CandidateFailure,
5
5
  ImplementationPhase,
6
+ ImplementGraphBoundary,
6
7
  PhaseAttempt,
7
8
  TaskBoundary,
8
9
  TaskFailure,
@@ -42,6 +43,22 @@ export interface TaskRecord {
42
43
  state: TaskState;
43
44
  }
44
45
 
46
+ export interface ImplementGraphRecord {
47
+ key: string;
48
+ workspaceRoot: string;
49
+ graphHash: string;
50
+ boundary: ImplementGraphBoundary;
51
+ completedTasks: Set<string>;
52
+ blockedTasks: Set<string>;
53
+ }
54
+
55
+ export function graphRecordKey(
56
+ workspaceRoot: string,
57
+ changeId: string,
58
+ ): string {
59
+ return `${workspaceRoot}\0${changeId}`;
60
+ }
61
+
45
62
  export function taskRecordKey(
46
63
  workspaceRoot: string,
47
64
  changeId: string,
@@ -97,6 +114,37 @@ export function workerIdentity(model: {
97
114
 
98
115
  export class WorkerRegistry {
99
116
  private readonly tasks = new Map<string, TaskRecord>();
117
+ private readonly graphs = new Map<string, ImplementGraphRecord>();
118
+
119
+ admitGraph(
120
+ boundary: ImplementGraphBoundary,
121
+ graphHash: string,
122
+ workspaceRoot: string,
123
+ state: {
124
+ completedTasks: readonly string[];
125
+ blockedTasks: readonly string[];
126
+ },
127
+ ): ImplementGraphRecord {
128
+ const key = graphRecordKey(workspaceRoot, boundary.changeId);
129
+ if (this.graphs.has(key)) throw new Error("duplicate graph admission");
130
+ const record: ImplementGraphRecord = {
131
+ key,
132
+ workspaceRoot,
133
+ graphHash,
134
+ boundary: structuredClone(boundary),
135
+ completedTasks: new Set(state.completedTasks),
136
+ blockedTasks: new Set(state.blockedTasks),
137
+ };
138
+ this.graphs.set(key, record);
139
+ return record;
140
+ }
141
+
142
+ getGraph(
143
+ workspaceRoot: string,
144
+ changeId: string,
145
+ ): ImplementGraphRecord | undefined {
146
+ return this.graphs.get(graphRecordKey(workspaceRoot, changeId));
147
+ }
100
148
 
101
149
  has(key: string): boolean {
102
150
  return this.tasks.has(key);
@@ -151,5 +199,6 @@ export class WorkerRegistry {
151
199
 
152
200
  clear(): void {
153
201
  this.tasks.clear();
202
+ this.graphs.clear();
154
203
  }
155
204
  }