@kontourai/flow-agents 3.12.0 → 3.12.1
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/CHANGELOG.md +7 -0
- package/build/src/builder-gate-action-envelope.d.ts +2 -1
- package/build/src/builder-gate-action-envelope.js +4 -3
- package/build/src/continuation-validation.js +31 -1
- package/dist/base/build/package.json +1 -1
- package/dist/base/build/src/builder-gate-action-envelope.d.ts +2 -1
- package/dist/base/build/src/builder-gate-action-envelope.js +4 -3
- package/dist/base/build/src/continuation-validation.js +31 -1
- package/dist/base/docs/migrations.md +5 -0
- package/dist/base/install.sh +1 -1
- package/dist/claude-code/build/package.json +1 -1
- package/dist/claude-code/build/src/builder-gate-action-envelope.d.ts +2 -1
- package/dist/claude-code/build/src/builder-gate-action-envelope.js +4 -3
- package/dist/claude-code/build/src/continuation-validation.js +31 -1
- package/dist/claude-code/docs/migrations.md +5 -0
- package/dist/claude-code/install.sh +1 -1
- package/dist/codex/build/package.json +1 -1
- package/dist/codex/build/src/builder-gate-action-envelope.d.ts +2 -1
- package/dist/codex/build/src/builder-gate-action-envelope.js +4 -3
- package/dist/codex/build/src/continuation-validation.js +31 -1
- package/dist/codex/docs/migrations.md +5 -0
- package/dist/codex/install.sh +1 -1
- package/dist/kiro/build/package.json +1 -1
- package/dist/kiro/build/src/builder-gate-action-envelope.d.ts +2 -1
- package/dist/kiro/build/src/builder-gate-action-envelope.js +4 -3
- package/dist/kiro/build/src/continuation-validation.js +31 -1
- package/dist/kiro/docs/migrations.md +5 -0
- package/dist/kiro/install.sh +1 -1
- package/dist/opencode/build/package.json +1 -1
- package/dist/opencode/build/src/builder-gate-action-envelope.d.ts +2 -1
- package/dist/opencode/build/src/builder-gate-action-envelope.js +4 -3
- package/dist/opencode/build/src/continuation-validation.js +31 -1
- package/dist/opencode/docs/migrations.md +5 -0
- package/dist/opencode/install.sh +1 -1
- package/dist/pi/build/package.json +1 -1
- package/dist/pi/build/src/builder-gate-action-envelope.d.ts +2 -1
- package/dist/pi/build/src/builder-gate-action-envelope.js +4 -3
- package/dist/pi/build/src/continuation-validation.js +31 -1
- package/dist/pi/docs/migrations.md +5 -0
- package/dist/pi/install.sh +1 -1
- package/docs/migrations.md +5 -0
- package/package.json +1 -1
- package/src/builder-gate-action-envelope.ts +6 -4
- package/src/cli/builder-flow-runtime.test.mjs +4 -2
- package/src/cli/continuation-driver.test.mjs +16 -1
- package/src/continuation-validation.ts +31 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [3.12.1](https://github.com/kontourai/flow-agents/compare/v3.12.0...v3.12.1) (2026-07-13)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Fixes
|
|
7
|
+
|
|
8
|
+
* bind gate-action requirements to gates ([#608](https://github.com/kontourai/flow-agents/issues/608)) ([b1e9ad1](https://github.com/kontourai/flow-agents/commit/b1e9ad1a03ba1b88e09d6ace156e33c010bc6848))
|
|
9
|
+
|
|
3
10
|
## [3.12.0](https://github.com/kontourai/flow-agents/compare/v3.11.0...v3.12.0) (2026-07-13)
|
|
4
11
|
|
|
5
12
|
|
|
@@ -35,7 +35,7 @@ export type GateActionPublicMutation = GateActionWorkflowMutation | {
|
|
|
35
35
|
};
|
|
36
36
|
};
|
|
37
37
|
export type GateActionEnvelope = {
|
|
38
|
-
schema_version: "
|
|
38
|
+
schema_version: "2.0";
|
|
39
39
|
flow: {
|
|
40
40
|
run_id: string;
|
|
41
41
|
definition_id: string;
|
|
@@ -47,6 +47,7 @@ export type GateActionEnvelope = {
|
|
|
47
47
|
gate: {
|
|
48
48
|
requirements: Array<{
|
|
49
49
|
id: string;
|
|
50
|
+
gate_id: string;
|
|
50
51
|
required: boolean;
|
|
51
52
|
description: string;
|
|
52
53
|
claim_type: string;
|
|
@@ -70,7 +70,7 @@ function deriveFlowRequirements(input, action) {
|
|
|
70
70
|
const matched = new Set((Array.isArray(outcome.matched_expectations) ? outcome.matched_expectations : [])
|
|
71
71
|
.flatMap((entry) => isRecord(entry) && typeof entry.expectation_id === "string" ? [entry.expectation_id] : []));
|
|
72
72
|
return expectationsForGate(gate, input.run.config)
|
|
73
|
-
.map((expectation) => requirementFromExpectation(expectation, matched, typeof outcome.accepted_exception_id === "string"));
|
|
73
|
+
.map((expectation) => requirementFromExpectation(gate.id, expectation, matched, typeof outcome.accepted_exception_id === "string"));
|
|
74
74
|
});
|
|
75
75
|
if (requirements.length > MAX_REQUIREMENTS)
|
|
76
76
|
throw new Error("Builder gate-action envelope requirements exceed the supported bound");
|
|
@@ -91,7 +91,7 @@ function assembleGateActionEnvelope(input, loaded, derived) {
|
|
|
91
91
|
.filter((binding) => !isControlArtifact(binding.artifact) && binding.expectation_ids.some((id) => unresolvedRequired.includes(id)))
|
|
92
92
|
.map((binding) => binding.artifact);
|
|
93
93
|
const envelope = {
|
|
94
|
-
schema_version: "
|
|
94
|
+
schema_version: "2.0",
|
|
95
95
|
flow: {
|
|
96
96
|
run_id: input.run.runId,
|
|
97
97
|
definition_id: input.run.definitionId,
|
|
@@ -215,7 +215,7 @@ function workflowEvidenceMutation(expectationId, sessionArgument, packageVersion
|
|
|
215
215
|
],
|
|
216
216
|
};
|
|
217
217
|
}
|
|
218
|
-
function requirementFromExpectation(expectation, satisfied, acceptedException) {
|
|
218
|
+
function requirementFromExpectation(gateId, expectation, satisfied, acceptedException) {
|
|
219
219
|
const record = expectation;
|
|
220
220
|
const claim = isRecord(record.bundle_claim) ? record.bundle_claim : null;
|
|
221
221
|
if (typeof record.id !== "string" || typeof record.description !== "string" || !claim || typeof claim.claimType !== "string") {
|
|
@@ -223,6 +223,7 @@ function requirementFromExpectation(expectation, satisfied, acceptedException) {
|
|
|
223
223
|
}
|
|
224
224
|
return {
|
|
225
225
|
id: record.id,
|
|
226
|
+
gate_id: gateId,
|
|
226
227
|
required: record.required === true,
|
|
227
228
|
description: record.description,
|
|
228
229
|
claim_type: claim.claimType,
|
|
@@ -18,12 +18,42 @@ export function validateSnapshot(value) {
|
|
|
18
18
|
return structuredClone(value);
|
|
19
19
|
}
|
|
20
20
|
function validateGateActionEnvelope(value) {
|
|
21
|
-
if (!value || typeof value !== "object" || value.schema_version !== "
|
|
21
|
+
if (!value || typeof value !== "object" || value.schema_version !== "2.0" || !value.flow || typeof value.flow.current_step !== "string"
|
|
22
22
|
|| !value.progress || !Array.isArray(value.progress.canonical_evidence) || !Array.isArray(value.progress.observed_artifacts))
|
|
23
23
|
throw new Error("continuation snapshot gate-action envelope is malformed");
|
|
24
|
+
if (value.gate !== undefined)
|
|
25
|
+
validateGateRequirementBindings(value);
|
|
24
26
|
if (Buffer.byteLength(JSON.stringify(value), "utf8") > 65_536)
|
|
25
27
|
throw new Error("continuation snapshot gate-action envelope exceeds 65536 bytes");
|
|
26
28
|
}
|
|
29
|
+
function validateGateRequirementBindings(value) {
|
|
30
|
+
if (!Array.isArray(value.flow.gate_ids) || !Array.isArray(value.gate.requirements) || !Array.isArray(value.gate.accepted_exceptions)) {
|
|
31
|
+
throw new Error("continuation snapshot gate-action envelope has malformed gate bindings");
|
|
32
|
+
}
|
|
33
|
+
const activeGates = new Set(value.flow.gate_ids);
|
|
34
|
+
const acceptedGates = new Set();
|
|
35
|
+
for (const exception of value.gate.accepted_exceptions) {
|
|
36
|
+
if (!exception || typeof exception.gate_id !== "string" || typeof exception.exception_id !== "string"
|
|
37
|
+
|| !activeGates.has(exception.gate_id) || acceptedGates.has(exception.gate_id)) {
|
|
38
|
+
throw new Error("continuation snapshot gate-action envelope has invalid accepted exceptions");
|
|
39
|
+
}
|
|
40
|
+
acceptedGates.add(exception.gate_id);
|
|
41
|
+
}
|
|
42
|
+
const gatesWithRequirements = new Set();
|
|
43
|
+
for (const requirement of value.gate.requirements) {
|
|
44
|
+
if (!requirement || typeof requirement.gate_id !== "string" || !activeGates.has(requirement.gate_id)) {
|
|
45
|
+
throw new Error("continuation snapshot gate-action envelope has an invalid requirement gate binding");
|
|
46
|
+
}
|
|
47
|
+
gatesWithRequirements.add(requirement.gate_id);
|
|
48
|
+
if ((requirement.status === "accepted_exception") !== acceptedGates.has(requirement.gate_id)
|
|
49
|
+
&& requirement.status !== "satisfied") {
|
|
50
|
+
throw new Error("continuation snapshot gate-action envelope has inconsistent exception bindings");
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if ([...acceptedGates].some((gateId) => !gatesWithRequirements.has(gateId))) {
|
|
54
|
+
throw new Error("continuation snapshot gate-action envelope exception has no bound requirements");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
27
57
|
export function validateTurnResult(value) {
|
|
28
58
|
if (!value || typeof value !== "object" || (value.status !== "completed" && value.status !== "wait"))
|
|
29
59
|
throw new Error("continuation adapter must return status completed or wait");
|
|
@@ -35,7 +35,7 @@ export type GateActionPublicMutation = GateActionWorkflowMutation | {
|
|
|
35
35
|
};
|
|
36
36
|
};
|
|
37
37
|
export type GateActionEnvelope = {
|
|
38
|
-
schema_version: "
|
|
38
|
+
schema_version: "2.0";
|
|
39
39
|
flow: {
|
|
40
40
|
run_id: string;
|
|
41
41
|
definition_id: string;
|
|
@@ -47,6 +47,7 @@ export type GateActionEnvelope = {
|
|
|
47
47
|
gate: {
|
|
48
48
|
requirements: Array<{
|
|
49
49
|
id: string;
|
|
50
|
+
gate_id: string;
|
|
50
51
|
required: boolean;
|
|
51
52
|
description: string;
|
|
52
53
|
claim_type: string;
|
|
@@ -70,7 +70,7 @@ function deriveFlowRequirements(input, action) {
|
|
|
70
70
|
const matched = new Set((Array.isArray(outcome.matched_expectations) ? outcome.matched_expectations : [])
|
|
71
71
|
.flatMap((entry) => isRecord(entry) && typeof entry.expectation_id === "string" ? [entry.expectation_id] : []));
|
|
72
72
|
return expectationsForGate(gate, input.run.config)
|
|
73
|
-
.map((expectation) => requirementFromExpectation(expectation, matched, typeof outcome.accepted_exception_id === "string"));
|
|
73
|
+
.map((expectation) => requirementFromExpectation(gate.id, expectation, matched, typeof outcome.accepted_exception_id === "string"));
|
|
74
74
|
});
|
|
75
75
|
if (requirements.length > MAX_REQUIREMENTS)
|
|
76
76
|
throw new Error("Builder gate-action envelope requirements exceed the supported bound");
|
|
@@ -91,7 +91,7 @@ function assembleGateActionEnvelope(input, loaded, derived) {
|
|
|
91
91
|
.filter((binding) => !isControlArtifact(binding.artifact) && binding.expectation_ids.some((id) => unresolvedRequired.includes(id)))
|
|
92
92
|
.map((binding) => binding.artifact);
|
|
93
93
|
const envelope = {
|
|
94
|
-
schema_version: "
|
|
94
|
+
schema_version: "2.0",
|
|
95
95
|
flow: {
|
|
96
96
|
run_id: input.run.runId,
|
|
97
97
|
definition_id: input.run.definitionId,
|
|
@@ -215,7 +215,7 @@ function workflowEvidenceMutation(expectationId, sessionArgument, packageVersion
|
|
|
215
215
|
],
|
|
216
216
|
};
|
|
217
217
|
}
|
|
218
|
-
function requirementFromExpectation(expectation, satisfied, acceptedException) {
|
|
218
|
+
function requirementFromExpectation(gateId, expectation, satisfied, acceptedException) {
|
|
219
219
|
const record = expectation;
|
|
220
220
|
const claim = isRecord(record.bundle_claim) ? record.bundle_claim : null;
|
|
221
221
|
if (typeof record.id !== "string" || typeof record.description !== "string" || !claim || typeof claim.claimType !== "string") {
|
|
@@ -223,6 +223,7 @@ function requirementFromExpectation(expectation, satisfied, acceptedException) {
|
|
|
223
223
|
}
|
|
224
224
|
return {
|
|
225
225
|
id: record.id,
|
|
226
|
+
gate_id: gateId,
|
|
226
227
|
required: record.required === true,
|
|
227
228
|
description: record.description,
|
|
228
229
|
claim_type: claim.claimType,
|
|
@@ -18,12 +18,42 @@ export function validateSnapshot(value) {
|
|
|
18
18
|
return structuredClone(value);
|
|
19
19
|
}
|
|
20
20
|
function validateGateActionEnvelope(value) {
|
|
21
|
-
if (!value || typeof value !== "object" || value.schema_version !== "
|
|
21
|
+
if (!value || typeof value !== "object" || value.schema_version !== "2.0" || !value.flow || typeof value.flow.current_step !== "string"
|
|
22
22
|
|| !value.progress || !Array.isArray(value.progress.canonical_evidence) || !Array.isArray(value.progress.observed_artifacts))
|
|
23
23
|
throw new Error("continuation snapshot gate-action envelope is malformed");
|
|
24
|
+
if (value.gate !== undefined)
|
|
25
|
+
validateGateRequirementBindings(value);
|
|
24
26
|
if (Buffer.byteLength(JSON.stringify(value), "utf8") > 65_536)
|
|
25
27
|
throw new Error("continuation snapshot gate-action envelope exceeds 65536 bytes");
|
|
26
28
|
}
|
|
29
|
+
function validateGateRequirementBindings(value) {
|
|
30
|
+
if (!Array.isArray(value.flow.gate_ids) || !Array.isArray(value.gate.requirements) || !Array.isArray(value.gate.accepted_exceptions)) {
|
|
31
|
+
throw new Error("continuation snapshot gate-action envelope has malformed gate bindings");
|
|
32
|
+
}
|
|
33
|
+
const activeGates = new Set(value.flow.gate_ids);
|
|
34
|
+
const acceptedGates = new Set();
|
|
35
|
+
for (const exception of value.gate.accepted_exceptions) {
|
|
36
|
+
if (!exception || typeof exception.gate_id !== "string" || typeof exception.exception_id !== "string"
|
|
37
|
+
|| !activeGates.has(exception.gate_id) || acceptedGates.has(exception.gate_id)) {
|
|
38
|
+
throw new Error("continuation snapshot gate-action envelope has invalid accepted exceptions");
|
|
39
|
+
}
|
|
40
|
+
acceptedGates.add(exception.gate_id);
|
|
41
|
+
}
|
|
42
|
+
const gatesWithRequirements = new Set();
|
|
43
|
+
for (const requirement of value.gate.requirements) {
|
|
44
|
+
if (!requirement || typeof requirement.gate_id !== "string" || !activeGates.has(requirement.gate_id)) {
|
|
45
|
+
throw new Error("continuation snapshot gate-action envelope has an invalid requirement gate binding");
|
|
46
|
+
}
|
|
47
|
+
gatesWithRequirements.add(requirement.gate_id);
|
|
48
|
+
if ((requirement.status === "accepted_exception") !== acceptedGates.has(requirement.gate_id)
|
|
49
|
+
&& requirement.status !== "satisfied") {
|
|
50
|
+
throw new Error("continuation snapshot gate-action envelope has inconsistent exception bindings");
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if ([...acceptedGates].some((gateId) => !gatesWithRequirements.has(gateId))) {
|
|
54
|
+
throw new Error("continuation snapshot gate-action envelope exception has no bound requirements");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
27
57
|
export function validateTurnResult(value) {
|
|
28
58
|
if (!value || typeof value !== "object" || (value.status !== "completed" && value.status !== "wait"))
|
|
29
59
|
throw new Error("continuation adapter must return status completed or wait");
|
|
@@ -6,6 +6,11 @@ title: Migrations
|
|
|
6
6
|
|
|
7
7
|
## Unreleased
|
|
8
8
|
|
|
9
|
+
- Gate-action envelopes now use `schema_version: "2.0"`. Each entry in
|
|
10
|
+
`gate.requirements` includes its originating `gate_id`, allowing consumers to
|
|
11
|
+
verify accepted exceptions without private Flow-definition knowledge. There
|
|
12
|
+
is no legacy envelope fallback; adapters that validate or project the public
|
|
13
|
+
envelope must require version 2.0 and the new binding.
|
|
9
14
|
- Workflow runtime artifacts now live under `.kontourai/flow-agents/` instead
|
|
10
15
|
of earlier runtime roots such as `.flow-agents/` or `.agents/flow-agents/`.
|
|
11
16
|
Move any local session directories, sidecars, or `current.json` pointers you
|
package/dist/base/install.sh
CHANGED
|
@@ -72,7 +72,7 @@ if [[ ! -e "$DEST/AGENTS.md" && ! -L "$DEST/AGENTS.md" ]]; then
|
|
|
72
72
|
rm -f "$instruction_tmp"
|
|
73
73
|
fi
|
|
74
74
|
if command -v node >/dev/null 2>&1; then
|
|
75
|
-
node "$DEST/scripts/install-merge.js" --stamp-only --version "3.12.
|
|
75
|
+
node "$DEST/scripts/install-merge.js" --stamp-only --version "3.12.1" --install-record "$DEST/.flow-agents/install.json" --runtime "base" || true
|
|
76
76
|
fi
|
|
77
77
|
if [[ ${#CONSOLE_CONFIG_ARGS[@]} -gt 0 || -n "${FLOW_AGENTS_TELEMETRY_SINK:-}" || -n "${FLOW_AGENTS_TELEMETRY_SINKS:-}" || -n "${FLOW_AGENTS_CONSOLE_URL:-}" || -n "${CONSOLE_TELEMETRY_URL:-}" || -n "${CONSOLE_URL:-}" || -n "${FLOW_AGENTS_CONSOLE_TOKEN_FILE:-}" || -n "${CONSOLE_TELEMETRY_TOKEN_FILE:-}" ]]; then
|
|
78
78
|
bash "$DEST/scripts/telemetry/install-console-config.sh" "$DEST/scripts/telemetry/telemetry.conf" "${CONSOLE_CONFIG_ARGS[@]}"
|
|
@@ -35,7 +35,7 @@ export type GateActionPublicMutation = GateActionWorkflowMutation | {
|
|
|
35
35
|
};
|
|
36
36
|
};
|
|
37
37
|
export type GateActionEnvelope = {
|
|
38
|
-
schema_version: "
|
|
38
|
+
schema_version: "2.0";
|
|
39
39
|
flow: {
|
|
40
40
|
run_id: string;
|
|
41
41
|
definition_id: string;
|
|
@@ -47,6 +47,7 @@ export type GateActionEnvelope = {
|
|
|
47
47
|
gate: {
|
|
48
48
|
requirements: Array<{
|
|
49
49
|
id: string;
|
|
50
|
+
gate_id: string;
|
|
50
51
|
required: boolean;
|
|
51
52
|
description: string;
|
|
52
53
|
claim_type: string;
|
|
@@ -70,7 +70,7 @@ function deriveFlowRequirements(input, action) {
|
|
|
70
70
|
const matched = new Set((Array.isArray(outcome.matched_expectations) ? outcome.matched_expectations : [])
|
|
71
71
|
.flatMap((entry) => isRecord(entry) && typeof entry.expectation_id === "string" ? [entry.expectation_id] : []));
|
|
72
72
|
return expectationsForGate(gate, input.run.config)
|
|
73
|
-
.map((expectation) => requirementFromExpectation(expectation, matched, typeof outcome.accepted_exception_id === "string"));
|
|
73
|
+
.map((expectation) => requirementFromExpectation(gate.id, expectation, matched, typeof outcome.accepted_exception_id === "string"));
|
|
74
74
|
});
|
|
75
75
|
if (requirements.length > MAX_REQUIREMENTS)
|
|
76
76
|
throw new Error("Builder gate-action envelope requirements exceed the supported bound");
|
|
@@ -91,7 +91,7 @@ function assembleGateActionEnvelope(input, loaded, derived) {
|
|
|
91
91
|
.filter((binding) => !isControlArtifact(binding.artifact) && binding.expectation_ids.some((id) => unresolvedRequired.includes(id)))
|
|
92
92
|
.map((binding) => binding.artifact);
|
|
93
93
|
const envelope = {
|
|
94
|
-
schema_version: "
|
|
94
|
+
schema_version: "2.0",
|
|
95
95
|
flow: {
|
|
96
96
|
run_id: input.run.runId,
|
|
97
97
|
definition_id: input.run.definitionId,
|
|
@@ -215,7 +215,7 @@ function workflowEvidenceMutation(expectationId, sessionArgument, packageVersion
|
|
|
215
215
|
],
|
|
216
216
|
};
|
|
217
217
|
}
|
|
218
|
-
function requirementFromExpectation(expectation, satisfied, acceptedException) {
|
|
218
|
+
function requirementFromExpectation(gateId, expectation, satisfied, acceptedException) {
|
|
219
219
|
const record = expectation;
|
|
220
220
|
const claim = isRecord(record.bundle_claim) ? record.bundle_claim : null;
|
|
221
221
|
if (typeof record.id !== "string" || typeof record.description !== "string" || !claim || typeof claim.claimType !== "string") {
|
|
@@ -223,6 +223,7 @@ function requirementFromExpectation(expectation, satisfied, acceptedException) {
|
|
|
223
223
|
}
|
|
224
224
|
return {
|
|
225
225
|
id: record.id,
|
|
226
|
+
gate_id: gateId,
|
|
226
227
|
required: record.required === true,
|
|
227
228
|
description: record.description,
|
|
228
229
|
claim_type: claim.claimType,
|
|
@@ -18,12 +18,42 @@ export function validateSnapshot(value) {
|
|
|
18
18
|
return structuredClone(value);
|
|
19
19
|
}
|
|
20
20
|
function validateGateActionEnvelope(value) {
|
|
21
|
-
if (!value || typeof value !== "object" || value.schema_version !== "
|
|
21
|
+
if (!value || typeof value !== "object" || value.schema_version !== "2.0" || !value.flow || typeof value.flow.current_step !== "string"
|
|
22
22
|
|| !value.progress || !Array.isArray(value.progress.canonical_evidence) || !Array.isArray(value.progress.observed_artifacts))
|
|
23
23
|
throw new Error("continuation snapshot gate-action envelope is malformed");
|
|
24
|
+
if (value.gate !== undefined)
|
|
25
|
+
validateGateRequirementBindings(value);
|
|
24
26
|
if (Buffer.byteLength(JSON.stringify(value), "utf8") > 65_536)
|
|
25
27
|
throw new Error("continuation snapshot gate-action envelope exceeds 65536 bytes");
|
|
26
28
|
}
|
|
29
|
+
function validateGateRequirementBindings(value) {
|
|
30
|
+
if (!Array.isArray(value.flow.gate_ids) || !Array.isArray(value.gate.requirements) || !Array.isArray(value.gate.accepted_exceptions)) {
|
|
31
|
+
throw new Error("continuation snapshot gate-action envelope has malformed gate bindings");
|
|
32
|
+
}
|
|
33
|
+
const activeGates = new Set(value.flow.gate_ids);
|
|
34
|
+
const acceptedGates = new Set();
|
|
35
|
+
for (const exception of value.gate.accepted_exceptions) {
|
|
36
|
+
if (!exception || typeof exception.gate_id !== "string" || typeof exception.exception_id !== "string"
|
|
37
|
+
|| !activeGates.has(exception.gate_id) || acceptedGates.has(exception.gate_id)) {
|
|
38
|
+
throw new Error("continuation snapshot gate-action envelope has invalid accepted exceptions");
|
|
39
|
+
}
|
|
40
|
+
acceptedGates.add(exception.gate_id);
|
|
41
|
+
}
|
|
42
|
+
const gatesWithRequirements = new Set();
|
|
43
|
+
for (const requirement of value.gate.requirements) {
|
|
44
|
+
if (!requirement || typeof requirement.gate_id !== "string" || !activeGates.has(requirement.gate_id)) {
|
|
45
|
+
throw new Error("continuation snapshot gate-action envelope has an invalid requirement gate binding");
|
|
46
|
+
}
|
|
47
|
+
gatesWithRequirements.add(requirement.gate_id);
|
|
48
|
+
if ((requirement.status === "accepted_exception") !== acceptedGates.has(requirement.gate_id)
|
|
49
|
+
&& requirement.status !== "satisfied") {
|
|
50
|
+
throw new Error("continuation snapshot gate-action envelope has inconsistent exception bindings");
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if ([...acceptedGates].some((gateId) => !gatesWithRequirements.has(gateId))) {
|
|
54
|
+
throw new Error("continuation snapshot gate-action envelope exception has no bound requirements");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
27
57
|
export function validateTurnResult(value) {
|
|
28
58
|
if (!value || typeof value !== "object" || (value.status !== "completed" && value.status !== "wait"))
|
|
29
59
|
throw new Error("continuation adapter must return status completed or wait");
|
|
@@ -6,6 +6,11 @@ title: Migrations
|
|
|
6
6
|
|
|
7
7
|
## Unreleased
|
|
8
8
|
|
|
9
|
+
- Gate-action envelopes now use `schema_version: "2.0"`. Each entry in
|
|
10
|
+
`gate.requirements` includes its originating `gate_id`, allowing consumers to
|
|
11
|
+
verify accepted exceptions without private Flow-definition knowledge. There
|
|
12
|
+
is no legacy envelope fallback; adapters that validate or project the public
|
|
13
|
+
envelope must require version 2.0 and the new binding.
|
|
9
14
|
- Workflow runtime artifacts now live under `.kontourai/flow-agents/` instead
|
|
10
15
|
of earlier runtime roots such as `.flow-agents/` or `.agents/flow-agents/`.
|
|
11
16
|
Move any local session directories, sidecars, or `current.json` pointers you
|
|
@@ -72,7 +72,7 @@ if [[ ! -e "$DEST/CLAUDE.md" && ! -L "$DEST/CLAUDE.md" ]]; then
|
|
|
72
72
|
rm -f "$instruction_tmp"
|
|
73
73
|
fi
|
|
74
74
|
if command -v node >/dev/null 2>&1; then
|
|
75
|
-
node "$DEST/scripts/install-merge.js" --config "$DEST/.claude/settings.json" --managed-hooks "$SRC/.claude/settings.json" --version "3.12.
|
|
75
|
+
node "$DEST/scripts/install-merge.js" --config "$DEST/.claude/settings.json" --managed-hooks "$SRC/.claude/settings.json" --version "3.12.1" --install-record "$DEST/.flow-agents/install.json" --runtime "claude-code" || true
|
|
76
76
|
fi
|
|
77
77
|
if [[ ${#CONSOLE_CONFIG_ARGS[@]} -gt 0 || -n "${FLOW_AGENTS_TELEMETRY_SINK:-}" || -n "${FLOW_AGENTS_TELEMETRY_SINKS:-}" || -n "${FLOW_AGENTS_CONSOLE_URL:-}" || -n "${CONSOLE_TELEMETRY_URL:-}" || -n "${CONSOLE_URL:-}" || -n "${FLOW_AGENTS_CONSOLE_TOKEN_FILE:-}" || -n "${CONSOLE_TELEMETRY_TOKEN_FILE:-}" ]]; then
|
|
78
78
|
bash "$DEST/scripts/telemetry/install-console-config.sh" "$DEST/scripts/telemetry/telemetry.conf" "${CONSOLE_CONFIG_ARGS[@]}"
|
|
@@ -35,7 +35,7 @@ export type GateActionPublicMutation = GateActionWorkflowMutation | {
|
|
|
35
35
|
};
|
|
36
36
|
};
|
|
37
37
|
export type GateActionEnvelope = {
|
|
38
|
-
schema_version: "
|
|
38
|
+
schema_version: "2.0";
|
|
39
39
|
flow: {
|
|
40
40
|
run_id: string;
|
|
41
41
|
definition_id: string;
|
|
@@ -47,6 +47,7 @@ export type GateActionEnvelope = {
|
|
|
47
47
|
gate: {
|
|
48
48
|
requirements: Array<{
|
|
49
49
|
id: string;
|
|
50
|
+
gate_id: string;
|
|
50
51
|
required: boolean;
|
|
51
52
|
description: string;
|
|
52
53
|
claim_type: string;
|
|
@@ -70,7 +70,7 @@ function deriveFlowRequirements(input, action) {
|
|
|
70
70
|
const matched = new Set((Array.isArray(outcome.matched_expectations) ? outcome.matched_expectations : [])
|
|
71
71
|
.flatMap((entry) => isRecord(entry) && typeof entry.expectation_id === "string" ? [entry.expectation_id] : []));
|
|
72
72
|
return expectationsForGate(gate, input.run.config)
|
|
73
|
-
.map((expectation) => requirementFromExpectation(expectation, matched, typeof outcome.accepted_exception_id === "string"));
|
|
73
|
+
.map((expectation) => requirementFromExpectation(gate.id, expectation, matched, typeof outcome.accepted_exception_id === "string"));
|
|
74
74
|
});
|
|
75
75
|
if (requirements.length > MAX_REQUIREMENTS)
|
|
76
76
|
throw new Error("Builder gate-action envelope requirements exceed the supported bound");
|
|
@@ -91,7 +91,7 @@ function assembleGateActionEnvelope(input, loaded, derived) {
|
|
|
91
91
|
.filter((binding) => !isControlArtifact(binding.artifact) && binding.expectation_ids.some((id) => unresolvedRequired.includes(id)))
|
|
92
92
|
.map((binding) => binding.artifact);
|
|
93
93
|
const envelope = {
|
|
94
|
-
schema_version: "
|
|
94
|
+
schema_version: "2.0",
|
|
95
95
|
flow: {
|
|
96
96
|
run_id: input.run.runId,
|
|
97
97
|
definition_id: input.run.definitionId,
|
|
@@ -215,7 +215,7 @@ function workflowEvidenceMutation(expectationId, sessionArgument, packageVersion
|
|
|
215
215
|
],
|
|
216
216
|
};
|
|
217
217
|
}
|
|
218
|
-
function requirementFromExpectation(expectation, satisfied, acceptedException) {
|
|
218
|
+
function requirementFromExpectation(gateId, expectation, satisfied, acceptedException) {
|
|
219
219
|
const record = expectation;
|
|
220
220
|
const claim = isRecord(record.bundle_claim) ? record.bundle_claim : null;
|
|
221
221
|
if (typeof record.id !== "string" || typeof record.description !== "string" || !claim || typeof claim.claimType !== "string") {
|
|
@@ -223,6 +223,7 @@ function requirementFromExpectation(expectation, satisfied, acceptedException) {
|
|
|
223
223
|
}
|
|
224
224
|
return {
|
|
225
225
|
id: record.id,
|
|
226
|
+
gate_id: gateId,
|
|
226
227
|
required: record.required === true,
|
|
227
228
|
description: record.description,
|
|
228
229
|
claim_type: claim.claimType,
|
|
@@ -18,12 +18,42 @@ export function validateSnapshot(value) {
|
|
|
18
18
|
return structuredClone(value);
|
|
19
19
|
}
|
|
20
20
|
function validateGateActionEnvelope(value) {
|
|
21
|
-
if (!value || typeof value !== "object" || value.schema_version !== "
|
|
21
|
+
if (!value || typeof value !== "object" || value.schema_version !== "2.0" || !value.flow || typeof value.flow.current_step !== "string"
|
|
22
22
|
|| !value.progress || !Array.isArray(value.progress.canonical_evidence) || !Array.isArray(value.progress.observed_artifacts))
|
|
23
23
|
throw new Error("continuation snapshot gate-action envelope is malformed");
|
|
24
|
+
if (value.gate !== undefined)
|
|
25
|
+
validateGateRequirementBindings(value);
|
|
24
26
|
if (Buffer.byteLength(JSON.stringify(value), "utf8") > 65_536)
|
|
25
27
|
throw new Error("continuation snapshot gate-action envelope exceeds 65536 bytes");
|
|
26
28
|
}
|
|
29
|
+
function validateGateRequirementBindings(value) {
|
|
30
|
+
if (!Array.isArray(value.flow.gate_ids) || !Array.isArray(value.gate.requirements) || !Array.isArray(value.gate.accepted_exceptions)) {
|
|
31
|
+
throw new Error("continuation snapshot gate-action envelope has malformed gate bindings");
|
|
32
|
+
}
|
|
33
|
+
const activeGates = new Set(value.flow.gate_ids);
|
|
34
|
+
const acceptedGates = new Set();
|
|
35
|
+
for (const exception of value.gate.accepted_exceptions) {
|
|
36
|
+
if (!exception || typeof exception.gate_id !== "string" || typeof exception.exception_id !== "string"
|
|
37
|
+
|| !activeGates.has(exception.gate_id) || acceptedGates.has(exception.gate_id)) {
|
|
38
|
+
throw new Error("continuation snapshot gate-action envelope has invalid accepted exceptions");
|
|
39
|
+
}
|
|
40
|
+
acceptedGates.add(exception.gate_id);
|
|
41
|
+
}
|
|
42
|
+
const gatesWithRequirements = new Set();
|
|
43
|
+
for (const requirement of value.gate.requirements) {
|
|
44
|
+
if (!requirement || typeof requirement.gate_id !== "string" || !activeGates.has(requirement.gate_id)) {
|
|
45
|
+
throw new Error("continuation snapshot gate-action envelope has an invalid requirement gate binding");
|
|
46
|
+
}
|
|
47
|
+
gatesWithRequirements.add(requirement.gate_id);
|
|
48
|
+
if ((requirement.status === "accepted_exception") !== acceptedGates.has(requirement.gate_id)
|
|
49
|
+
&& requirement.status !== "satisfied") {
|
|
50
|
+
throw new Error("continuation snapshot gate-action envelope has inconsistent exception bindings");
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if ([...acceptedGates].some((gateId) => !gatesWithRequirements.has(gateId))) {
|
|
54
|
+
throw new Error("continuation snapshot gate-action envelope exception has no bound requirements");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
27
57
|
export function validateTurnResult(value) {
|
|
28
58
|
if (!value || typeof value !== "object" || (value.status !== "completed" && value.status !== "wait"))
|
|
29
59
|
throw new Error("continuation adapter must return status completed or wait");
|
|
@@ -6,6 +6,11 @@ title: Migrations
|
|
|
6
6
|
|
|
7
7
|
## Unreleased
|
|
8
8
|
|
|
9
|
+
- Gate-action envelopes now use `schema_version: "2.0"`. Each entry in
|
|
10
|
+
`gate.requirements` includes its originating `gate_id`, allowing consumers to
|
|
11
|
+
verify accepted exceptions without private Flow-definition knowledge. There
|
|
12
|
+
is no legacy envelope fallback; adapters that validate or project the public
|
|
13
|
+
envelope must require version 2.0 and the new binding.
|
|
9
14
|
- Workflow runtime artifacts now live under `.kontourai/flow-agents/` instead
|
|
10
15
|
of earlier runtime roots such as `.flow-agents/` or `.agents/flow-agents/`.
|
|
11
16
|
Move any local session directories, sidecars, or `current.json` pointers you
|
package/dist/codex/install.sh
CHANGED
|
@@ -60,7 +60,7 @@ SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
60
60
|
mkdir -p "$DEST"
|
|
61
61
|
rsync -a --exclude='/AGENTS.md' --exclude='/CLAUDE.md' --exclude=".codex/hooks.json" "$SRC"/ "$DEST"/
|
|
62
62
|
if command -v node >/dev/null 2>&1; then
|
|
63
|
-
node "$DEST/scripts/install-merge.js" --config "$DEST/.codex/hooks.json" --managed-hooks "$SRC/.codex/hooks.json" --version "3.12.
|
|
63
|
+
node "$DEST/scripts/install-merge.js" --config "$DEST/.codex/hooks.json" --managed-hooks "$SRC/.codex/hooks.json" --version "3.12.1" --install-record "$DEST/.flow-agents/install.json" --runtime "codex" || true
|
|
64
64
|
fi
|
|
65
65
|
if [[ ${#CONSOLE_CONFIG_ARGS[@]} -gt 0 || -n "${FLOW_AGENTS_TELEMETRY_SINK:-}" || -n "${FLOW_AGENTS_TELEMETRY_SINKS:-}" || -n "${FLOW_AGENTS_CONSOLE_URL:-}" || -n "${CONSOLE_TELEMETRY_URL:-}" || -n "${CONSOLE_URL:-}" || -n "${FLOW_AGENTS_CONSOLE_TOKEN_FILE:-}" || -n "${CONSOLE_TELEMETRY_TOKEN_FILE:-}" ]]; then
|
|
66
66
|
bash "$DEST/scripts/telemetry/install-console-config.sh" "$DEST/scripts/telemetry/telemetry.conf" "${CONSOLE_CONFIG_ARGS[@]}"
|
|
@@ -35,7 +35,7 @@ export type GateActionPublicMutation = GateActionWorkflowMutation | {
|
|
|
35
35
|
};
|
|
36
36
|
};
|
|
37
37
|
export type GateActionEnvelope = {
|
|
38
|
-
schema_version: "
|
|
38
|
+
schema_version: "2.0";
|
|
39
39
|
flow: {
|
|
40
40
|
run_id: string;
|
|
41
41
|
definition_id: string;
|
|
@@ -47,6 +47,7 @@ export type GateActionEnvelope = {
|
|
|
47
47
|
gate: {
|
|
48
48
|
requirements: Array<{
|
|
49
49
|
id: string;
|
|
50
|
+
gate_id: string;
|
|
50
51
|
required: boolean;
|
|
51
52
|
description: string;
|
|
52
53
|
claim_type: string;
|
|
@@ -70,7 +70,7 @@ function deriveFlowRequirements(input, action) {
|
|
|
70
70
|
const matched = new Set((Array.isArray(outcome.matched_expectations) ? outcome.matched_expectations : [])
|
|
71
71
|
.flatMap((entry) => isRecord(entry) && typeof entry.expectation_id === "string" ? [entry.expectation_id] : []));
|
|
72
72
|
return expectationsForGate(gate, input.run.config)
|
|
73
|
-
.map((expectation) => requirementFromExpectation(expectation, matched, typeof outcome.accepted_exception_id === "string"));
|
|
73
|
+
.map((expectation) => requirementFromExpectation(gate.id, expectation, matched, typeof outcome.accepted_exception_id === "string"));
|
|
74
74
|
});
|
|
75
75
|
if (requirements.length > MAX_REQUIREMENTS)
|
|
76
76
|
throw new Error("Builder gate-action envelope requirements exceed the supported bound");
|
|
@@ -91,7 +91,7 @@ function assembleGateActionEnvelope(input, loaded, derived) {
|
|
|
91
91
|
.filter((binding) => !isControlArtifact(binding.artifact) && binding.expectation_ids.some((id) => unresolvedRequired.includes(id)))
|
|
92
92
|
.map((binding) => binding.artifact);
|
|
93
93
|
const envelope = {
|
|
94
|
-
schema_version: "
|
|
94
|
+
schema_version: "2.0",
|
|
95
95
|
flow: {
|
|
96
96
|
run_id: input.run.runId,
|
|
97
97
|
definition_id: input.run.definitionId,
|
|
@@ -215,7 +215,7 @@ function workflowEvidenceMutation(expectationId, sessionArgument, packageVersion
|
|
|
215
215
|
],
|
|
216
216
|
};
|
|
217
217
|
}
|
|
218
|
-
function requirementFromExpectation(expectation, satisfied, acceptedException) {
|
|
218
|
+
function requirementFromExpectation(gateId, expectation, satisfied, acceptedException) {
|
|
219
219
|
const record = expectation;
|
|
220
220
|
const claim = isRecord(record.bundle_claim) ? record.bundle_claim : null;
|
|
221
221
|
if (typeof record.id !== "string" || typeof record.description !== "string" || !claim || typeof claim.claimType !== "string") {
|
|
@@ -223,6 +223,7 @@ function requirementFromExpectation(expectation, satisfied, acceptedException) {
|
|
|
223
223
|
}
|
|
224
224
|
return {
|
|
225
225
|
id: record.id,
|
|
226
|
+
gate_id: gateId,
|
|
226
227
|
required: record.required === true,
|
|
227
228
|
description: record.description,
|
|
228
229
|
claim_type: claim.claimType,
|
|
@@ -18,12 +18,42 @@ export function validateSnapshot(value) {
|
|
|
18
18
|
return structuredClone(value);
|
|
19
19
|
}
|
|
20
20
|
function validateGateActionEnvelope(value) {
|
|
21
|
-
if (!value || typeof value !== "object" || value.schema_version !== "
|
|
21
|
+
if (!value || typeof value !== "object" || value.schema_version !== "2.0" || !value.flow || typeof value.flow.current_step !== "string"
|
|
22
22
|
|| !value.progress || !Array.isArray(value.progress.canonical_evidence) || !Array.isArray(value.progress.observed_artifacts))
|
|
23
23
|
throw new Error("continuation snapshot gate-action envelope is malformed");
|
|
24
|
+
if (value.gate !== undefined)
|
|
25
|
+
validateGateRequirementBindings(value);
|
|
24
26
|
if (Buffer.byteLength(JSON.stringify(value), "utf8") > 65_536)
|
|
25
27
|
throw new Error("continuation snapshot gate-action envelope exceeds 65536 bytes");
|
|
26
28
|
}
|
|
29
|
+
function validateGateRequirementBindings(value) {
|
|
30
|
+
if (!Array.isArray(value.flow.gate_ids) || !Array.isArray(value.gate.requirements) || !Array.isArray(value.gate.accepted_exceptions)) {
|
|
31
|
+
throw new Error("continuation snapshot gate-action envelope has malformed gate bindings");
|
|
32
|
+
}
|
|
33
|
+
const activeGates = new Set(value.flow.gate_ids);
|
|
34
|
+
const acceptedGates = new Set();
|
|
35
|
+
for (const exception of value.gate.accepted_exceptions) {
|
|
36
|
+
if (!exception || typeof exception.gate_id !== "string" || typeof exception.exception_id !== "string"
|
|
37
|
+
|| !activeGates.has(exception.gate_id) || acceptedGates.has(exception.gate_id)) {
|
|
38
|
+
throw new Error("continuation snapshot gate-action envelope has invalid accepted exceptions");
|
|
39
|
+
}
|
|
40
|
+
acceptedGates.add(exception.gate_id);
|
|
41
|
+
}
|
|
42
|
+
const gatesWithRequirements = new Set();
|
|
43
|
+
for (const requirement of value.gate.requirements) {
|
|
44
|
+
if (!requirement || typeof requirement.gate_id !== "string" || !activeGates.has(requirement.gate_id)) {
|
|
45
|
+
throw new Error("continuation snapshot gate-action envelope has an invalid requirement gate binding");
|
|
46
|
+
}
|
|
47
|
+
gatesWithRequirements.add(requirement.gate_id);
|
|
48
|
+
if ((requirement.status === "accepted_exception") !== acceptedGates.has(requirement.gate_id)
|
|
49
|
+
&& requirement.status !== "satisfied") {
|
|
50
|
+
throw new Error("continuation snapshot gate-action envelope has inconsistent exception bindings");
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if ([...acceptedGates].some((gateId) => !gatesWithRequirements.has(gateId))) {
|
|
54
|
+
throw new Error("continuation snapshot gate-action envelope exception has no bound requirements");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
27
57
|
export function validateTurnResult(value) {
|
|
28
58
|
if (!value || typeof value !== "object" || (value.status !== "completed" && value.status !== "wait"))
|
|
29
59
|
throw new Error("continuation adapter must return status completed or wait");
|
|
@@ -6,6 +6,11 @@ title: Migrations
|
|
|
6
6
|
|
|
7
7
|
## Unreleased
|
|
8
8
|
|
|
9
|
+
- Gate-action envelopes now use `schema_version: "2.0"`. Each entry in
|
|
10
|
+
`gate.requirements` includes its originating `gate_id`, allowing consumers to
|
|
11
|
+
verify accepted exceptions without private Flow-definition knowledge. There
|
|
12
|
+
is no legacy envelope fallback; adapters that validate or project the public
|
|
13
|
+
envelope must require version 2.0 and the new binding.
|
|
9
14
|
- Workflow runtime artifacts now live under `.kontourai/flow-agents/` instead
|
|
10
15
|
of earlier runtime roots such as `.flow-agents/` or `.agents/flow-agents/`.
|
|
11
16
|
Move any local session directories, sidecars, or `current.json` pointers you
|
package/dist/kiro/install.sh
CHANGED
|
@@ -73,7 +73,7 @@ fi
|
|
|
73
73
|
export DEST
|
|
74
74
|
find "$DEST" \( -path "$DEST/AGENTS.md" -o -path "$DEST/CLAUDE.md" \) -prune -o -type f \( -name '*.json' -o -name '*.md' -o -name '*.sh' -o -name '*.js' -o -name '*.ts' -o -name '*.yaml' -o -name '*.yml' \) -print0 | xargs -0 perl -0pi -e 's#__KIRO_PACKAGE_ROOT__#$ENV{DEST}#g'
|
|
75
75
|
if command -v node >/dev/null 2>&1; then
|
|
76
|
-
node "$DEST/scripts/install-merge.js" --stamp-only --version "3.12.
|
|
76
|
+
node "$DEST/scripts/install-merge.js" --stamp-only --version "3.12.1" --install-record "$DEST/.flow-agents/install.json" --runtime "kiro" || true
|
|
77
77
|
fi
|
|
78
78
|
if [[ ${#CONSOLE_CONFIG_ARGS[@]} -gt 0 || -n "${FLOW_AGENTS_TELEMETRY_SINK:-}" || -n "${FLOW_AGENTS_TELEMETRY_SINKS:-}" || -n "${FLOW_AGENTS_CONSOLE_URL:-}" || -n "${CONSOLE_TELEMETRY_URL:-}" || -n "${CONSOLE_URL:-}" || -n "${FLOW_AGENTS_CONSOLE_TOKEN_FILE:-}" || -n "${CONSOLE_TELEMETRY_TOKEN_FILE:-}" ]]; then
|
|
79
79
|
bash "$DEST/scripts/telemetry/install-console-config.sh" "$DEST/scripts/telemetry/telemetry.conf" "${CONSOLE_CONFIG_ARGS[@]}"
|
|
@@ -35,7 +35,7 @@ export type GateActionPublicMutation = GateActionWorkflowMutation | {
|
|
|
35
35
|
};
|
|
36
36
|
};
|
|
37
37
|
export type GateActionEnvelope = {
|
|
38
|
-
schema_version: "
|
|
38
|
+
schema_version: "2.0";
|
|
39
39
|
flow: {
|
|
40
40
|
run_id: string;
|
|
41
41
|
definition_id: string;
|
|
@@ -47,6 +47,7 @@ export type GateActionEnvelope = {
|
|
|
47
47
|
gate: {
|
|
48
48
|
requirements: Array<{
|
|
49
49
|
id: string;
|
|
50
|
+
gate_id: string;
|
|
50
51
|
required: boolean;
|
|
51
52
|
description: string;
|
|
52
53
|
claim_type: string;
|
|
@@ -70,7 +70,7 @@ function deriveFlowRequirements(input, action) {
|
|
|
70
70
|
const matched = new Set((Array.isArray(outcome.matched_expectations) ? outcome.matched_expectations : [])
|
|
71
71
|
.flatMap((entry) => isRecord(entry) && typeof entry.expectation_id === "string" ? [entry.expectation_id] : []));
|
|
72
72
|
return expectationsForGate(gate, input.run.config)
|
|
73
|
-
.map((expectation) => requirementFromExpectation(expectation, matched, typeof outcome.accepted_exception_id === "string"));
|
|
73
|
+
.map((expectation) => requirementFromExpectation(gate.id, expectation, matched, typeof outcome.accepted_exception_id === "string"));
|
|
74
74
|
});
|
|
75
75
|
if (requirements.length > MAX_REQUIREMENTS)
|
|
76
76
|
throw new Error("Builder gate-action envelope requirements exceed the supported bound");
|
|
@@ -91,7 +91,7 @@ function assembleGateActionEnvelope(input, loaded, derived) {
|
|
|
91
91
|
.filter((binding) => !isControlArtifact(binding.artifact) && binding.expectation_ids.some((id) => unresolvedRequired.includes(id)))
|
|
92
92
|
.map((binding) => binding.artifact);
|
|
93
93
|
const envelope = {
|
|
94
|
-
schema_version: "
|
|
94
|
+
schema_version: "2.0",
|
|
95
95
|
flow: {
|
|
96
96
|
run_id: input.run.runId,
|
|
97
97
|
definition_id: input.run.definitionId,
|
|
@@ -215,7 +215,7 @@ function workflowEvidenceMutation(expectationId, sessionArgument, packageVersion
|
|
|
215
215
|
],
|
|
216
216
|
};
|
|
217
217
|
}
|
|
218
|
-
function requirementFromExpectation(expectation, satisfied, acceptedException) {
|
|
218
|
+
function requirementFromExpectation(gateId, expectation, satisfied, acceptedException) {
|
|
219
219
|
const record = expectation;
|
|
220
220
|
const claim = isRecord(record.bundle_claim) ? record.bundle_claim : null;
|
|
221
221
|
if (typeof record.id !== "string" || typeof record.description !== "string" || !claim || typeof claim.claimType !== "string") {
|
|
@@ -223,6 +223,7 @@ function requirementFromExpectation(expectation, satisfied, acceptedException) {
|
|
|
223
223
|
}
|
|
224
224
|
return {
|
|
225
225
|
id: record.id,
|
|
226
|
+
gate_id: gateId,
|
|
226
227
|
required: record.required === true,
|
|
227
228
|
description: record.description,
|
|
228
229
|
claim_type: claim.claimType,
|
|
@@ -18,12 +18,42 @@ export function validateSnapshot(value) {
|
|
|
18
18
|
return structuredClone(value);
|
|
19
19
|
}
|
|
20
20
|
function validateGateActionEnvelope(value) {
|
|
21
|
-
if (!value || typeof value !== "object" || value.schema_version !== "
|
|
21
|
+
if (!value || typeof value !== "object" || value.schema_version !== "2.0" || !value.flow || typeof value.flow.current_step !== "string"
|
|
22
22
|
|| !value.progress || !Array.isArray(value.progress.canonical_evidence) || !Array.isArray(value.progress.observed_artifacts))
|
|
23
23
|
throw new Error("continuation snapshot gate-action envelope is malformed");
|
|
24
|
+
if (value.gate !== undefined)
|
|
25
|
+
validateGateRequirementBindings(value);
|
|
24
26
|
if (Buffer.byteLength(JSON.stringify(value), "utf8") > 65_536)
|
|
25
27
|
throw new Error("continuation snapshot gate-action envelope exceeds 65536 bytes");
|
|
26
28
|
}
|
|
29
|
+
function validateGateRequirementBindings(value) {
|
|
30
|
+
if (!Array.isArray(value.flow.gate_ids) || !Array.isArray(value.gate.requirements) || !Array.isArray(value.gate.accepted_exceptions)) {
|
|
31
|
+
throw new Error("continuation snapshot gate-action envelope has malformed gate bindings");
|
|
32
|
+
}
|
|
33
|
+
const activeGates = new Set(value.flow.gate_ids);
|
|
34
|
+
const acceptedGates = new Set();
|
|
35
|
+
for (const exception of value.gate.accepted_exceptions) {
|
|
36
|
+
if (!exception || typeof exception.gate_id !== "string" || typeof exception.exception_id !== "string"
|
|
37
|
+
|| !activeGates.has(exception.gate_id) || acceptedGates.has(exception.gate_id)) {
|
|
38
|
+
throw new Error("continuation snapshot gate-action envelope has invalid accepted exceptions");
|
|
39
|
+
}
|
|
40
|
+
acceptedGates.add(exception.gate_id);
|
|
41
|
+
}
|
|
42
|
+
const gatesWithRequirements = new Set();
|
|
43
|
+
for (const requirement of value.gate.requirements) {
|
|
44
|
+
if (!requirement || typeof requirement.gate_id !== "string" || !activeGates.has(requirement.gate_id)) {
|
|
45
|
+
throw new Error("continuation snapshot gate-action envelope has an invalid requirement gate binding");
|
|
46
|
+
}
|
|
47
|
+
gatesWithRequirements.add(requirement.gate_id);
|
|
48
|
+
if ((requirement.status === "accepted_exception") !== acceptedGates.has(requirement.gate_id)
|
|
49
|
+
&& requirement.status !== "satisfied") {
|
|
50
|
+
throw new Error("continuation snapshot gate-action envelope has inconsistent exception bindings");
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if ([...acceptedGates].some((gateId) => !gatesWithRequirements.has(gateId))) {
|
|
54
|
+
throw new Error("continuation snapshot gate-action envelope exception has no bound requirements");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
27
57
|
export function validateTurnResult(value) {
|
|
28
58
|
if (!value || typeof value !== "object" || (value.status !== "completed" && value.status !== "wait"))
|
|
29
59
|
throw new Error("continuation adapter must return status completed or wait");
|
|
@@ -6,6 +6,11 @@ title: Migrations
|
|
|
6
6
|
|
|
7
7
|
## Unreleased
|
|
8
8
|
|
|
9
|
+
- Gate-action envelopes now use `schema_version: "2.0"`. Each entry in
|
|
10
|
+
`gate.requirements` includes its originating `gate_id`, allowing consumers to
|
|
11
|
+
verify accepted exceptions without private Flow-definition knowledge. There
|
|
12
|
+
is no legacy envelope fallback; adapters that validate or project the public
|
|
13
|
+
envelope must require version 2.0 and the new binding.
|
|
9
14
|
- Workflow runtime artifacts now live under `.kontourai/flow-agents/` instead
|
|
10
15
|
of earlier runtime roots such as `.flow-agents/` or `.agents/flow-agents/`.
|
|
11
16
|
Move any local session directories, sidecars, or `current.json` pointers you
|
package/dist/opencode/install.sh
CHANGED
|
@@ -72,7 +72,7 @@ if [[ ! -e "$DEST/AGENTS.md" && ! -L "$DEST/AGENTS.md" ]]; then
|
|
|
72
72
|
rm -f "$instruction_tmp"
|
|
73
73
|
fi
|
|
74
74
|
if command -v node >/dev/null 2>&1; then
|
|
75
|
-
node "$DEST/scripts/install-merge.js" --config "$DEST/opencode.json" --managed-hooks "$SRC/opencode.json" --version "3.12.
|
|
75
|
+
node "$DEST/scripts/install-merge.js" --config "$DEST/opencode.json" --managed-hooks "$SRC/opencode.json" --version "3.12.1" --install-record "$DEST/.flow-agents/install.json" --runtime "opencode" || true
|
|
76
76
|
fi
|
|
77
77
|
if [[ ${#CONSOLE_CONFIG_ARGS[@]} -gt 0 || -n "${FLOW_AGENTS_TELEMETRY_SINK:-}" || -n "${FLOW_AGENTS_TELEMETRY_SINKS:-}" || -n "${FLOW_AGENTS_CONSOLE_URL:-}" || -n "${CONSOLE_TELEMETRY_URL:-}" || -n "${CONSOLE_URL:-}" || -n "${FLOW_AGENTS_CONSOLE_TOKEN_FILE:-}" || -n "${CONSOLE_TELEMETRY_TOKEN_FILE:-}" ]]; then
|
|
78
78
|
bash "$DEST/scripts/telemetry/install-console-config.sh" "$DEST/scripts/telemetry/telemetry.conf" "${CONSOLE_CONFIG_ARGS[@]}"
|
|
@@ -35,7 +35,7 @@ export type GateActionPublicMutation = GateActionWorkflowMutation | {
|
|
|
35
35
|
};
|
|
36
36
|
};
|
|
37
37
|
export type GateActionEnvelope = {
|
|
38
|
-
schema_version: "
|
|
38
|
+
schema_version: "2.0";
|
|
39
39
|
flow: {
|
|
40
40
|
run_id: string;
|
|
41
41
|
definition_id: string;
|
|
@@ -47,6 +47,7 @@ export type GateActionEnvelope = {
|
|
|
47
47
|
gate: {
|
|
48
48
|
requirements: Array<{
|
|
49
49
|
id: string;
|
|
50
|
+
gate_id: string;
|
|
50
51
|
required: boolean;
|
|
51
52
|
description: string;
|
|
52
53
|
claim_type: string;
|
|
@@ -70,7 +70,7 @@ function deriveFlowRequirements(input, action) {
|
|
|
70
70
|
const matched = new Set((Array.isArray(outcome.matched_expectations) ? outcome.matched_expectations : [])
|
|
71
71
|
.flatMap((entry) => isRecord(entry) && typeof entry.expectation_id === "string" ? [entry.expectation_id] : []));
|
|
72
72
|
return expectationsForGate(gate, input.run.config)
|
|
73
|
-
.map((expectation) => requirementFromExpectation(expectation, matched, typeof outcome.accepted_exception_id === "string"));
|
|
73
|
+
.map((expectation) => requirementFromExpectation(gate.id, expectation, matched, typeof outcome.accepted_exception_id === "string"));
|
|
74
74
|
});
|
|
75
75
|
if (requirements.length > MAX_REQUIREMENTS)
|
|
76
76
|
throw new Error("Builder gate-action envelope requirements exceed the supported bound");
|
|
@@ -91,7 +91,7 @@ function assembleGateActionEnvelope(input, loaded, derived) {
|
|
|
91
91
|
.filter((binding) => !isControlArtifact(binding.artifact) && binding.expectation_ids.some((id) => unresolvedRequired.includes(id)))
|
|
92
92
|
.map((binding) => binding.artifact);
|
|
93
93
|
const envelope = {
|
|
94
|
-
schema_version: "
|
|
94
|
+
schema_version: "2.0",
|
|
95
95
|
flow: {
|
|
96
96
|
run_id: input.run.runId,
|
|
97
97
|
definition_id: input.run.definitionId,
|
|
@@ -215,7 +215,7 @@ function workflowEvidenceMutation(expectationId, sessionArgument, packageVersion
|
|
|
215
215
|
],
|
|
216
216
|
};
|
|
217
217
|
}
|
|
218
|
-
function requirementFromExpectation(expectation, satisfied, acceptedException) {
|
|
218
|
+
function requirementFromExpectation(gateId, expectation, satisfied, acceptedException) {
|
|
219
219
|
const record = expectation;
|
|
220
220
|
const claim = isRecord(record.bundle_claim) ? record.bundle_claim : null;
|
|
221
221
|
if (typeof record.id !== "string" || typeof record.description !== "string" || !claim || typeof claim.claimType !== "string") {
|
|
@@ -223,6 +223,7 @@ function requirementFromExpectation(expectation, satisfied, acceptedException) {
|
|
|
223
223
|
}
|
|
224
224
|
return {
|
|
225
225
|
id: record.id,
|
|
226
|
+
gate_id: gateId,
|
|
226
227
|
required: record.required === true,
|
|
227
228
|
description: record.description,
|
|
228
229
|
claim_type: claim.claimType,
|
|
@@ -18,12 +18,42 @@ export function validateSnapshot(value) {
|
|
|
18
18
|
return structuredClone(value);
|
|
19
19
|
}
|
|
20
20
|
function validateGateActionEnvelope(value) {
|
|
21
|
-
if (!value || typeof value !== "object" || value.schema_version !== "
|
|
21
|
+
if (!value || typeof value !== "object" || value.schema_version !== "2.0" || !value.flow || typeof value.flow.current_step !== "string"
|
|
22
22
|
|| !value.progress || !Array.isArray(value.progress.canonical_evidence) || !Array.isArray(value.progress.observed_artifacts))
|
|
23
23
|
throw new Error("continuation snapshot gate-action envelope is malformed");
|
|
24
|
+
if (value.gate !== undefined)
|
|
25
|
+
validateGateRequirementBindings(value);
|
|
24
26
|
if (Buffer.byteLength(JSON.stringify(value), "utf8") > 65_536)
|
|
25
27
|
throw new Error("continuation snapshot gate-action envelope exceeds 65536 bytes");
|
|
26
28
|
}
|
|
29
|
+
function validateGateRequirementBindings(value) {
|
|
30
|
+
if (!Array.isArray(value.flow.gate_ids) || !Array.isArray(value.gate.requirements) || !Array.isArray(value.gate.accepted_exceptions)) {
|
|
31
|
+
throw new Error("continuation snapshot gate-action envelope has malformed gate bindings");
|
|
32
|
+
}
|
|
33
|
+
const activeGates = new Set(value.flow.gate_ids);
|
|
34
|
+
const acceptedGates = new Set();
|
|
35
|
+
for (const exception of value.gate.accepted_exceptions) {
|
|
36
|
+
if (!exception || typeof exception.gate_id !== "string" || typeof exception.exception_id !== "string"
|
|
37
|
+
|| !activeGates.has(exception.gate_id) || acceptedGates.has(exception.gate_id)) {
|
|
38
|
+
throw new Error("continuation snapshot gate-action envelope has invalid accepted exceptions");
|
|
39
|
+
}
|
|
40
|
+
acceptedGates.add(exception.gate_id);
|
|
41
|
+
}
|
|
42
|
+
const gatesWithRequirements = new Set();
|
|
43
|
+
for (const requirement of value.gate.requirements) {
|
|
44
|
+
if (!requirement || typeof requirement.gate_id !== "string" || !activeGates.has(requirement.gate_id)) {
|
|
45
|
+
throw new Error("continuation snapshot gate-action envelope has an invalid requirement gate binding");
|
|
46
|
+
}
|
|
47
|
+
gatesWithRequirements.add(requirement.gate_id);
|
|
48
|
+
if ((requirement.status === "accepted_exception") !== acceptedGates.has(requirement.gate_id)
|
|
49
|
+
&& requirement.status !== "satisfied") {
|
|
50
|
+
throw new Error("continuation snapshot gate-action envelope has inconsistent exception bindings");
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if ([...acceptedGates].some((gateId) => !gatesWithRequirements.has(gateId))) {
|
|
54
|
+
throw new Error("continuation snapshot gate-action envelope exception has no bound requirements");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
27
57
|
export function validateTurnResult(value) {
|
|
28
58
|
if (!value || typeof value !== "object" || (value.status !== "completed" && value.status !== "wait"))
|
|
29
59
|
throw new Error("continuation adapter must return status completed or wait");
|
|
@@ -6,6 +6,11 @@ title: Migrations
|
|
|
6
6
|
|
|
7
7
|
## Unreleased
|
|
8
8
|
|
|
9
|
+
- Gate-action envelopes now use `schema_version: "2.0"`. Each entry in
|
|
10
|
+
`gate.requirements` includes its originating `gate_id`, allowing consumers to
|
|
11
|
+
verify accepted exceptions without private Flow-definition knowledge. There
|
|
12
|
+
is no legacy envelope fallback; adapters that validate or project the public
|
|
13
|
+
envelope must require version 2.0 and the new binding.
|
|
9
14
|
- Workflow runtime artifacts now live under `.kontourai/flow-agents/` instead
|
|
10
15
|
of earlier runtime roots such as `.flow-agents/` or `.agents/flow-agents/`.
|
|
11
16
|
Move any local session directories, sidecars, or `current.json` pointers you
|
package/dist/pi/install.sh
CHANGED
|
@@ -72,7 +72,7 @@ if [[ ! -e "$DEST/AGENTS.md" && ! -L "$DEST/AGENTS.md" ]]; then
|
|
|
72
72
|
rm -f "$instruction_tmp"
|
|
73
73
|
fi
|
|
74
74
|
if command -v node >/dev/null 2>&1; then
|
|
75
|
-
node "$DEST/scripts/install-merge.js" --stamp-only --version "3.12.
|
|
75
|
+
node "$DEST/scripts/install-merge.js" --stamp-only --version "3.12.1" --install-record "$DEST/.flow-agents/install.json" --runtime "pi" || true
|
|
76
76
|
fi
|
|
77
77
|
if [[ ${#CONSOLE_CONFIG_ARGS[@]} -gt 0 || -n "${FLOW_AGENTS_TELEMETRY_SINK:-}" || -n "${FLOW_AGENTS_TELEMETRY_SINKS:-}" || -n "${FLOW_AGENTS_CONSOLE_URL:-}" || -n "${CONSOLE_TELEMETRY_URL:-}" || -n "${CONSOLE_URL:-}" || -n "${FLOW_AGENTS_CONSOLE_TOKEN_FILE:-}" || -n "${CONSOLE_TELEMETRY_TOKEN_FILE:-}" ]]; then
|
|
78
78
|
bash "$DEST/scripts/telemetry/install-console-config.sh" "$DEST/scripts/telemetry/telemetry.conf" "${CONSOLE_CONFIG_ARGS[@]}"
|
package/docs/migrations.md
CHANGED
|
@@ -6,6 +6,11 @@ title: Migrations
|
|
|
6
6
|
|
|
7
7
|
## Unreleased
|
|
8
8
|
|
|
9
|
+
- Gate-action envelopes now use `schema_version: "2.0"`. Each entry in
|
|
10
|
+
`gate.requirements` includes its originating `gate_id`, allowing consumers to
|
|
11
|
+
verify accepted exceptions without private Flow-definition knowledge. There
|
|
12
|
+
is no legacy envelope fallback; adapters that validate or project the public
|
|
13
|
+
envelope must require version 2.0 and the new binding.
|
|
9
14
|
- Workflow runtime artifacts now live under `.kontourai/flow-agents/` instead
|
|
10
15
|
of earlier runtime roots such as `.flow-agents/` or `.agents/flow-agents/`.
|
|
11
16
|
Move any local session directories, sidecars, or `current.json` pointers you
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kontourai/flow-agents",
|
|
3
|
-
"version": "3.12.
|
|
3
|
+
"version": "3.12.1",
|
|
4
4
|
"description": "Flow Agents — a Kontour product that applies Flow and Veritas discipline as a portable process layer inside the agent tools you already use: Claude Code, Codex, Kiro, opencode, pi, and GitHub Actions — with framework adapters (AWS Strands preview) on the same policy-engine contract.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agents",
|
|
@@ -53,7 +53,7 @@ export type GateActionPublicMutation =
|
|
|
53
53
|
};
|
|
54
54
|
|
|
55
55
|
export type GateActionEnvelope = {
|
|
56
|
-
schema_version: "
|
|
56
|
+
schema_version: "2.0";
|
|
57
57
|
flow: {
|
|
58
58
|
run_id: string;
|
|
59
59
|
definition_id: string;
|
|
@@ -65,6 +65,7 @@ export type GateActionEnvelope = {
|
|
|
65
65
|
gate: {
|
|
66
66
|
requirements: Array<{
|
|
67
67
|
id: string;
|
|
68
|
+
gate_id: string;
|
|
68
69
|
required: boolean;
|
|
69
70
|
description: string;
|
|
70
71
|
claim_type: string;
|
|
@@ -222,7 +223,7 @@ function deriveFlowRequirements(input: BuilderGateActionEnvelopeInput, action: K
|
|
|
222
223
|
const matched = new Set((Array.isArray(outcome.matched_expectations) ? outcome.matched_expectations : [])
|
|
223
224
|
.flatMap((entry) => isRecord(entry) && typeof entry.expectation_id === "string" ? [entry.expectation_id] : []));
|
|
224
225
|
return (expectationsForGate(gate, input.run.config) as FlowExpectation[])
|
|
225
|
-
.map((expectation) => requirementFromExpectation(expectation, matched, typeof outcome.accepted_exception_id === "string"));
|
|
226
|
+
.map((expectation) => requirementFromExpectation(gate.id, expectation, matched, typeof outcome.accepted_exception_id === "string"));
|
|
226
227
|
});
|
|
227
228
|
if (requirements.length > MAX_REQUIREMENTS) throw new Error("Builder gate-action envelope requirements exceed the supported bound");
|
|
228
229
|
if (!sameSet(action.expectation_ids, requirements.map((requirement) => requirement.id))) {
|
|
@@ -243,7 +244,7 @@ function assembleGateActionEnvelope(input: BuilderGateActionEnvelopeInput, loade
|
|
|
243
244
|
.filter((binding) => !isControlArtifact(binding.artifact) && binding.expectation_ids.some((id) => unresolvedRequired.includes(id)))
|
|
244
245
|
.map((binding) => binding.artifact);
|
|
245
246
|
const envelope: GateActionEnvelope = {
|
|
246
|
-
schema_version: "
|
|
247
|
+
schema_version: "2.0",
|
|
247
248
|
flow: {
|
|
248
249
|
run_id: input.run.runId,
|
|
249
250
|
definition_id: input.run.definitionId,
|
|
@@ -379,7 +380,7 @@ function workflowEvidenceMutation(expectationId: string, sessionArgument: string
|
|
|
379
380
|
};
|
|
380
381
|
}
|
|
381
382
|
|
|
382
|
-
function requirementFromExpectation(expectation: FlowExpectation, satisfied: Set<string>, acceptedException: boolean): GateActionEnvelope["gate"]["requirements"][number] {
|
|
383
|
+
function requirementFromExpectation(gateId: string, expectation: FlowExpectation, satisfied: Set<string>, acceptedException: boolean): GateActionEnvelope["gate"]["requirements"][number] {
|
|
383
384
|
const record = expectation as unknown as AnyRecord;
|
|
384
385
|
const claim = isRecord(record.bundle_claim) ? record.bundle_claim : null;
|
|
385
386
|
if (typeof record.id !== "string" || typeof record.description !== "string" || !claim || typeof claim.claimType !== "string") {
|
|
@@ -387,6 +388,7 @@ function requirementFromExpectation(expectation: FlowExpectation, satisfied: Set
|
|
|
387
388
|
}
|
|
388
389
|
return {
|
|
389
390
|
id: record.id,
|
|
391
|
+
gate_id: gateId,
|
|
390
392
|
required: record.required === true,
|
|
391
393
|
description: record.description,
|
|
392
394
|
claim_type: claim.claimType,
|
|
@@ -372,7 +372,8 @@ test("small-model client can start and advance from projected actions without ch
|
|
|
372
372
|
assert.ok(started.projection.next_action.command.includes(`'workflow' 'status' '--session-dir' '.kontourai/flow-agents/${session.slug}' '--json'`));
|
|
373
373
|
const envelope = started.gateActionEnvelope;
|
|
374
374
|
assert.equal(Object.hasOwn(started.projection.next_action, "gate_action_envelope"), false, "durable state has no duplicate envelope");
|
|
375
|
-
assert.equal(envelope.schema_version, "
|
|
375
|
+
assert.equal(envelope.schema_version, "2.0");
|
|
376
|
+
assert.equal(envelope.gate.requirements[0].gate_id, "pull-work-gate");
|
|
376
377
|
assert.equal(envelope.action.implementation_allowed, false, "implementation is forbidden before builder.build/execute");
|
|
377
378
|
assert.deepEqual(envelope.action.declared_evidence, ["selected-work"]);
|
|
378
379
|
assert.deepEqual(envelope.action.declared_artifacts, [`<slug>--pull-work.md`, "trust.bundle#selected-work"]);
|
|
@@ -481,6 +482,7 @@ test("accepted Flow exceptions explicitly waive the current envelope requirement
|
|
|
481
482
|
});
|
|
482
483
|
const recovered = await recoverBuilderFlowSession({ sessionDir: session.sessionDir });
|
|
483
484
|
assert.deepEqual(recovered.gateActionEnvelope.gate.accepted_exceptions, [{ gate_id: "pull-work-gate", exception_id: exception.id }]);
|
|
485
|
+
assert.equal(recovered.gateActionEnvelope.gate.requirements[0].gate_id, "pull-work-gate");
|
|
484
486
|
assert.equal(recovered.gateActionEnvelope.gate.requirements[0].status, "accepted_exception");
|
|
485
487
|
assert.deepEqual(recovered.gateActionEnvelope.stop_condition.required.unresolved_evidence_ids, []);
|
|
486
488
|
assert.deepEqual(recovered.gateActionEnvelope.stop_condition.required.artifact_refs, []);
|
|
@@ -761,7 +763,7 @@ test("public workflow drive signs adapter evidence with a consumed one-time key"
|
|
|
761
763
|
assert.deepEqual(payload.adapter_turns.map((turn) => turn.request), observedRequests);
|
|
762
764
|
assert.equal(payload.adapter_turns[0].request.schema_version, "1.0");
|
|
763
765
|
assert.equal(payload.adapter_turns[0].request.next_action.status, "continue");
|
|
764
|
-
assert.equal(payload.adapter_turns[0].request.gate_action_envelope.schema_version, "
|
|
766
|
+
assert.equal(payload.adapter_turns[0].request.gate_action_envelope.schema_version, "2.0");
|
|
765
767
|
assert.deepEqual(payload.adapter_turns.map((turn) => turn.request), observedRequests, "the signed payload binds the unchanged envelope bytes observed by the adapter");
|
|
766
768
|
assert.deepEqual(payload.adapter_turns[0].result.evidence.usage, { input_tokens: 10, output_tokens: 2 });
|
|
767
769
|
const tampered = Buffer.from(JSON.stringify({ ...payload, max_turns: 2 }));
|
|
@@ -9,6 +9,7 @@ import { createHash, generateKeyPairSync, randomBytes, sign } from "node:crypto"
|
|
|
9
9
|
|
|
10
10
|
import { MAX_CONTINUATION_ADAPTER_EVIDENCE_BYTES, MAX_CONTINUATION_TURN_RESULT_BYTES, ContinuationAdapterTimeoutError, createFileContinuationStore, runContinuationDriver, withContinuationDriverLock } from "../../build/src/continuation-driver.js";
|
|
11
11
|
import { executeContinuationAdapter, executeLoadedContinuationAdapter, loadContinuationAdapterCommand, waitForContinuationBarrier } from "../../build/src/cli/continuation-adapter.js";
|
|
12
|
+
import { validateSnapshot } from "../../build/src/continuation-validation.js";
|
|
12
13
|
|
|
13
14
|
const require = createRequire(import.meta.url);
|
|
14
15
|
const activeTurnAuthority = require("../../scripts/hooks/lib/continuation-turn-authority.js");
|
|
@@ -38,7 +39,7 @@ function snapshot(step, status = "active") {
|
|
|
38
39
|
function envelopeSnapshot(step, { evidence = [], artifacts = [], implementationAllowed = false, status = "active" } = {}) {
|
|
39
40
|
const value = snapshot(step, status);
|
|
40
41
|
value.gate_action_envelope = {
|
|
41
|
-
schema_version: "
|
|
42
|
+
schema_version: "2.0",
|
|
42
43
|
flow: { current_step: step, status },
|
|
43
44
|
action: { implementation_allowed: implementationAllowed },
|
|
44
45
|
stop_condition: { kind: "one_turn", adapter_evidence_is_gate_evidence: false },
|
|
@@ -47,6 +48,20 @@ function envelopeSnapshot(step, { evidence = [], artifacts = [], implementationA
|
|
|
47
48
|
return value;
|
|
48
49
|
}
|
|
49
50
|
|
|
51
|
+
test("gate-action envelope rejects partial accepted-exception bindings", () => {
|
|
52
|
+
const partial = envelopeSnapshot("execute");
|
|
53
|
+
partial.gate_action_envelope.flow.gate_ids = ["execute-gate"];
|
|
54
|
+
partial.gate_action_envelope.gate = {
|
|
55
|
+
requirements: [
|
|
56
|
+
{ id: "implemented", gate_id: "execute-gate", required: true, status: "accepted_exception" },
|
|
57
|
+
{ id: "verified", gate_id: "execute-gate", required: true, status: "unresolved" },
|
|
58
|
+
],
|
|
59
|
+
unresolved_requirement_ids: ["verified"],
|
|
60
|
+
accepted_exceptions: [{ gate_id: "execute-gate", exception_id: "exception-1" }],
|
|
61
|
+
};
|
|
62
|
+
assert.throws(() => validateSnapshot(partial), /inconsistent exception bindings/);
|
|
63
|
+
});
|
|
64
|
+
|
|
50
65
|
function terminalProgressSnapshot(status, { step = "learn", evidence = [], artifacts = [] } = {}) {
|
|
51
66
|
const value = snapshot(step, status);
|
|
52
67
|
value.progress_snapshot = {
|
|
@@ -16,11 +16,41 @@ export function validateSnapshot(value: ContinuationSnapshot): ContinuationSnaps
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
function validateGateActionEnvelope(value: GateActionEnvelope): void {
|
|
19
|
-
if (!value || typeof value !== "object" || value.schema_version !== "
|
|
19
|
+
if (!value || typeof value !== "object" || value.schema_version !== "2.0" || !value.flow || typeof value.flow.current_step !== "string"
|
|
20
20
|
|| !value.progress || !Array.isArray(value.progress.canonical_evidence) || !Array.isArray(value.progress.observed_artifacts)) throw new Error("continuation snapshot gate-action envelope is malformed");
|
|
21
|
+
if (value.gate !== undefined) validateGateRequirementBindings(value);
|
|
21
22
|
if (Buffer.byteLength(JSON.stringify(value), "utf8") > 65_536) throw new Error("continuation snapshot gate-action envelope exceeds 65536 bytes");
|
|
22
23
|
}
|
|
23
24
|
|
|
25
|
+
function validateGateRequirementBindings(value: GateActionEnvelope): void {
|
|
26
|
+
if (!Array.isArray(value.flow.gate_ids) || !Array.isArray(value.gate.requirements) || !Array.isArray(value.gate.accepted_exceptions)) {
|
|
27
|
+
throw new Error("continuation snapshot gate-action envelope has malformed gate bindings");
|
|
28
|
+
}
|
|
29
|
+
const activeGates = new Set(value.flow.gate_ids);
|
|
30
|
+
const acceptedGates = new Set<string>();
|
|
31
|
+
for (const exception of value.gate.accepted_exceptions) {
|
|
32
|
+
if (!exception || typeof exception.gate_id !== "string" || typeof exception.exception_id !== "string"
|
|
33
|
+
|| !activeGates.has(exception.gate_id) || acceptedGates.has(exception.gate_id)) {
|
|
34
|
+
throw new Error("continuation snapshot gate-action envelope has invalid accepted exceptions");
|
|
35
|
+
}
|
|
36
|
+
acceptedGates.add(exception.gate_id);
|
|
37
|
+
}
|
|
38
|
+
const gatesWithRequirements = new Set<string>();
|
|
39
|
+
for (const requirement of value.gate.requirements) {
|
|
40
|
+
if (!requirement || typeof requirement.gate_id !== "string" || !activeGates.has(requirement.gate_id)) {
|
|
41
|
+
throw new Error("continuation snapshot gate-action envelope has an invalid requirement gate binding");
|
|
42
|
+
}
|
|
43
|
+
gatesWithRequirements.add(requirement.gate_id);
|
|
44
|
+
if ((requirement.status === "accepted_exception") !== acceptedGates.has(requirement.gate_id)
|
|
45
|
+
&& requirement.status !== "satisfied") {
|
|
46
|
+
throw new Error("continuation snapshot gate-action envelope has inconsistent exception bindings");
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if ([...acceptedGates].some((gateId) => !gatesWithRequirements.has(gateId))) {
|
|
50
|
+
throw new Error("continuation snapshot gate-action envelope exception has no bound requirements");
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
24
54
|
export function validateTurnResult(value: ContinuationTurnResult): ContinuationTurnResult {
|
|
25
55
|
if (!value || typeof value !== "object" || (value.status !== "completed" && value.status !== "wait")) throw new Error("continuation adapter must return status completed or wait");
|
|
26
56
|
if (value.status === "wait") validateBarrier(value.barrier);
|