@kontourai/flow-agents 3.6.0 → 3.8.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.
Files changed (96) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/build/src/builder-flow-run-adapter.d.ts +22 -2
  3. package/build/src/builder-flow-run-adapter.js +93 -28
  4. package/build/src/builder-flow-runtime.d.ts +8 -3
  5. package/build/src/builder-flow-runtime.js +407 -51
  6. package/build/src/cli/assignment-provider.d.ts +4 -7
  7. package/build/src/cli/assignment-provider.js +80 -14
  8. package/build/src/cli/builder-run.js +14 -18
  9. package/build/src/cli/workflow-sidecar.d.ts +28 -4
  10. package/build/src/cli/workflow-sidecar.js +825 -103
  11. package/build/src/cli/workflow.js +301 -43
  12. package/build/src/flow-kit/validate.d.ts +17 -0
  13. package/build/src/flow-kit/validate.js +340 -2
  14. package/build/src/index.js +5 -5
  15. package/build/src/lib/observed-command.d.ts +7 -0
  16. package/build/src/lib/observed-command.js +100 -0
  17. package/build/src/tools/build-universal-bundles.js +53 -3
  18. package/build/src/tools/generate-context-map.js +5 -3
  19. package/context/scripts/hooks/lib/kit-catalog.js +1 -1
  20. package/context/scripts/hooks/stop-goal-fit.js +78 -9
  21. package/docs/context-map.md +22 -20
  22. package/docs/developer-architecture.md +1 -1
  23. package/docs/getting-started.md +9 -1
  24. package/docs/public-workflow-cli.md +76 -7
  25. package/docs/skills-map.md +51 -27
  26. package/docs/spec/builder-flow-runtime.md +75 -40
  27. package/docs/workflow-usage-guide.md +109 -42
  28. package/evals/fixtures/hook-influence/cases.json +2 -2
  29. package/evals/integration/test_builder_entry_enforcement.sh +52 -41
  30. package/evals/integration/test_builder_step_producers.sh +330 -6
  31. package/evals/integration/test_bundle_install.sh +318 -65
  32. package/evals/integration/test_bundle_lifecycle.sh +4 -6
  33. package/evals/integration/test_critique_supersession_roundtrip.sh +21 -8
  34. package/evals/integration/test_current_json_per_actor.sh +12 -0
  35. package/evals/integration/test_dual_emit_flow_step.sh +62 -43
  36. package/evals/integration/test_flowdef_session_activation.sh +145 -25
  37. package/evals/integration/test_flowdef_session_history_preservation.sh +23 -21
  38. package/evals/integration/test_gate_lockdown.sh +3 -5
  39. package/evals/integration/test_goal_fit_hook.sh +60 -2
  40. package/evals/integration/test_install_merge.sh +18 -8
  41. package/evals/integration/test_liveness_verdict.sh +14 -17
  42. package/evals/integration/test_phase_map_and_gate_claim.sh +63 -38
  43. package/evals/integration/test_public_workflow_cli.sh +334 -14
  44. package/evals/integration/test_pull_work_liveness_preflight.sh +22 -66
  45. package/evals/integration/test_sidecar_field_preservation.sh +36 -11
  46. package/evals/integration/test_workflow_sidecar_writer.sh +277 -44
  47. package/evals/integration/test_workflow_steering_hook.sh +15 -38
  48. package/evals/run.sh +2 -0
  49. package/evals/static/test_builder_skill_coherence.sh +247 -0
  50. package/evals/static/test_library_exports.sh +5 -2
  51. package/evals/static/test_universal_bundles.sh +44 -1
  52. package/evals/static/test_workflow_skills.sh +13 -325
  53. package/kits/builder/flows/build.flow.json +22 -0
  54. package/kits/builder/flows/shape.flow.json +9 -9
  55. package/kits/builder/kit.json +70 -16
  56. package/kits/builder/skills/builder-shape/SKILL.md +75 -75
  57. package/kits/builder/skills/continue-work/SKILL.md +45 -106
  58. package/kits/builder/skills/deliver/SKILL.md +96 -442
  59. package/kits/builder/skills/design-probe/SKILL.md +40 -139
  60. package/kits/builder/skills/evidence-gate/SKILL.md +59 -201
  61. package/kits/builder/skills/execute-plan/SKILL.md +54 -125
  62. package/kits/builder/skills/fix-bug/SKILL.md +42 -132
  63. package/kits/builder/skills/gate-review/SKILL.md +60 -211
  64. package/kits/builder/skills/idea-to-backlog/SKILL.md +63 -244
  65. package/kits/builder/skills/learning-review/SKILL.md +63 -170
  66. package/kits/builder/skills/pickup-probe/SKILL.md +54 -111
  67. package/kits/builder/skills/plan-work/SKILL.md +51 -185
  68. package/kits/builder/skills/pull-work/SKILL.md +136 -485
  69. package/kits/builder/skills/release-readiness/SKILL.md +66 -107
  70. package/kits/builder/skills/review-work/SKILL.md +89 -176
  71. package/kits/builder/skills/tdd-workflow/SKILL.md +53 -147
  72. package/kits/builder/skills/verify-work/SKILL.md +101 -113
  73. package/package.json +2 -2
  74. package/packaging/README.md +1 -1
  75. package/scripts/README.md +1 -1
  76. package/scripts/hooks/lib/kit-catalog.js +1 -1
  77. package/scripts/hooks/stop-goal-fit.js +78 -9
  78. package/scripts/install-codex-home.sh +63 -23
  79. package/scripts/install-owned-files.js +18 -2
  80. package/src/builder-flow-run-adapter.ts +118 -32
  81. package/src/builder-flow-runtime.ts +426 -64
  82. package/src/cli/assignment-provider-lock.test.mjs +83 -0
  83. package/src/cli/assignment-provider.ts +75 -14
  84. package/src/cli/builder-flow-run-adapter.test.mjs +4 -2
  85. package/src/cli/builder-flow-runtime.test.mjs +436 -18
  86. package/src/cli/builder-run.ts +3 -9
  87. package/src/cli/kit-metadata-security.test.mjs +232 -6
  88. package/src/cli/public-api.test.mjs +15 -0
  89. package/src/cli/workflow-sidecar-execution-proof.test.mjs +90 -0
  90. package/src/cli/workflow-sidecar.ts +746 -103
  91. package/src/cli/workflow.ts +288 -41
  92. package/src/flow-kit/validate.ts +320 -2
  93. package/src/index.ts +6 -5
  94. package/src/lib/observed-command.ts +96 -0
  95. package/src/tools/build-universal-bundles.ts +51 -3
  96. package/src/tools/generate-context-map.ts +5 -3
