@abelxiaoxing/cadence 1.1.0 → 1.2.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.
@@ -20,7 +20,6 @@ import { runtimeForWorkerRoute } from "./parent-provider.ts";
20
20
  import {
21
21
  loadRoutePolicy,
22
22
  type RoutePolicy,
23
- unavailableRoutePolicy,
24
23
  type WorkerRole,
25
24
  } from "./route-policy.ts";
26
25
  import { type BrokerActivityUpdate, RunWorkerBroker } from "./worker-broker.ts";
@@ -319,7 +318,7 @@ export class PacketRuntime {
319
318
  if (!agent) {
320
319
  throw new Error("packet agent definition is unavailable");
321
320
  }
322
- const broker = this.#brokerFor(context.cwd);
321
+ const broker = this.#brokerFor(context);
323
322
  const routed = await broker.run({
324
323
  runId: `${packet.stage}:${packet.id}`,
325
324
  operationId: packet.id,
@@ -405,8 +404,8 @@ export class PacketRuntime {
405
404
  return routed.value.child;
406
405
  }
407
406
 
408
- #brokerFor(cwd: string): RunWorkerBroker {
409
- const root = path.resolve(cwd);
407
+ #brokerFor(context: PacketContext): RunWorkerBroker {
408
+ const root = path.resolve(context.cwd);
410
409
  const existing = this.#brokers.get(root);
411
410
  let policy = this.#routePolicy;
412
411
  if (policy && existing) return existing;
@@ -414,12 +413,13 @@ export class PacketRuntime {
414
413
  const resolution = loadRoutePolicy({
415
414
  cwd: root,
416
415
  ...(this.#routePolicyHome ? { home: this.#routePolicyHome } : {}),
416
+ ...(context.model ? { parentModel: context.model } : {}),
417
417
  });
418
- if (!resolution.ok && resolution.source.kind !== "none") {
418
+ if (!resolution.ok) {
419
419
  const code = resolution.diagnostics[0]?.code ?? "policy-invalid";
420
420
  throw new Error(`route-policy-unavailable:${code}`);
421
421
  }
422
- policy = resolution.ok ? resolution.policy : unavailableRoutePolicy();
422
+ policy = resolution.policy;
423
423
  }
424
424
  const serializedPolicy = JSON.stringify(policy);
425
425
  if (existing && this.#brokerPolicies.get(root) === serializedPolicy) {
@@ -1,5 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
- import { existsSync, lstatSync, readFileSync } from "node:fs";
2
+ import { lstatSync, readFileSync } from "node:fs";
3
3
  import { homedir } from "node:os";
4
4
  import path from "node:path";
5
5
  import { ROLES } from "./contracts.ts";
@@ -21,6 +21,11 @@ export interface RouteCapabilities {
21
21
  maxTokens: number;
22
22
  }
23
23
 
24
+ export interface ParentModelCapabilities {
25
+ contextWindow: number;
26
+ maxTokens: number;
27
+ }
28
+
24
29
  interface RouteBase {
25
30
  id: string;
26
31
  kind: "inherited" | "custom";
@@ -50,10 +55,9 @@ export interface RoutePolicy {
50
55
 
51
56
  export type RoutePolicySource =
52
57
  | { kind: "project" | "user"; path: string }
53
- | { kind: "none" };
58
+ | { kind: "default" };
54
59
 
55
60
  export type RoutePolicyDiagnosticCode =
56
- | "policy-missing"
57
61
  | "policy-unreadable"
58
62
  | "policy-invalid-json"
59
63
  | "policy-invalid"
@@ -77,7 +81,7 @@ export type ParsedRoutePolicy =
77
81
  export type RoutePolicyResolution =
78
82
  | {
79
83
  ok: true;
80
- source: Extract<RoutePolicySource, { kind: "project" | "user" }>;
84
+ source: RoutePolicySource;
81
85
  policy: RoutePolicy;
82
86
  }
83
87
  | {
@@ -87,8 +91,8 @@ export type RoutePolicyResolution =
87
91
  };
88
92
 
89
93
  /**
90
- * Internal fail-closed policy used while the selected external policy is
91
- * missing or invalid. It deliberately contains no inherited-parent route:
94
+ * Internal fail-closed policy used while a selected explicit policy is
95
+ * invalid. It deliberately contains no inherited-parent route:
92
96
  * the broker therefore reports endpoint-unavailable without making a Worker
93
97
  * request, while the local durable control plane remains available.
94
98
  *
@@ -351,8 +355,16 @@ export function parseRoutePolicy(value: unknown): ParsedRoutePolicy {
351
355
  };
352
356
  }
353
357
 
354
- /** Parent-model route used by read-only Design and Diagnose packets. */
355
- export function parentRoutePolicy(): RoutePolicy {
358
+ /** Default parent-model route used when no explicit policy file exists. */
359
+ export function parentRoutePolicy(
360
+ parentModel?: ParentModelCapabilities,
361
+ ): RoutePolicy {
362
+ const contextWindow = positiveInteger(parentModel?.contextWindow)
363
+ ? parentModel.contextWindow
364
+ : 1;
365
+ const maxTokens = positiveInteger(parentModel?.maxTokens)
366
+ ? Math.min(parentModel.maxTokens, contextWindow)
367
+ : 1;
356
368
  const parsed = parseRoutePolicy({
357
369
  version: ROUTE_POLICY_VERSION,
358
370
  routes: {
@@ -361,8 +373,8 @@ export function parentRoutePolicy(): RoutePolicy {
361
373
  capabilities: {
362
374
  roles: [...ROLES],
363
375
  dialects: [...ROUTE_DIALECTS],
364
- contextWindow: 1_000_000,
365
- maxTokens: 500_000,
376
+ contextWindow,
377
+ maxTokens,
366
378
  },
367
379
  },
368
380
  },
@@ -372,25 +384,41 @@ export function parentRoutePolicy(): RoutePolicy {
372
384
  return parsed.policy;
373
385
  }
374
386
 
375
- function selectedSource(cwd: string, home: string): RoutePolicySource {
387
+ function selectedSource(
388
+ cwd: string,
389
+ home: string,
390
+ ): { kind: "project" | "user"; path: string } | { kind: "none" } {
391
+ const mayExist = (candidate: string): boolean => {
392
+ try {
393
+ return lstatSync(candidate, { throwIfNoEntry: false }) !== undefined;
394
+ } catch {
395
+ // Permission and path-observation failures are explicit unreadable
396
+ // policy candidates, not proof that the policy is absent.
397
+ return true;
398
+ }
399
+ };
376
400
  const projectPath = path.join(cwd, ".pi", "cadence", "routes.json");
377
- if (existsSync(projectPath)) return { kind: "project", path: projectPath };
401
+ if (mayExist(projectPath)) return { kind: "project", path: projectPath };
378
402
  const userPath = path.join(home, ".pi", "agent", "cadence", "routes.json");
379
- if (existsSync(userPath)) return { kind: "user", path: userPath };
403
+ if (mayExist(userPath)) return { kind: "user", path: userPath };
380
404
  return { kind: "none" };
381
405
  }
382
406
 
383
407
  export function loadRoutePolicy(
384
- options: { cwd?: string; home?: string } = {},
408
+ options: {
409
+ cwd?: string;
410
+ home?: string;
411
+ parentModel?: ParentModelCapabilities;
412
+ } = {},
385
413
  ): RoutePolicyResolution {
386
414
  const cwd = path.resolve(options.cwd ?? process.cwd());
387
415
  const home = path.resolve(options.home ?? homedir());
388
416
  const source = selectedSource(cwd, home);
389
417
  if (source.kind === "none") {
390
418
  return {
391
- ok: false,
392
- source,
393
- diagnostics: [{ code: "policy-missing" }],
419
+ ok: true,
420
+ source: { kind: "default" },
421
+ policy: parentRoutePolicy(options.parentModel),
394
422
  };
395
423
  }
396
424
  let bytes: string;
package/src/run-state.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
- import type { ControlStage } from "./control-contracts.ts";
2
+
3
+ export const RUN_STAGES = ["abel-design", "abel-implement"] as const;
4
+ export type RunStage = (typeof RUN_STAGES)[number];
3
5
 
4
6
  export const RUN_STATES = [
5
7
  "created",
@@ -47,12 +49,17 @@ export interface DeliveryBindingProjection {
47
49
  gate: DeliveryGate;
48
50
  revision: number;
49
51
  receiptHash: string;
52
+ approvalProof?: {
53
+ revision: number;
54
+ contractHash: string;
55
+ recordHash: string;
56
+ };
50
57
  }
51
58
 
52
59
  export interface RunProjection {
53
60
  runId: string;
54
61
  rootHash: string;
55
- stage: ControlStage;
62
+ stage: RunStage;
56
63
  change?: string;
57
64
  provisionalKey?: string;
58
65
  state: RunState;
@@ -133,6 +140,7 @@ const LEGAL_TRANSITIONS: Readonly<Record<RunState, readonly RunState[]>> = {
133
140
  "running",
134
141
  "approval-needed",
135
142
  "recovering",
143
+ "completed",
136
144
  "discarded",
137
145
  "rejected",
138
146
  ],
@@ -263,12 +271,31 @@ function applyVerifiedEvent(
263
271
  const gate = event.payload.gate;
264
272
  const revision = event.payload.revision;
265
273
  const receiptHash = event.payload.receiptHash;
274
+ const approvalProof = event.payload.approvalProof;
266
275
  if (
267
276
  (gate !== "gate-a" && gate !== "gate-b") ||
268
277
  !Number.isSafeInteger(revision) ||
269
278
  (revision as number) < 1 ||
270
279
  typeof receiptHash !== "string" ||
271
- !SHA256.test(receiptHash)
280
+ !SHA256.test(receiptHash) ||
281
+ (approvalProof !== undefined &&
282
+ (!approvalProof ||
283
+ typeof approvalProof !== "object" ||
284
+ Array.isArray(approvalProof) ||
285
+ !Number.isSafeInteger(
286
+ (approvalProof as Record<string, unknown>).revision,
287
+ ) ||
288
+ Number((approvalProof as Record<string, unknown>).revision) < 1 ||
289
+ typeof (approvalProof as Record<string, unknown>).contractHash !==
290
+ "string" ||
291
+ !SHA256.test(
292
+ String((approvalProof as Record<string, unknown>).contractHash),
293
+ ) ||
294
+ typeof (approvalProof as Record<string, unknown>).recordHash !==
295
+ "string" ||
296
+ !SHA256.test(
297
+ String((approvalProof as Record<string, unknown>).recordHash),
298
+ )))
272
299
  ) {
273
300
  throw new Error("event-integrity-delivery-binding");
274
301
  }
@@ -276,13 +303,25 @@ function applyVerifiedEvent(
276
303
  gate: gate as DeliveryGate,
277
304
  revision: revision as number,
278
305
  receiptHash,
306
+ ...(approvalProof
307
+ ? {
308
+ approvalProof: structuredClone(
309
+ approvalProof as DeliveryBindingProjection["approvalProof"],
310
+ ),
311
+ }
312
+ : {}),
279
313
  };
280
314
  const existing = projection.deliveryBindings.find(
281
315
  (candidate) =>
282
316
  candidate.gate === binding.gate &&
283
317
  candidate.revision === binding.revision,
284
318
  );
285
- if (existing && existing.receiptHash !== binding.receiptHash) {
319
+ if (
320
+ existing &&
321
+ (existing.receiptHash !== binding.receiptHash ||
322
+ canonicalJson(existing.approvalProof) !==
323
+ canonicalJson(binding.approvalProof))
324
+ ) {
286
325
  throw new Error("event-integrity-delivery-conflict");
287
326
  }
288
327
  const deliveryBindings = existing
package/src/run-store.ts CHANGED
@@ -1,12 +1,12 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { chmodSync, closeSync, lstatSync, openSync } from "node:fs";
3
3
  import { DatabaseSync } from "node:sqlite";
4
- import type { ControlStage } from "./control-contracts.ts";
5
4
  import {
6
5
  createRunEvent,
7
6
  type DeliveryGate,
8
7
  type RunEvent,
9
8
  type RunProjection,
9
+ type RunStage,
10
10
  type RunState,
11
11
  reduceRunEvents,
12
12
  ZERO_EVENT_HASH,
@@ -17,7 +17,7 @@ import {
17
17
  StateRootError,
18
18
  } from "./state-root.ts";
19
19
 
20
- export const RUN_STORE_SCHEMA_VERSION = 2 as const;
20
+ export const RUN_STORE_SCHEMA_VERSION = 4 as const;
21
21
 
22
22
  const CHANGE_NAME = /^[a-z0-9](?:[a-z0-9-]{0,126}[a-z0-9])?$/u;
23
23
  const IDENTIFIER = /^[a-z0-9](?:[a-z0-9._:-]{0,126}[a-z0-9])?$/iu;
@@ -37,6 +37,7 @@ interface EventRow {
37
37
  }
38
38
 
39
39
  interface OperationRow {
40
+ kind: string;
40
41
  state: string;
41
42
  outcome_json: string | null;
42
43
  lease_token: string | null;
@@ -49,7 +50,7 @@ export interface RunStoreOptions {
49
50
  }
50
51
 
51
52
  export interface StartRunInput {
52
- stage: ControlStage;
53
+ stage: RunStage;
53
54
  change?: string;
54
55
  provisionalKey?: string;
55
56
  operationId: string;
@@ -60,6 +61,11 @@ export interface BindDeliveryInput {
60
61
  gate: DeliveryGate;
61
62
  revision: number;
62
63
  receiptHash: string;
64
+ approvalProof?: {
65
+ revision: number;
66
+ contractHash: string;
67
+ recordHash: string;
68
+ };
63
69
  operationId: string;
64
70
  lease?: OperationLease;
65
71
  }
@@ -79,7 +85,7 @@ export interface OperationLeaseStatus {
79
85
 
80
86
  const SCHEMA = `
81
87
  CREATE TABLE IF NOT EXISTS schema_meta (
82
- version INTEGER PRIMARY KEY CHECK (version = 2)
88
+ version INTEGER PRIMARY KEY CHECK (version = 4)
83
89
  ) STRICT;
84
90
 
85
91
  CREATE TABLE IF NOT EXISTS runs (
@@ -103,6 +109,9 @@ const SCHEMA = `
103
109
  gate TEXT NOT NULL,
104
110
  revision INTEGER NOT NULL,
105
111
  receipt_hash TEXT NOT NULL,
112
+ approval_revision INTEGER,
113
+ contract_hash TEXT,
114
+ record_hash TEXT,
106
115
  operation_id TEXT NOT NULL,
107
116
  PRIMARY KEY (run_id, gate, revision),
108
117
  UNIQUE (run_id, operation_id)
@@ -330,7 +339,7 @@ export class RunStore {
330
339
  #operation(runId: string, operationId: string): OperationRow | undefined {
331
340
  return this.#database
332
341
  .prepare(
333
- `SELECT state, outcome_json, lease_token, lease_expires_at
342
+ `SELECT kind, state, outcome_json, lease_token, lease_expires_at
334
343
  FROM operations WHERE run_id = ? AND operation_id = ?`,
335
344
  )
336
345
  .get(runId, operationId) as OperationRow | undefined;
@@ -339,8 +348,10 @@ export class RunStore {
339
348
  #operationOutcome(
340
349
  runId: string,
341
350
  operationId: string,
351
+ kind: string,
342
352
  ): RunProjection | undefined {
343
353
  const row = this.#operation(runId, operationId);
354
+ if (row && row.kind !== kind) throw new Error("operation-id-conflict");
344
355
  if (row?.state !== "committed" || row.outcome_json === null)
345
356
  return undefined;
346
357
  return parseProjection(row.outcome_json);
@@ -433,7 +444,7 @@ export class RunStore {
433
444
  }
434
445
  const key = lookupKey(input);
435
446
  return this.#transaction(() => {
436
- const existing = this.#database
447
+ let existing = this.#database
437
448
  .prepare(
438
449
  `SELECT run_id, projection_json FROM runs
439
450
  WHERE root_hash = ? AND stage = ? AND lookup_key = ?`,
@@ -441,10 +452,23 @@ export class RunStore {
441
452
  .get(this.stateRoot.consumerRootHash, input.stage, key) as
442
453
  | RunRow
443
454
  | undefined;
455
+ if (existing) {
456
+ const projection = parseProjection(existing.projection_json);
457
+ if (
458
+ input.stage === "abel-design" &&
459
+ ["completed", "discarded", "rejected"].includes(projection.state)
460
+ ) {
461
+ this.#database
462
+ .prepare("UPDATE runs SET lookup_key = ? WHERE run_id = ?")
463
+ .run(`terminal:${existing.run_id}`, existing.run_id);
464
+ existing = undefined;
465
+ }
466
+ }
444
467
  if (existing) {
445
468
  const replay = this.#operationOutcome(
446
469
  existing.run_id,
447
470
  input.operationId,
471
+ "start",
448
472
  );
449
473
  if (replay) return replay;
450
474
  const projection = parseProjection(existing.projection_json);
@@ -519,7 +543,11 @@ export class RunStore {
519
543
  requireIdentifier(input.operationId, "operation-id");
520
544
  if (!CHANGE_NAME.test(input.change)) throw new Error("invalid-change");
521
545
  return this.#transaction(() => {
522
- const replay = this.#operationOutcome(input.runId, input.operationId);
546
+ const replay = this.#operationOutcome(
547
+ input.runId,
548
+ input.operationId,
549
+ "change-bound",
550
+ );
523
551
  if (replay) return replay;
524
552
  const current = this.status(input.runId);
525
553
  if (current.stage !== "abel-design" || !current.provisionalKey) {
@@ -561,7 +589,12 @@ export class RunStore {
561
589
  (input.gate !== "gate-a" && input.gate !== "gate-b") ||
562
590
  !Number.isSafeInteger(input.revision) ||
563
591
  input.revision < 1 ||
564
- !SHA256.test(input.receiptHash)
592
+ !SHA256.test(input.receiptHash) ||
593
+ (input.approvalProof !== undefined &&
594
+ (!Number.isSafeInteger(input.approvalProof.revision) ||
595
+ input.approvalProof.revision < 1 ||
596
+ !SHA256.test(input.approvalProof.contractHash) ||
597
+ !SHA256.test(input.approvalProof.recordHash)))
565
598
  ) {
566
599
  throw new Error("invalid-delivery-binding");
567
600
  }
@@ -572,18 +605,35 @@ export class RunStore {
572
605
  if (input.lease.runId !== input.runId) throw new Error("lease-fenced");
573
606
  this.#assertLease(input.lease);
574
607
  }
575
- const replay = this.#operationOutcome(input.runId, input.operationId);
608
+ const replay = this.#operationOutcome(
609
+ input.runId,
610
+ input.operationId,
611
+ "delivery-bound",
612
+ );
576
613
  if (replay) return replay;
577
614
  const existing = this.#database
578
615
  .prepare(
579
- `SELECT receipt_hash FROM delivery_bindings
616
+ `SELECT receipt_hash, approval_revision, contract_hash, record_hash
617
+ FROM delivery_bindings
580
618
  WHERE run_id = ? AND gate = ? AND revision = ?`,
581
619
  )
582
620
  .get(input.runId, input.gate, input.revision) as
583
- | { receipt_hash: string }
621
+ | {
622
+ receipt_hash: string;
623
+ approval_revision: number | null;
624
+ contract_hash: string | null;
625
+ record_hash: string | null;
626
+ }
584
627
  | undefined;
585
628
  if (existing) {
586
- if (existing.receipt_hash !== input.receiptHash) {
629
+ if (
630
+ existing.receipt_hash !== input.receiptHash ||
631
+ existing.approval_revision !==
632
+ (input.approvalProof?.revision ?? null) ||
633
+ existing.contract_hash !==
634
+ (input.approvalProof?.contractHash ?? null) ||
635
+ existing.record_hash !== (input.approvalProof?.recordHash ?? null)
636
+ ) {
587
637
  throw new Error("delivery-revision-conflict");
588
638
  }
589
639
  const projection = this.status(input.runId);
@@ -599,20 +649,27 @@ export class RunStore {
599
649
  this.#database
600
650
  .prepare(
601
651
  `INSERT INTO delivery_bindings(
602
- run_id, gate, revision, receipt_hash, operation_id
603
- ) VALUES (?, ?, ?, ?, ?)`,
652
+ run_id, gate, revision, receipt_hash, approval_revision,
653
+ contract_hash, record_hash, operation_id
654
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
604
655
  )
605
656
  .run(
606
657
  input.runId,
607
658
  input.gate,
608
659
  input.revision,
609
660
  input.receiptHash,
661
+ input.approvalProof?.revision ?? null,
662
+ input.approvalProof?.contractHash ?? null,
663
+ input.approvalProof?.recordHash ?? null,
610
664
  input.operationId,
611
665
  );
612
666
  const projection = this.#appendEvent(input.runId, "delivery-bound", {
613
667
  gate: input.gate,
614
668
  revision: input.revision,
615
669
  receiptHash: input.receiptHash,
670
+ ...(input.approvalProof
671
+ ? { approvalProof: structuredClone(input.approvalProof) }
672
+ : {}),
616
673
  });
617
674
  this.#recordOperation(
618
675
  input.runId,
@@ -643,6 +700,33 @@ export class RunStore {
643
700
  });
644
701
  }
645
702
 
703
+ bindDeliveriesAtomically(
704
+ inputs: readonly BindDeliveryInput[],
705
+ write: (database: DatabaseSync) => void,
706
+ ): RunProjection {
707
+ if (inputs.length === 0) throw new Error("delivery-binding-empty");
708
+ for (const input of inputs) this.#requireDeliveryBinding(input);
709
+ const runId = inputs[0]?.runId;
710
+ if (
711
+ !runId ||
712
+ inputs.some(
713
+ (input) =>
714
+ input.runId !== runId ||
715
+ (input.lease?.token ?? null) !== (inputs[0]?.lease?.token ?? null),
716
+ ) ||
717
+ typeof write !== "function"
718
+ ) {
719
+ throw new Error("delivery-binding-write-invalid");
720
+ }
721
+ return this.#transaction(() => {
722
+ let projection: RunProjection | undefined;
723
+ for (const input of inputs) projection = this.#bindDelivery(input);
724
+ write(this.#database);
725
+ if (!projection) throw new Error("delivery-binding-empty");
726
+ return projection;
727
+ });
728
+ }
729
+
646
730
  acquireLease(input: {
647
731
  runId: string;
648
732
  operationId: string;
@@ -839,7 +923,11 @@ export class RunStore {
839
923
  if (input.lease.runId !== input.runId) throw new Error("lease-fenced");
840
924
  this.#assertLease(input.lease);
841
925
  }
842
- const replay = this.#operationOutcome(input.runId, input.operationId);
926
+ const replay = this.#operationOutcome(
927
+ input.runId,
928
+ input.operationId,
929
+ "state-transitioned",
930
+ );
843
931
  if (replay) return replay;
844
932
  const current = this.status(input.runId);
845
933
  const projection = this.#appendEvent(input.runId, "state-transitioned", {
@@ -1320,7 +1320,7 @@ export function renderActivityCall(
1320
1320
  context?: ActivityRenderContext,
1321
1321
  ): Component {
1322
1322
  const value = asRecord(args);
1323
- if (typeof value?.command === "string" && value.version === 2) {
1323
+ if (typeof value?.command === "string") {
1324
1324
  if (context?.executionStarted || context?.isError) return new Text("");
1325
1325
  const projected = projectWorkflowActivity(value, { state: "queued" }, 0);
1326
1326
  const display: ActivitySnapshot = {