@kontourai/flow-agents 3.5.0 → 3.7.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.
- package/.github/workflows/ci.yml +4 -0
- package/CHANGELOG.md +15 -0
- package/build/src/builder-flow-run-adapter.d.ts +32 -3
- package/build/src/builder-flow-run-adapter.js +113 -20
- package/build/src/builder-flow-runtime.d.ts +26 -3
- package/build/src/builder-flow-runtime.js +502 -49
- package/build/src/builder-lifecycle-authority.d.ts +35 -0
- package/build/src/builder-lifecycle-authority.js +219 -0
- package/build/src/cli/assignment-provider.d.ts +14 -7
- package/build/src/cli/assignment-provider.js +128 -65
- package/build/src/cli/builder-run.js +46 -9
- package/build/src/cli/workflow-artifact-cleanup-audit.js +3 -0
- package/build/src/cli/workflow-sidecar.d.ts +30 -3
- package/build/src/cli/workflow-sidecar.js +825 -99
- package/build/src/cli/workflow.d.ts +2 -0
- package/build/src/cli/workflow.js +769 -0
- package/build/src/cli.js +2 -0
- package/build/src/flow-kit/validate.d.ts +17 -0
- package/build/src/flow-kit/validate.js +340 -2
- package/build/src/index.d.ts +4 -0
- package/build/src/index.js +7 -5
- package/build/src/lib/observed-command.d.ts +7 -0
- package/build/src/lib/observed-command.js +100 -0
- package/build/src/lib/package-version.d.ts +2 -0
- package/build/src/lib/package-version.js +13 -0
- package/build/src/lib/pinned-cli-command.d.ts +6 -0
- package/build/src/lib/pinned-cli-command.js +21 -0
- package/build/src/tools/generate-context-map.js +5 -3
- package/context/contracts/artifact-contract.md +1 -1
- package/context/scripts/hooks/config-protection.js +8 -1
- package/context/scripts/hooks/lib/config-protection-remedies.js +3 -0
- package/context/scripts/hooks/lib/kit-catalog.js +1 -1
- package/context/scripts/hooks/stop-goal-fit.js +79 -10
- package/docs/context-map.md +24 -20
- package/docs/developer-architecture.md +1 -1
- package/docs/public-workflow-cli.md +132 -0
- package/docs/skills-map.md +51 -27
- package/docs/spec/builder-flow-runtime.md +78 -40
- package/docs/workflow-usage-guide.md +110 -38
- package/evals/ci/run-baseline.sh +2 -0
- package/evals/fixtures/hook-influence/cases.json +2 -2
- package/evals/integration/test_builder_entry_enforcement.sh +57 -45
- package/evals/integration/test_builder_step_producers.sh +297 -6
- package/evals/integration/test_bundle_install.sh +258 -55
- package/evals/integration/test_critique_supersession_roundtrip.sh +21 -8
- package/evals/integration/test_current_json_per_actor.sh +12 -0
- package/evals/integration/test_dual_emit_flow_step.sh +62 -43
- package/evals/integration/test_flowdef_session_activation.sh +145 -25
- package/evals/integration/test_flowdef_session_history_preservation.sh +23 -21
- package/evals/integration/test_gate_lockdown.sh +3 -5
- package/evals/integration/test_goal_fit_hook.sh +60 -2
- package/evals/integration/test_liveness_verdict.sh +14 -17
- package/evals/integration/test_phase_map_and_gate_claim.sh +63 -38
- package/evals/integration/test_public_workflow_cli.sh +573 -0
- package/evals/integration/test_pull_work_liveness_preflight.sh +22 -66
- package/evals/integration/test_sidecar_field_preservation.sh +36 -11
- package/evals/integration/test_workflow_sidecar_writer.sh +277 -44
- package/evals/integration/test_workflow_steering_hook.sh +15 -38
- package/evals/run.sh +2 -0
- package/evals/static/test_builder_skill_coherence.sh +247 -0
- package/evals/static/test_library_exports.sh +5 -2
- package/evals/static/test_workflow_skills.sh +13 -325
- package/kits/builder/flows/build.flow.json +22 -0
- package/kits/builder/flows/shape.flow.json +9 -9
- package/kits/builder/kit.json +70 -16
- package/kits/builder/skills/builder-shape/SKILL.md +75 -75
- package/kits/builder/skills/continue-work/SKILL.md +45 -106
- package/kits/builder/skills/deliver/SKILL.md +96 -442
- package/kits/builder/skills/design-probe/SKILL.md +40 -139
- package/kits/builder/skills/evidence-gate/SKILL.md +59 -201
- package/kits/builder/skills/execute-plan/SKILL.md +54 -125
- package/kits/builder/skills/fix-bug/SKILL.md +42 -132
- package/kits/builder/skills/gate-review/SKILL.md +60 -211
- package/kits/builder/skills/idea-to-backlog/SKILL.md +63 -244
- package/kits/builder/skills/learning-review/SKILL.md +63 -170
- package/kits/builder/skills/pickup-probe/SKILL.md +54 -111
- package/kits/builder/skills/plan-work/SKILL.md +51 -185
- package/kits/builder/skills/pull-work/SKILL.md +136 -485
- package/kits/builder/skills/release-readiness/SKILL.md +66 -107
- package/kits/builder/skills/review-work/SKILL.md +89 -176
- package/kits/builder/skills/tdd-workflow/SKILL.md +53 -147
- package/kits/builder/skills/verify-work/SKILL.md +101 -113
- package/package.json +2 -2
- package/schemas/builder-lifecycle-authorization.schema.json +57 -0
- package/schemas/lifecycle-authority-keys.schema.json +25 -0
- package/schemas/workflow-state.schema.json +1 -1
- package/scripts/hooks/config-protection.js +8 -1
- package/scripts/hooks/lib/config-protection-remedies.js +3 -0
- package/scripts/hooks/lib/kit-catalog.js +1 -1
- package/scripts/hooks/stop-goal-fit.js +79 -10
- package/src/builder-flow-run-adapter.ts +156 -23
- package/src/builder-flow-runtime.ts +535 -53
- package/src/builder-lifecycle-authority.ts +218 -0
- package/src/cli/assignment-provider-lock.test.mjs +83 -0
- package/src/cli/assignment-provider.ts +91 -22
- package/src/cli/builder-flow-run-adapter.test.mjs +4 -2
- package/src/cli/builder-flow-runtime.test.mjs +597 -8
- package/src/cli/builder-run.ts +54 -9
- package/src/cli/kit-metadata-security.test.mjs +232 -6
- package/src/cli/public-api.test.mjs +15 -0
- package/src/cli/workflow-artifact-cleanup-audit.ts +3 -0
- package/src/cli/workflow-sidecar-execution-proof.test.mjs +90 -0
- package/src/cli/workflow-sidecar.ts +748 -99
- package/src/cli/workflow.ts +708 -0
- package/src/cli.ts +2 -0
- package/src/flow-kit/validate.ts +320 -2
- package/src/index.ts +20 -5
- package/src/lib/observed-command.ts +96 -0
- package/src/lib/package-version.ts +15 -0
- package/src/lib/pinned-cli-command.ts +23 -0
- package/src/tools/generate-context-map.ts +5 -3
|
@@ -77,7 +77,7 @@ const PRE_EXECUTION_PHASES = new Set(['idea', 'backlog', 'pickup', 'planning']);
|
|
|
77
77
|
// Terminal tasks are complete — they must never gate a stop or count as "active".
|
|
78
78
|
// A stale current.json pointing at one, or a graveyard of finished states, must
|
|
79
79
|
// not block an unrelated session.
|
|
80
|
-
const TERMINAL_STATUSES = new Set(['done', 'delivered', 'accepted', 'archived', 'complete', 'completed']);
|
|
80
|
+
const TERMINAL_STATUSES = new Set(['done', 'delivered', 'canceled', 'accepted', 'archived', 'complete', 'completed']);
|
|
81
81
|
|
|
82
82
|
function isTerminalDeliveredState(state) {
|
|
83
83
|
if (!state || typeof state !== 'object') return false;
|
|
@@ -1759,6 +1759,37 @@ function hasSidecarPresence(artifactDir) {
|
|
|
1759
1759
|
return fs.existsSync(path.join(artifactDir, 'state.json')) || fs.existsSync(path.join(artifactDir, 'trust.bundle'));
|
|
1760
1760
|
}
|
|
1761
1761
|
|
|
1762
|
+
function canonicalFlowState(root, artifactDir) {
|
|
1763
|
+
if (!artifactDir) return { state: null, error: null };
|
|
1764
|
+
const slug = path.basename(artifactDir);
|
|
1765
|
+
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(slug)) return { state: null, error: 'canonical Flow run slug is malformed' };
|
|
1766
|
+
const runDir = path.join(root, '.kontourai', 'flow', 'runs', slug);
|
|
1767
|
+
const components = [
|
|
1768
|
+
path.join(root, '.kontourai'),
|
|
1769
|
+
path.join(root, '.kontourai', 'flow'),
|
|
1770
|
+
path.join(root, '.kontourai', 'flow', 'runs'),
|
|
1771
|
+
runDir,
|
|
1772
|
+
];
|
|
1773
|
+
try {
|
|
1774
|
+
for (const component of components) {
|
|
1775
|
+
const stat = fs.lstatSync(component);
|
|
1776
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) return { state: null, error: `canonical Flow run has an unsafe parent component: ${component}` };
|
|
1777
|
+
}
|
|
1778
|
+
const file = path.join(runDir, 'state.json');
|
|
1779
|
+
const stat = fs.lstatSync(file);
|
|
1780
|
+
if (stat.isSymbolicLink() || !stat.isFile()) return { state: null, error: 'canonical Flow state must be a non-symlink regular file' };
|
|
1781
|
+
const state = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
1782
|
+
if (!state || typeof state !== 'object' || Array.isArray(state)
|
|
1783
|
+
|| typeof state.status !== 'string' || !state.status.trim()
|
|
1784
|
+
|| typeof state.current_step !== 'string' || !state.current_step.trim()) {
|
|
1785
|
+
return { state: null, error: 'canonical Flow state is malformed' };
|
|
1786
|
+
}
|
|
1787
|
+
return { state, error: null };
|
|
1788
|
+
} catch (error) {
|
|
1789
|
+
return { state: null, error: `canonical Flow state is unavailable or malformed: ${safeOneLine(error && error.message || error, 120)}` };
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1762
1793
|
// WS8 (AC10a): when current.json names a slug whose session directory does NOT exist,
|
|
1763
1794
|
// return that slug so analyze() can log the staleness rather than silently falling back to
|
|
1764
1795
|
// a global mtime scan that could resurface an abandoned/never-real session as active.
|
|
@@ -1871,7 +1902,7 @@ function missingBundleOrStateSignal(artifactDir, activeFlowStep) {
|
|
|
1871
1902
|
//
|
|
1872
1903
|
// Both are used in analyze() for blocking decisions AND in run() for the AC2
|
|
1873
1904
|
// MAX_BLOCKS hard-block guard (preventing auto-release of hard blocks).
|
|
1874
|
-
const HARD_BLOCK = /contradicts evidence\.json|caught false-completion|evidence verdict:|evidence check .+ status:|critique status|critique open|required sidecar is missing|command-log integrity check FAILED|gate misconfiguration:|exit-code-laundered|NOT_VERIFIED \(ambiguous\)/;
|
|
1905
|
+
const HARD_BLOCK = /contradicts evidence\.json|caught false-completion|evidence verdict:|evidence check .+ status:|critique status|critique open|required sidecar is missing|command-log integrity check FAILED|gate misconfiguration:|exit-code-laundered|NOT_VERIFIED \(ambiguous\)|canonical Flow (?:run remains active|state is unsafe or malformed)/;
|
|
1875
1906
|
// FULL_BLOCK adds: workflow-state hygiene, surface-unavailable fail-closed, missing log.
|
|
1876
1907
|
const FULL_BLOCK = /status:|Definition Of Done|Goal Fit|sidecar validation:|contradicts evidence\.json|workflow state|evidence verdict|evidence check|NOT_VERIFIED gap|critique status|critique open|next action|caught false-completion|NOT_VERIFIED —|command-log integrity check FAILED|gate misconfiguration:|surface unavailable —|expected capture log is missing|exit-code-laundered|malformed-evidence|NOT_VERIFIED \(ambiguous\)/;
|
|
1877
1908
|
|
|
@@ -1897,7 +1928,31 @@ async function analyze(root, now = Date.now()) {
|
|
|
1897
1928
|
artifacts = artifacts.filter(a => a && hasSidecarPresence(path.dirname(a.file)));
|
|
1898
1929
|
}
|
|
1899
1930
|
|
|
1900
|
-
|
|
1931
|
+
const scopedCanonicalFlow = canonicalFlowState(root, scoped);
|
|
1932
|
+
const scopedState = scoped ? readJsonFile(path.join(scoped, 'state.json')) : null;
|
|
1933
|
+
const scopedProjectedActive = Boolean(scopedState && scopedState.flow_run && normalizedStatus(scopedState.flow_run.status) === 'active');
|
|
1934
|
+
if (scopedProjectedActive && scopedCanonicalFlow.error) {
|
|
1935
|
+
return {
|
|
1936
|
+
warnings: [`workflow state: canonical Flow state is unsafe or malformed for the active scoped session: ${scopedCanonicalFlow.error}. Resolve the canonical run before stopping.`],
|
|
1937
|
+
blocking: true,
|
|
1938
|
+
activeFlowRun: true,
|
|
1939
|
+
latestArtifactDir: scoped,
|
|
1940
|
+
gatePrefix: '[stop-gate]',
|
|
1941
|
+
};
|
|
1942
|
+
}
|
|
1943
|
+
if (artifacts.length === 0) {
|
|
1944
|
+
if (normalizedStatus(scopedCanonicalFlow.state?.status) === 'active') {
|
|
1945
|
+
const activeStep = safeOneLine(scopedCanonicalFlow.state.current_step || 'unknown', 80);
|
|
1946
|
+
return {
|
|
1947
|
+
warnings: [`workflow state: canonical Flow run remains active at step ${activeStep}; complete or explicitly cancel the run before stopping.`],
|
|
1948
|
+
blocking: true,
|
|
1949
|
+
activeFlowRun: true,
|
|
1950
|
+
latestArtifactDir: scoped,
|
|
1951
|
+
gatePrefix: '[stop-gate]',
|
|
1952
|
+
};
|
|
1953
|
+
}
|
|
1954
|
+
return { warnings: [], blocking: false, activeFlowRun: false, latestArtifactDir: null };
|
|
1955
|
+
}
|
|
1901
1956
|
|
|
1902
1957
|
const latest = artifacts[0];
|
|
1903
1958
|
const latestArtifactDir = path.dirname(latest.file);
|
|
@@ -1965,14 +2020,25 @@ async function analyze(root, now = Date.now()) {
|
|
|
1965
2020
|
// Use module-scope HARD_BLOCK / FULL_BLOCK (defined above analyze()).
|
|
1966
2021
|
// pre-execution/terminal tasks: only HARD_BLOCK signals cause a block.
|
|
1967
2022
|
// execution-onward tasks: FULL_BLOCK signals cause a block.
|
|
1968
|
-
const
|
|
2023
|
+
const canonicalFlow = canonicalFlowState(root, latestArtifactDir);
|
|
2024
|
+
const activeProjectedFlow = gateState && gateState.flow_run && normalizedStatus(gateState.flow_run.status) === 'active';
|
|
2025
|
+
const unsafeActiveCanonical = Boolean(activeProjectedFlow && canonicalFlow.error);
|
|
2026
|
+
if (unsafeActiveCanonical) {
|
|
2027
|
+
warnings.push(`workflow state: canonical Flow state is unsafe or malformed for the active scoped session: ${canonicalFlow.error}. Resolve the canonical run before stopping.`);
|
|
2028
|
+
}
|
|
2029
|
+
const activeFlowRun = normalizedStatus(canonicalFlow.state?.status) === 'active'
|
|
2030
|
+
|| (gateState && gateState.flow_run && normalizedStatus(gateState.flow_run.status) === 'active');
|
|
2031
|
+
if (activeFlowRun && !warnings.some(w => /canonical Flow run remains active/.test(w))) {
|
|
2032
|
+
const activeStep = safeOneLine(canonicalFlow.state?.current_step || gateState?.flow_run?.current_step || 'unknown', 80);
|
|
2033
|
+
warnings.push(`workflow state: canonical Flow run remains active at step ${activeStep}; complete or explicitly cancel the run before stopping.`);
|
|
2034
|
+
}
|
|
1969
2035
|
const blockRe = ((preExecution && !activeFlowRun) || terminal) ? HARD_BLOCK : FULL_BLOCK;
|
|
1970
|
-
const blocking = warnings.some(w => {
|
|
2036
|
+
const blocking = activeFlowRun || warnings.some(w => {
|
|
1971
2037
|
// Capture cross-reference warn-mode notes never block (operator opted out).
|
|
1972
2038
|
if (/\[backstop in warn mode — not blocking\]/.test(w)) return false;
|
|
1973
2039
|
return blockRe.test(w);
|
|
1974
2040
|
});
|
|
1975
|
-
return { warnings, blocking, preExecution, gatePrefix: gateLabel(activeFlowStep), latestArtifactDir };
|
|
2041
|
+
return { warnings, blocking, activeFlowRun, preExecution, gatePrefix: gateLabel(activeFlowStep), latestArtifactDir };
|
|
1976
2042
|
}
|
|
1977
2043
|
|
|
1978
2044
|
/**
|
|
@@ -2220,8 +2286,9 @@ function releaseOnNonTerminalStop(root, artifactDir) {
|
|
|
2220
2286
|
|
|
2221
2287
|
const state = readJsonFile(path.join(artifactDir, 'state.json'));
|
|
2222
2288
|
if (!state) return; // AC5: no state.json — nothing to gate a release decision on.
|
|
2223
|
-
|
|
2224
|
-
|
|
2289
|
+
const canonicalFlow = canonicalFlowState(root, artifactDir);
|
|
2290
|
+
if (normalizedStatus(canonicalFlow.state?.status) === 'active' || (state.flow_run && normalizedStatus(state.flow_run.status) === 'active')) {
|
|
2291
|
+
process.stderr.write(`[Hook] Goal Fit: stop-hook release skipped for active Flow run "${safeOneLine(state.flow_run?.run_id || state.task_slug || path.basename(artifactDir), 80)}"; continuation remains governed by Flow state.\n`);
|
|
2225
2292
|
return;
|
|
2226
2293
|
}
|
|
2227
2294
|
|
|
@@ -2367,7 +2434,7 @@ async function run(rawInput) {
|
|
|
2367
2434
|
// with runtime-constructed paths or by modifying the warning
|
|
2368
2435
|
// text so the hash changes. The real anchor is external (signed checkpoints + human
|
|
2369
2436
|
// review). This raises the cost of the burn-through-the-counter escape vector.
|
|
2370
|
-
const isHardBlock = result.warnings.some(w => {
|
|
2437
|
+
const isHardBlock = result.activeFlowRun || result.warnings.some(w => {
|
|
2371
2438
|
if (/\[backstop in warn mode — not blocking\]/.test(w)) return false;
|
|
2372
2439
|
return HARD_BLOCK.test(w);
|
|
2373
2440
|
});
|
|
@@ -2375,7 +2442,9 @@ async function run(rawInput) {
|
|
|
2375
2442
|
// Do NOT clear the streak — keep accumulating so the same hard block stays visible.
|
|
2376
2443
|
return {
|
|
2377
2444
|
stdout: rawInput,
|
|
2378
|
-
stderr:
|
|
2445
|
+
stderr: result.activeFlowRun
|
|
2446
|
+
? `${message}\n${gatePrefix} max-blocks reached but canonical Flow remains active — not auto-releasing; complete or explicitly cancel the run.`
|
|
2447
|
+
: `${message}\n${gatePrefix} max-blocks reached but the block is a caught false-completion / integrity failure — not auto-releasing; requires a real fix or operator override.`,
|
|
2379
2448
|
exitCode: 2,
|
|
2380
2449
|
};
|
|
2381
2450
|
}
|
|
@@ -1,19 +1,22 @@
|
|
|
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";
|
|
6
6
|
import {
|
|
7
7
|
attachEvidence,
|
|
8
|
+
cancelRun,
|
|
8
9
|
evaluateRun,
|
|
9
10
|
expectationsForGate,
|
|
10
11
|
loadRun,
|
|
11
12
|
normalizeTrustBundle,
|
|
12
13
|
openGates,
|
|
13
|
-
|
|
14
|
+
pauseRun,
|
|
15
|
+
resumeRun,
|
|
14
16
|
startRun,
|
|
15
17
|
validateDefinition,
|
|
16
18
|
type FlowEvidenceEntry,
|
|
19
|
+
type FlowLifecycleRequest,
|
|
17
20
|
type FlowRunState,
|
|
18
21
|
type GateOutcome,
|
|
19
22
|
type JsonObject,
|
|
@@ -22,6 +25,9 @@ import { resolveEffectiveFlowDefinition } from "./lib/flow-resolver.js";
|
|
|
22
25
|
|
|
23
26
|
export const BUILDER_BUILD_FLOW_ID = "builder.build";
|
|
24
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;
|
|
25
31
|
|
|
26
32
|
export interface BuilderBuildTrustBundleEvidenceInput {
|
|
27
33
|
gate: string;
|
|
@@ -30,6 +36,8 @@ export interface BuilderBuildTrustBundleEvidenceInput {
|
|
|
30
36
|
* Callers must not pass raw user-controlled paths to this local runtime API.
|
|
31
37
|
*/
|
|
32
38
|
file: string;
|
|
39
|
+
/** SHA-256 of the immutable snapshot validated by Flow Agents before Flow attaches it. */
|
|
40
|
+
expectedSha256?: string;
|
|
33
41
|
status?: "passed" | "failed";
|
|
34
42
|
producer?: string;
|
|
35
43
|
authorityTrace?: string;
|
|
@@ -52,6 +60,10 @@ export interface StartBuilderBuildRunInput {
|
|
|
52
60
|
runId?: string;
|
|
53
61
|
}
|
|
54
62
|
|
|
63
|
+
export interface StartBuilderFlowRunInput extends StartBuilderBuildRunInput {
|
|
64
|
+
flowId: BuilderFlowId;
|
|
65
|
+
}
|
|
66
|
+
|
|
55
67
|
export interface EvaluateBuilderBuildRunInput {
|
|
56
68
|
runId: string;
|
|
57
69
|
/**
|
|
@@ -67,8 +79,13 @@ export interface LoadBuilderBuildRunInput {
|
|
|
67
79
|
cwd?: string;
|
|
68
80
|
}
|
|
69
81
|
|
|
70
|
-
export interface
|
|
71
|
-
|
|
82
|
+
export interface ChangeBuilderBuildRunLifecycleInput extends LoadBuilderBuildRunInput {
|
|
83
|
+
request: FlowLifecycleRequest;
|
|
84
|
+
at?: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface BuilderFlowRunResult {
|
|
88
|
+
definitionId: BuilderFlowId;
|
|
72
89
|
definitionVersion: string;
|
|
73
90
|
runId: string;
|
|
74
91
|
dir: string;
|
|
@@ -79,6 +96,10 @@ export interface BuilderBuildRunResult {
|
|
|
79
96
|
freshnessTransitions: JsonObject[];
|
|
80
97
|
}
|
|
81
98
|
|
|
99
|
+
export interface BuilderBuildRunResult extends Omit<BuilderFlowRunResult, "definitionId"> {
|
|
100
|
+
definitionId: typeof BUILDER_BUILD_FLOW_ID;
|
|
101
|
+
}
|
|
102
|
+
|
|
82
103
|
export type BuilderBuildRunIdentityMismatch = "definition-id" | "definition-version" | "definition-content";
|
|
83
104
|
|
|
84
105
|
export class BuilderBuildRunInputError extends Error {
|
|
@@ -119,20 +140,29 @@ export class BuilderBuildRunIdentityError extends Error {
|
|
|
119
140
|
}
|
|
120
141
|
|
|
121
142
|
export function resolveBuilderBuildFlowDefinitionPath(startDir = moduleDirectory()): string {
|
|
122
|
-
|
|
123
|
-
return path.join(root, BUILDER_BUILD_FLOW_RELATIVE_PATH);
|
|
143
|
+
return resolveBuilderFlowDefinitionPath(BUILDER_BUILD_FLOW_ID, startDir);
|
|
124
144
|
}
|
|
125
145
|
|
|
126
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> {
|
|
127
157
|
assertRuntimeInput(input, ["evidence", "now", "gate"]);
|
|
128
158
|
if (!isNonEmptyString(input.subject)) {
|
|
129
159
|
throw new BuilderBuildRunInputError("subject", "must be a non-empty string");
|
|
130
160
|
}
|
|
131
161
|
|
|
132
162
|
const cwd = input.cwd ?? process.cwd();
|
|
133
|
-
const definitionPath =
|
|
134
|
-
const definition = await
|
|
135
|
-
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);
|
|
136
166
|
const started = await startRun(runtimeDefinitionPath, {
|
|
137
167
|
cwd,
|
|
138
168
|
runId: input.runId,
|
|
@@ -141,12 +171,17 @@ export async function startBuilderBuildRun(input: StartBuilderBuildRunInput): Pr
|
|
|
141
171
|
subject: input.subject,
|
|
142
172
|
},
|
|
143
173
|
});
|
|
144
|
-
const run = await
|
|
174
|
+
const run = await loadCanonicalBuilderFlowRun(started.runId, cwd, definition);
|
|
145
175
|
|
|
146
176
|
return resultFromRun(run, started.runId);
|
|
147
177
|
}
|
|
148
178
|
|
|
149
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> {
|
|
150
185
|
assertRuntimeInput(input, ["now", "gate"]);
|
|
151
186
|
if (Array.isArray(input.evidence)) {
|
|
152
187
|
throw new BuilderBuildRunInputError("evidence", "must be zero or one evidence object, not an array");
|
|
@@ -154,16 +189,26 @@ export async function evaluateBuilderBuildRun(input: EvaluateBuilderBuildRunInpu
|
|
|
154
189
|
|
|
155
190
|
const cwd = input.cwd ?? process.cwd();
|
|
156
191
|
const run = await loadRun(input.runId, cwd);
|
|
157
|
-
const definition = await
|
|
192
|
+
const definition = await loadShippedBuilderFlowDefinitionForRun(input.runId, run.definition);
|
|
158
193
|
assertCanonicalDefinition(input.runId, definition, run.definition);
|
|
159
194
|
|
|
160
195
|
let attachedEvidence: FlowEvidenceEntry[] = [];
|
|
161
196
|
if (input.evidence !== undefined) {
|
|
162
197
|
const evidence = validateEvidenceInput(input.evidence);
|
|
163
198
|
assertCurrentOpenGate(run.definition, run.state, evidence.gate);
|
|
164
|
-
const
|
|
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")));
|
|
165
206
|
assertBundleSubjects(normalized.bundle, run.state.subject, openGates(run.definition, run.state)[0]);
|
|
166
|
-
|
|
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];
|
|
167
212
|
}
|
|
168
213
|
|
|
169
214
|
const evaluated = await evaluateRun(input.runId, { cwd });
|
|
@@ -181,15 +226,73 @@ export async function evaluateBuilderBuildRun(input: EvaluateBuilderBuildRunInpu
|
|
|
181
226
|
}
|
|
182
227
|
|
|
183
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> {
|
|
184
234
|
assertRuntimeInput(input, ["evidence", "now", "gate"]);
|
|
185
235
|
const cwd = input.cwd ?? process.cwd();
|
|
186
236
|
const run = await loadRun(input.runId, cwd);
|
|
187
|
-
const definition = await
|
|
237
|
+
const definition = await loadShippedBuilderFlowDefinitionForRun(input.runId, run.definition);
|
|
188
238
|
assertCanonicalDefinition(input.runId, definition, run.definition);
|
|
189
239
|
return resultFromRun(run, input.runId);
|
|
190
240
|
}
|
|
191
241
|
|
|
192
|
-
function
|
|
242
|
+
export async function pauseBuilderBuildRun(input: ChangeBuilderBuildRunLifecycleInput): Promise<BuilderBuildRunResult> {
|
|
243
|
+
const result = await pauseBuilderFlowRun(input);
|
|
244
|
+
return asBuilderBuildResult(result, input.runId);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export async function resumeBuilderBuildRun(input: ChangeBuilderBuildRunLifecycleInput): Promise<BuilderBuildRunResult> {
|
|
248
|
+
const result = await resumeBuilderFlowRun(input);
|
|
249
|
+
return asBuilderBuildResult(result, input.runId);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export async function cancelBuilderBuildRun(input: ChangeBuilderBuildRunLifecycleInput): Promise<BuilderBuildRunResult & { idempotent: boolean }> {
|
|
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);
|
|
267
|
+
return { ...resultFromRun(changed, input.runId), idempotent: changed.idempotent };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async function changeBuilderFlowRunLifecycle(
|
|
271
|
+
input: ChangeBuilderBuildRunLifecycleInput,
|
|
272
|
+
operation: typeof pauseRun | typeof resumeRun,
|
|
273
|
+
): Promise<BuilderFlowRunResult> {
|
|
274
|
+
const changed = await changeBuilderFlowRunLifecycleResult(input, operation);
|
|
275
|
+
return resultFromRun(changed, input.runId);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async function changeBuilderFlowRunLifecycleResult(
|
|
279
|
+
input: ChangeBuilderBuildRunLifecycleInput,
|
|
280
|
+
operation: typeof pauseRun | typeof resumeRun | typeof cancelRun,
|
|
281
|
+
) {
|
|
282
|
+
assertRuntimeInput(input, []);
|
|
283
|
+
if (!isRecord(input.request)) throw new BuilderBuildRunInputError("request", "must be a lifecycle request object");
|
|
284
|
+
const cwd = input.cwd ?? process.cwd();
|
|
285
|
+
const before = await loadBuilderFlowRun({ runId: input.runId, cwd });
|
|
286
|
+
const changed = await operation(input.runId, { cwd, ...input.request, ...(input.at ? { at: input.at } : {}) });
|
|
287
|
+
const definition = await loadShippedBuilderFlowDefinitionForRun(input.runId, changed.definition);
|
|
288
|
+
assertCanonicalDefinition(input.runId, definition, changed.definition);
|
|
289
|
+
if (changed.state.subject !== before.state.subject) {
|
|
290
|
+
throw new BuilderBuildRunInputError("flow_run.state.subject", "changed during lifecycle transition");
|
|
291
|
+
}
|
|
292
|
+
return changed;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function resultFromRun(run: Awaited<ReturnType<typeof loadRun>>, runId: string): BuilderFlowRunResult {
|
|
193
296
|
return {
|
|
194
297
|
definitionId: run.definition.id,
|
|
195
298
|
definitionVersion: run.definition.version,
|
|
@@ -203,7 +306,7 @@ function resultFromRun(run: Awaited<ReturnType<typeof loadRun>>, runId: string):
|
|
|
203
306
|
};
|
|
204
307
|
}
|
|
205
308
|
|
|
206
|
-
async function
|
|
309
|
+
async function loadCanonicalBuilderFlowRun(
|
|
207
310
|
runId: string,
|
|
208
311
|
cwd: string,
|
|
209
312
|
definition: { id: string; version: string },
|
|
@@ -213,29 +316,55 @@ async function loadCanonicalBuilderBuildRun(
|
|
|
213
316
|
return run;
|
|
214
317
|
}
|
|
215
318
|
|
|
216
|
-
async function
|
|
319
|
+
async function loadShippedBuilderFlowDefinition(flowId: BuilderFlowId, definitionPath: string): Promise<{ id: string; version: string }> {
|
|
217
320
|
const packageRoot = findPackageRoot(path.dirname(definitionPath));
|
|
218
|
-
const effective = resolveEffectiveFlowDefinition(
|
|
321
|
+
const effective = resolveEffectiveFlowDefinition(flowId, packageRoot, { allowOverride: false });
|
|
219
322
|
if (!effective) {
|
|
220
323
|
throw new BuilderBuildRunInputError("definition", "could not compile the shipped uses_flow composition");
|
|
221
324
|
}
|
|
222
325
|
const definition = validateDefinition(effective);
|
|
223
|
-
if (definition.id !==
|
|
224
|
-
throw new BuilderBuildRunInputError("definition", `expected shipped definition id ${
|
|
326
|
+
if (definition.id !== flowId) {
|
|
327
|
+
throw new BuilderBuildRunInputError("definition", `expected shipped definition id ${flowId}`);
|
|
225
328
|
}
|
|
226
329
|
return definition;
|
|
227
330
|
}
|
|
228
331
|
|
|
229
|
-
function
|
|
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 {
|
|
230
341
|
const content = `${JSON.stringify(definition, null, 2)}\n`;
|
|
231
342
|
const digest = createHash("sha256").update(content).digest("hex").slice(0, 16);
|
|
232
343
|
const directory = path.join(cwd, ".kontourai", "flow-agents", "runtime-definitions");
|
|
233
344
|
mkdirSync(directory, { recursive: true });
|
|
234
|
-
const file = path.join(directory,
|
|
345
|
+
const file = path.join(directory, `${flowId.replace(".", "-")}-${digest}.flow.json`);
|
|
235
346
|
if (!existsSync(file)) writeFileSync(file, content);
|
|
236
347
|
return file;
|
|
237
348
|
}
|
|
238
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
|
+
|
|
239
368
|
function assertCanonicalDefinition(
|
|
240
369
|
runId: string,
|
|
241
370
|
expectedDefinition: { id: string; version: string },
|
|
@@ -272,6 +401,9 @@ function validateEvidenceInput(evidence: unknown): BuilderBuildTrustBundleEviden
|
|
|
272
401
|
if (!isNonEmptyString(evidence.file)) {
|
|
273
402
|
throw new BuilderBuildRunInputError("evidence.file", "must be a non-empty string");
|
|
274
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
|
+
}
|
|
275
407
|
if (evidence.status !== undefined && evidence.status !== "passed" && evidence.status !== "failed") {
|
|
276
408
|
throw new BuilderBuildRunInputError("evidence.status", "must be passed or failed");
|
|
277
409
|
}
|
|
@@ -311,11 +443,12 @@ function assertBundleSubjects(bundle: unknown, subject: string, gate: unknown):
|
|
|
311
443
|
}
|
|
312
444
|
}
|
|
313
445
|
|
|
314
|
-
function trustBundleAttachOptions(cwd: string, evidence: BuilderBuildTrustBundleEvidenceInput): JsonObject {
|
|
446
|
+
function trustBundleAttachOptions(cwd: string, evidence: BuilderBuildTrustBundleEvidenceInput, expectedSha256: string): JsonObject {
|
|
315
447
|
return {
|
|
316
448
|
cwd,
|
|
317
449
|
gate: evidence.gate,
|
|
318
450
|
file: evidence.file,
|
|
451
|
+
expectedSha256,
|
|
319
452
|
kind: "trust.bundle",
|
|
320
453
|
bundle: true,
|
|
321
454
|
...(evidence.status ? { status: evidence.status } : {}),
|