@@ -1,5 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
- import { existsSync, mkdirSync, writeFileSync } from "node:fs";
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
3
  import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { isDeepStrictEqual } from "node:util";
@@ -12,7 +12,6 @@ import {
12
12
  normalizeTrustBundle,
13
13
  openGates,
14
14
  pauseRun,
15
- readJson,
16
15
  resumeRun,
17
16
  startRun,
18
17
  validateDefinition,
@@ -26,6 +25,9 @@ import { resolveEffectiveFlowDefinition } from "./lib/flow-resolver.js";
26
25
 
27
26
  export const BUILDER_BUILD_FLOW_ID = "builder.build";
28
27
  export const BUILDER_BUILD_FLOW_RELATIVE_PATH = "kits/builder/flows/build.flow.json";
28
+ export const BUILDER_SHAPE_FLOW_ID = "builder.shape";
29
+ export const BUILDER_SHAPE_FLOW_RELATIVE_PATH = "kits/builder/flows/shape.flow.json";
30
+ export type BuilderFlowId = typeof BUILDER_BUILD_FLOW_ID | typeof BUILDER_SHAPE_FLOW_ID;
29
31
 
30
32
  export interface BuilderBuildTrustBundleEvidenceInput {
31
33
  gate: string;
@@ -34,6 +36,8 @@ export interface BuilderBuildTrustBundleEvidenceInput {
34
36
  * Callers must not pass raw user-controlled paths to this local runtime API.
35
37
  */
36
38
  file: string;
39
+ /** SHA-256 of the immutable snapshot validated by Flow Agents before Flow attaches it. */
40
+ expectedSha256?: string;
37
41
  status?: "passed" | "failed";
38
42
  producer?: string;
39
43
  authorityTrace?: string;
@@ -56,6 +60,10 @@ export interface StartBuilderBuildRunInput {
56
60
  runId?: string;
57
61
  }
58
62
 
63
+ export interface StartBuilderFlowRunInput extends StartBuilderBuildRunInput {
64
+ flowId: BuilderFlowId;
65
+ }
66
+
59
67
  export interface EvaluateBuilderBuildRunInput {
60
68
  runId: string;
61
69
  /**
@@ -76,8 +84,8 @@ export interface ChangeBuilderBuildRunLifecycleInput extends LoadBuilderBuildRun
76
84
  at?: string;
77
85
  }
78
86
 
79
- export interface BuilderBuildRunResult {
80
- definitionId: typeof BUILDER_BUILD_FLOW_ID;
87
+ export interface BuilderFlowRunResult {
88
+ definitionId: BuilderFlowId;
81
89
  definitionVersion: string;
82
90
  runId: string;
83
91
  dir: string;
@@ -88,6 +96,10 @@ export interface BuilderBuildRunResult {
88
96
  freshnessTransitions: JsonObject[];
89
97
  }
90
98
 
99
+ export interface BuilderBuildRunResult extends Omit<BuilderFlowRunResult, "definitionId"> {
100
+ definitionId: typeof BUILDER_BUILD_FLOW_ID;
101
+ }
102
+
91
103
  export type BuilderBuildRunIdentityMismatch = "definition-id" | "definition-version" | "definition-content";
92
104
 
93
105
  export class BuilderBuildRunInputError extends Error {
@@ -128,20 +140,29 @@ export class BuilderBuildRunIdentityError extends Error {
128
140
  }
129
141
 
130
142
  export function resolveBuilderBuildFlowDefinitionPath(startDir = moduleDirectory()): string {
131
- const root = findPackageRoot(startDir);
132
- return path.join(root, BUILDER_BUILD_FLOW_RELATIVE_PATH);
143
+ return resolveBuilderFlowDefinitionPath(BUILDER_BUILD_FLOW_ID, startDir);
133
144
  }
134
145
 
135
146
  export async function startBuilderBuildRun(input: StartBuilderBuildRunInput): Promise<BuilderBuildRunResult> {
147
+ const result = await startBuilderFlowRun({ ...input, flowId: BUILDER_BUILD_FLOW_ID });
148
+ return asBuilderBuildResult(result, input.runId ?? result.runId);
149
+ }
150
+
151
+ export function resolveBuilderFlowDefinitionPath(flowId: BuilderFlowId, startDir = moduleDirectory()): string {
152
+ const root = findPackageRoot(startDir);
153
+ return path.join(root, flowRelativePath(flowId));
154
+ }
155
+
156
+ export async function startBuilderFlowRun(input: StartBuilderFlowRunInput): Promise<BuilderFlowRunResult> {
136
157
  assertRuntimeInput(input, ["evidence", "now", "gate"]);
137
158
  if (!isNonEmptyString(input.subject)) {
138
159
  throw new BuilderBuildRunInputError("subject", "must be a non-empty string");
139
160
  }
140
161
 
141
162
  const cwd = input.cwd ?? process.cwd();
142
- const definitionPath = resolveBuilderBuildFlowDefinitionPath();
143
- const definition = await loadShippedBuilderBuildDefinition(definitionPath);
144
- const runtimeDefinitionPath = materializeRuntimeDefinition(cwd, definition);
163
+ const definitionPath = resolveBuilderFlowDefinitionPath(input.flowId);
164
+ const definition = await loadShippedBuilderFlowDefinition(input.flowId, definitionPath);
165
+ const runtimeDefinitionPath = materializeRuntimeDefinition(cwd, input.flowId, definition);
145
166
  const started = await startRun(runtimeDefinitionPath, {
146
167
  cwd,
147
168
  runId: input.runId,
@@ -150,12 +171,17 @@ export async function startBuilderBuildRun(input: StartBuilderBuildRunInput): Pr
150
171
  subject: input.subject,
151
172
  },
152
173
  });
153
- const run = await loadCanonicalBuilderBuildRun(started.runId, cwd, definition);
174
+ const run = await loadCanonicalBuilderFlowRun(started.runId, cwd, definition);
154
175
 
155
176
  return resultFromRun(run, started.runId);
156
177
  }
157
178
 
158
179
  export async function evaluateBuilderBuildRun(input: EvaluateBuilderBuildRunInput): Promise<BuilderBuildRunResult> {
180
+ const result = await evaluateBuilderFlowRun(input);
181
+ return asBuilderBuildResult(result, input.runId);
182
+ }
183
+
184
+ export async function evaluateBuilderFlowRun(input: EvaluateBuilderBuildRunInput): Promise<BuilderFlowRunResult> {
159
185
  assertRuntimeInput(input, ["now", "gate"]);
160
186
  if (Array.isArray(input.evidence)) {
161
187
  throw new BuilderBuildRunInputError("evidence", "must be zero or one evidence object, not an array");
@@ -163,16 +189,26 @@ export async function evaluateBuilderBuildRun(input: EvaluateBuilderBuildRunInpu
163
189
 
164
190
  const cwd = input.cwd ?? process.cwd();
165
191
  const run = await loadRun(input.runId, cwd);
166
- const definition = await loadShippedBuilderBuildDefinition(resolveBuilderBuildFlowDefinitionPath());
192
+ const definition = await loadShippedBuilderFlowDefinitionForRun(input.runId, run.definition);
167
193
  assertCanonicalDefinition(input.runId, definition, run.definition);
168
194
 
169
195
  let attachedEvidence: FlowEvidenceEntry[] = [];
170
196
  if (input.evidence !== undefined) {
171
197
  const evidence = validateEvidenceInput(input.evidence);
172
198
  assertCurrentOpenGate(run.definition, run.state, evidence.gate);
173
- const normalized = normalizeTrustBundle(await readJson(path.resolve(cwd, evidence.file)));
199
+ const source = path.resolve(cwd, evidence.file);
200
+ const bytes = readFileSync(source);
201
+ const validatedSha256 = createHash("sha256").update(bytes).digest("hex");
202
+ if (evidence.expectedSha256 && evidence.expectedSha256 !== validatedSha256) {
203
+ throw new BuilderBuildRunInputError("evidence.expectedSha256", "does not match the bytes presented for validation");
204
+ }
205
+ const normalized = normalizeTrustBundle(JSON.parse(bytes.toString("utf8")));
174
206
  assertBundleSubjects(normalized.bundle, run.state.subject, openGates(run.definition, run.state)[0]);
175
- attachedEvidence = [await attachEvidence(input.runId, trustBundleAttachOptions(cwd, evidence))];
207
+ const attached = await attachEvidence(input.runId, trustBundleAttachOptions(cwd, evidence, validatedSha256));
208
+ if (attached.sha256 !== validatedSha256) {
209
+ throw new BuilderBuildRunInputError("evidence.file", "changed after validation before Flow attachment");
210
+ }
211
+ attachedEvidence = [attached];
176
212
  }
177
213
 
178
214
  const evaluated = await evaluateRun(input.runId, { cwd });
@@ -190,45 +226,65 @@ export async function evaluateBuilderBuildRun(input: EvaluateBuilderBuildRunInpu
190
226
  }
191
227
 
192
228
  export async function loadBuilderBuildRun(input: LoadBuilderBuildRunInput): Promise<BuilderBuildRunResult> {
229
+ const result = await loadBuilderFlowRun(input);
230
+ return asBuilderBuildResult(result, input.runId);
231
+ }
232
+
233
+ export async function loadBuilderFlowRun(input: LoadBuilderBuildRunInput): Promise<BuilderFlowRunResult> {
193
234
  assertRuntimeInput(input, ["evidence", "now", "gate"]);
194
235
  const cwd = input.cwd ?? process.cwd();
195
236
  const run = await loadRun(input.runId, cwd);
196
- const definition = await loadShippedBuilderBuildDefinition(resolveBuilderBuildFlowDefinitionPath());
237
+ const definition = await loadShippedBuilderFlowDefinitionForRun(input.runId, run.definition);
197
238
  assertCanonicalDefinition(input.runId, definition, run.definition);
198
239
  return resultFromRun(run, input.runId);
199
240
  }
200
241
 
201
242
  export async function pauseBuilderBuildRun(input: ChangeBuilderBuildRunLifecycleInput): Promise<BuilderBuildRunResult> {
202
- return changeBuilderBuildRunLifecycle(input, pauseRun);
243
+ const result = await pauseBuilderFlowRun(input);
244
+ return asBuilderBuildResult(result, input.runId);
203
245
  }
204
246
 
205
247
  export async function resumeBuilderBuildRun(input: ChangeBuilderBuildRunLifecycleInput): Promise<BuilderBuildRunResult> {
206
- return changeBuilderBuildRunLifecycle(input, resumeRun);
248
+ const result = await resumeBuilderFlowRun(input);
249
+ return asBuilderBuildResult(result, input.runId);
207
250
  }
208
251
 
209
252
  export async function cancelBuilderBuildRun(input: ChangeBuilderBuildRunLifecycleInput): Promise<BuilderBuildRunResult & { idempotent: boolean }> {
210
- const changed = await changeBuilderBuildRunLifecycleResult(input, cancelRun);
253
+ const changed = await changeBuilderFlowRunLifecycleResult(input, cancelRun);
254
+ return { ...asBuilderBuildResult(resultFromRun(changed, input.runId), input.runId), idempotent: changed.idempotent };
255
+ }
256
+
257
+ export async function pauseBuilderFlowRun(input: ChangeBuilderBuildRunLifecycleInput): Promise<BuilderFlowRunResult> {
258
+ return changeBuilderFlowRunLifecycle(input, pauseRun);
259
+ }
260
+
261
+ export async function resumeBuilderFlowRun(input: ChangeBuilderBuildRunLifecycleInput): Promise<BuilderFlowRunResult> {
262
+ return changeBuilderFlowRunLifecycle(input, resumeRun);
263
+ }
264
+
265
+ export async function cancelBuilderFlowRun(input: ChangeBuilderBuildRunLifecycleInput): Promise<BuilderFlowRunResult & { idempotent: boolean }> {
266
+ const changed = await changeBuilderFlowRunLifecycleResult(input, cancelRun);
211
267
  return { ...resultFromRun(changed, input.runId), idempotent: changed.idempotent };
212
268
  }
213
269
 
214
- async function changeBuilderBuildRunLifecycle(
270
+ async function changeBuilderFlowRunLifecycle(
215
271
  input: ChangeBuilderBuildRunLifecycleInput,
216
272
  operation: typeof pauseRun | typeof resumeRun,
217
- ): Promise<BuilderBuildRunResult> {
218
- const changed = await changeBuilderBuildRunLifecycleResult(input, operation);
273
+ ): Promise<BuilderFlowRunResult> {
274
+ const changed = await changeBuilderFlowRunLifecycleResult(input, operation);
219
275
  return resultFromRun(changed, input.runId);
220
276
  }
221
277
 
222
- async function changeBuilderBuildRunLifecycleResult(
278
+ async function changeBuilderFlowRunLifecycleResult(
223
279
  input: ChangeBuilderBuildRunLifecycleInput,
224
280
  operation: typeof pauseRun | typeof resumeRun | typeof cancelRun,
225
281
  ) {
226
282
  assertRuntimeInput(input, []);
227
283
  if (!isRecord(input.request)) throw new BuilderBuildRunInputError("request", "must be a lifecycle request object");
228
284
  const cwd = input.cwd ?? process.cwd();
229
- const before = await loadBuilderBuildRun({ runId: input.runId, cwd });
285
+ const before = await loadBuilderFlowRun({ runId: input.runId, cwd });
230
286
  const changed = await operation(input.runId, { cwd, ...input.request, ...(input.at ? { at: input.at } : {}) });
231
- const definition = await loadShippedBuilderBuildDefinition(resolveBuilderBuildFlowDefinitionPath());
287
+ const definition = await loadShippedBuilderFlowDefinitionForRun(input.runId, changed.definition);
232
288
  assertCanonicalDefinition(input.runId, definition, changed.definition);
233
289
  if (changed.state.subject !== before.state.subject) {
234
290
  throw new BuilderBuildRunInputError("flow_run.state.subject", "changed during lifecycle transition");
@@ -236,7 +292,7 @@ async function changeBuilderBuildRunLifecycleResult(
236
292
  return changed;
237
293
  }
238
294
 
239
- function resultFromRun(run: Awaited<ReturnType<typeof loadRun>>, runId: string): BuilderBuildRunResult {
295
+ function resultFromRun(run: Awaited<ReturnType<typeof loadRun>>, runId: string): BuilderFlowRunResult {
240
296
  return {
241
297
  definitionId: run.definition.id,
242
298
  definitionVersion: run.definition.version,
@@ -250,7 +306,7 @@ function resultFromRun(run: Awaited<ReturnType<typeof loadRun>>, runId: string):
250
306
  };
251
307
  }
252
308
 
253
- async function loadCanonicalBuilderBuildRun(
309
+ async function loadCanonicalBuilderFlowRun(
254
310
  runId: string,
255
311
  cwd: string,
256
312
  definition: { id: string; version: string },
@@ -260,29 +316,55 @@ async function loadCanonicalBuilderBuildRun(
260
316
  return run;
261
317
  }
262
318
 
263
- async function loadShippedBuilderBuildDefinition(definitionPath: string): Promise<{ id: string; version: string }> {
319
+ async function loadShippedBuilderFlowDefinition(flowId: BuilderFlowId, definitionPath: string): Promise<{ id: string; version: string }> {
264
320
  const packageRoot = findPackageRoot(path.dirname(definitionPath));
265
- const effective = resolveEffectiveFlowDefinition(BUILDER_BUILD_FLOW_ID, packageRoot, { allowOverride: false });
321
+ const effective = resolveEffectiveFlowDefinition(flowId, packageRoot, { allowOverride: false });
266
322
  if (!effective) {
267
323
  throw new BuilderBuildRunInputError("definition", "could not compile the shipped uses_flow composition");
268
324
  }
269
325
  const definition = validateDefinition(effective);
270
- if (definition.id !== BUILDER_BUILD_FLOW_ID) {
271
- throw new BuilderBuildRunInputError("definition", `expected shipped definition id ${BUILDER_BUILD_FLOW_ID}`);
326
+ if (definition.id !== flowId) {
327
+ throw new BuilderBuildRunInputError("definition", `expected shipped definition id ${flowId}`);
272
328
  }
273
329
  return definition;
274
330
  }
275
331
 
276
- function materializeRuntimeDefinition(cwd: string, definition: unknown): string {
332
+ async function loadShippedBuilderFlowDefinitionForRun(runId: string, actualDefinition: { id: string; version: string }): Promise<{ id: string; version: string }> {
333
+ const flowId = actualDefinition.id;
334
+ if (!isBuilderFlowId(flowId)) {
335
+ throw new BuilderBuildRunIdentityError(runId, { id: BUILDER_BUILD_FLOW_ID, version: "unknown" }, actualDefinition, "definition-id");
336
+ }
337
+ return loadShippedBuilderFlowDefinition(flowId, resolveBuilderFlowDefinitionPath(flowId));
338
+ }
339
+
340
+ function materializeRuntimeDefinition(cwd: string, flowId: BuilderFlowId, definition: unknown): string {
277
341
  const content = `${JSON.stringify(definition, null, 2)}\n`;
278
342
  const digest = createHash("sha256").update(content).digest("hex").slice(0, 16);
279
343
  const directory = path.join(cwd, ".kontourai", "flow-agents", "runtime-definitions");
280
344
  mkdirSync(directory, { recursive: true });
281
- const file = path.join(directory, `builder-build-${digest}.flow.json`);
345
+ const file = path.join(directory, `${flowId.replace(".", "-")}-${digest}.flow.json`);
282
346
  if (!existsSync(file)) writeFileSync(file, content);
283
347
  return file;
284
348
  }
285
349
 
350
+ function flowRelativePath(flowId: BuilderFlowId): string {
351
+ return flowId === BUILDER_BUILD_FLOW_ID ? BUILDER_BUILD_FLOW_RELATIVE_PATH : BUILDER_SHAPE_FLOW_RELATIVE_PATH;
352
+ }
353
+
354
+ function isBuilderFlowId(value: string): value is BuilderFlowId {
355
+ return value === BUILDER_BUILD_FLOW_ID || value === BUILDER_SHAPE_FLOW_ID;
356
+ }
357
+
358
+ function assertExpectedFlow(runId: string, actual: BuilderFlowId, expected: BuilderFlowId): void {
359
+ if (actual === expected) return;
360
+ throw new BuilderBuildRunIdentityError(runId, { id: expected, version: "unknown" }, { id: actual, version: "unknown" }, "definition-id");
361
+ }
362
+
363
+ function asBuilderBuildResult(result: BuilderFlowRunResult, runId: string): BuilderBuildRunResult {
364
+ assertExpectedFlow(runId, result.definitionId, BUILDER_BUILD_FLOW_ID);
365
+ return result as BuilderBuildRunResult;
366
+ }
367
+
286
368
  function assertCanonicalDefinition(
287
369
  runId: string,
288
370
  expectedDefinition: { id: string; version: string },
@@ -319,6 +401,9 @@ function validateEvidenceInput(evidence: unknown): BuilderBuildTrustBundleEviden
319
401
  if (!isNonEmptyString(evidence.file)) {
320
402
  throw new BuilderBuildRunInputError("evidence.file", "must be a non-empty string");
321
403
  }
404
+ if (evidence.expectedSha256 !== undefined && (!isNonEmptyString(evidence.expectedSha256) || !/^[a-f0-9]{64}$/i.test(evidence.expectedSha256))) {
405
+ throw new BuilderBuildRunInputError("evidence.expectedSha256", "must be a SHA-256 hex digest");
406
+ }
322
407
  if (evidence.status !== undefined && evidence.status !== "passed" && evidence.status !== "failed") {
323
408
  throw new BuilderBuildRunInputError("evidence.status", "must be passed or failed");
324
409
  }
@@ -358,11 +443,12 @@ function assertBundleSubjects(bundle: unknown, subject: string, gate: unknown):
358
443
  }
359
444
  }
360
445
 
361
- function trustBundleAttachOptions(cwd: string, evidence: BuilderBuildTrustBundleEvidenceInput): JsonObject {
446
+ function trustBundleAttachOptions(cwd: string, evidence: BuilderBuildTrustBundleEvidenceInput, expectedSha256: string): JsonObject {
362
447
  return {
363
448
  cwd,
364
449
  gate: evidence.gate,
365
450
  file: evidence.file,
451
+ expectedSha256,
366
452
  kind: "trust.bundle",
367
453
  bundle: true,
368
454
  ...(evidence.status ? { status: evidence.status } : {}),