@kontourai/flow-agents 3.4.0 → 3.4.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
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [3.4.1](https://github.com/kontourai/flow-agents/compare/v3.4.0...v3.4.1) (2026-07-10)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Fixes
|
|
7
|
+
|
|
8
|
+
* resolve packaged Flow definitions in consumer repos ([#532](https://github.com/kontourai/flow-agents/issues/532)) ([3eb4a2e](https://github.com/kontourai/flow-agents/commit/3eb4a2e42d49a9c31b16ef62484cf5b0a001c7a1))
|
|
9
|
+
|
|
3
10
|
## [3.4.0](https://github.com/kontourai/flow-agents/compare/v3.3.0...v3.4.0) (2026-07-10)
|
|
4
11
|
|
|
5
12
|
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
* - FLOW_AGENTS_FLOW_DEFS_DIR resolves into a runtime artifact directory
|
|
21
21
|
* - The resolved path escapes the expected root (belt-and-suspenders)
|
|
22
22
|
*
|
|
23
|
-
*
|
|
24
|
-
* canonical
|
|
23
|
+
* An unsafe explicit override fails closed. Package fallback applies only to
|
|
24
|
+
* canonical lookup when no override was supplied.
|
|
25
25
|
*/
|
|
26
26
|
export declare function resolveFlowFilePath(kitId: string, flowName: string, flowId: string, repoRoot: string, allowOverride?: boolean): string | null;
|
|
27
27
|
/** A single gate expectation from a FlowDefinition expects[] entry. */
|
|
@@ -54,6 +54,35 @@ function isAgentWritableDir(resolvedDir) {
|
|
|
54
54
|
return false;
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
|
+
function installedPackageRoot() {
|
|
58
|
+
let directory = path.dirname(fileURLToPath(import.meta.url));
|
|
59
|
+
while (true) {
|
|
60
|
+
if (fs.existsSync(path.join(directory, "package.json")) && fs.existsSync(path.join(directory, "kits"))) {
|
|
61
|
+
return directory;
|
|
62
|
+
}
|
|
63
|
+
const parent = path.dirname(directory);
|
|
64
|
+
if (parent === directory)
|
|
65
|
+
return null;
|
|
66
|
+
directory = parent;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function packagedFlowFile(kitId, flowName, consumerRoot) {
|
|
70
|
+
const packageRoot = installedPackageRoot();
|
|
71
|
+
if (!packageRoot || path.resolve(packageRoot) === path.resolve(consumerRoot))
|
|
72
|
+
return null;
|
|
73
|
+
const kitsRoot = path.resolve(packageRoot, "kits");
|
|
74
|
+
const candidate = path.resolve(kitsRoot, kitId, "flows", `${flowName}.flow.json`);
|
|
75
|
+
if (!candidate.startsWith(kitsRoot + path.sep))
|
|
76
|
+
return null;
|
|
77
|
+
try {
|
|
78
|
+
const realKitsRoot = fs.realpathSync.native(kitsRoot);
|
|
79
|
+
const realCandidate = fs.realpathSync.native(candidate);
|
|
80
|
+
return realCandidate.startsWith(realKitsRoot + path.sep) ? realCandidate : null;
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
57
86
|
/**
|
|
58
87
|
* Build and validate the FlowDefinition file path.
|
|
59
88
|
*
|
|
@@ -62,8 +91,8 @@ function isAgentWritableDir(resolvedDir) {
|
|
|
62
91
|
* - FLOW_AGENTS_FLOW_DEFS_DIR resolves into a runtime artifact directory
|
|
63
92
|
* - The resolved path escapes the expected root (belt-and-suspenders)
|
|
64
93
|
*
|
|
65
|
-
*
|
|
66
|
-
* canonical
|
|
94
|
+
* An unsafe explicit override fails closed. Package fallback applies only to
|
|
95
|
+
* canonical lookup when no override was supplied.
|
|
67
96
|
*/
|
|
68
97
|
export function resolveFlowFilePath(kitId, flowName, flowId, repoRoot, allowOverride = true) {
|
|
69
98
|
// Primary defense: reject any slug containing traversal chars or non-identifier chars.
|
|
@@ -72,13 +101,11 @@ export function resolveFlowFilePath(kitId, flowName, flowId, repoRoot, allowOver
|
|
|
72
101
|
const override = allowOverride ? process.env["FLOW_AGENTS_FLOW_DEFS_DIR"] : undefined;
|
|
73
102
|
let expectedRoot;
|
|
74
103
|
let flowFilePath;
|
|
104
|
+
let canonicalLookup = false;
|
|
75
105
|
if (override) {
|
|
76
106
|
const resolvedOverride = path.resolve(override);
|
|
77
107
|
if (isAgentWritableDir(resolvedOverride)) {
|
|
78
|
-
|
|
79
|
-
// the canonical kit root. The session will resolve the real kit flow.
|
|
80
|
-
expectedRoot = path.resolve(repoRoot, "kits");
|
|
81
|
-
flowFilePath = path.join(repoRoot, "kits", kitId, "flows", `${flowName}.flow.json`);
|
|
108
|
+
return null;
|
|
82
109
|
}
|
|
83
110
|
else {
|
|
84
111
|
expectedRoot = resolvedOverride;
|
|
@@ -90,6 +117,7 @@ export function resolveFlowFilePath(kitId, flowName, flowId, repoRoot, allowOver
|
|
|
90
117
|
else {
|
|
91
118
|
expectedRoot = path.resolve(repoRoot, "kits");
|
|
92
119
|
flowFilePath = path.join(repoRoot, "kits", kitId, "flows", `${flowName}.flow.json`);
|
|
120
|
+
canonicalLookup = true;
|
|
93
121
|
}
|
|
94
122
|
// Belt-and-suspenders: confirm the resolved path stays within the expected root.
|
|
95
123
|
// After slug validation this is theoretically unreachable, but defense-in-depth
|
|
@@ -110,6 +138,11 @@ export function resolveFlowFilePath(kitId, flowName, flowId, repoRoot, allowOver
|
|
|
110
138
|
return realPath;
|
|
111
139
|
}
|
|
112
140
|
catch {
|
|
141
|
+
if (canonicalLookup) {
|
|
142
|
+
const packaged = packagedFlowFile(kitId, flowName, repoRoot);
|
|
143
|
+
if (packaged)
|
|
144
|
+
return packaged;
|
|
145
|
+
}
|
|
113
146
|
return resolvedPath;
|
|
114
147
|
}
|
|
115
148
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kontourai/flow-agents",
|
|
3
|
-
"version": "3.4.
|
|
3
|
+
"version": "3.4.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",
|
|
@@ -4,7 +4,7 @@ import fs from "node:fs";
|
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
|
|
7
|
-
import { resolveEffectiveFlowDefinition } from "../../build/src/lib/flow-resolver.js";
|
|
7
|
+
import { resolveEffectiveFlowDefinition, resolveFlowFilePath } from "../../build/src/lib/flow-resolver.js";
|
|
8
8
|
|
|
9
9
|
const REPO_ROOT = path.resolve(import.meta.dirname, "../..");
|
|
10
10
|
|
|
@@ -26,6 +26,35 @@ test("effective Builder definition materializes uses_flow and Flow-native comple
|
|
|
26
26
|
);
|
|
27
27
|
});
|
|
28
28
|
|
|
29
|
+
test("installed package definitions resolve when a consumer repo has no kits directory", () => {
|
|
30
|
+
const consumer = fs.mkdtempSync(path.join(os.tmpdir(), "flow-agents-consumer-"));
|
|
31
|
+
const resolved = resolveFlowFilePath("builder", "build", "builder.build", consumer, false);
|
|
32
|
+
assert.ok(resolved);
|
|
33
|
+
assert.equal(fs.realpathSync(resolved), fs.realpathSync(path.join(REPO_ROOT, "kits", "builder", "flows", "build.flow.json")));
|
|
34
|
+
assert.equal(resolveEffectiveFlowDefinition("builder.build", consumer, { allowOverride: false })?.version, "1.1");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("consumer-vendored definitions remain authoritative over package fallback", () => {
|
|
38
|
+
const consumer = fs.mkdtempSync(path.join(os.tmpdir(), "flow-agents-consumer-vendored-"));
|
|
39
|
+
const vendored = path.join(consumer, "kits", "builder", "flows", "build.flow.json");
|
|
40
|
+
writeJson(vendored, { id: "builder.build", version: "consumer", steps: [{ id: "local", next: null }], gates: {} });
|
|
41
|
+
assert.equal(resolveFlowFilePath("builder", "build", "builder.build", consumer, false), fs.realpathSync(vendored));
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("unsafe explicit overrides fail closed instead of using package fallback", () => {
|
|
45
|
+
const consumer = fs.mkdtempSync(path.join(os.tmpdir(), "flow-agents-consumer-unsafe-"));
|
|
46
|
+
const unsafeDefinitions = path.join(consumer, ".kontourai", "flow-agents", "definitions");
|
|
47
|
+
fs.mkdirSync(unsafeDefinitions, { recursive: true });
|
|
48
|
+
const prior = process.env.FLOW_AGENTS_FLOW_DEFS_DIR;
|
|
49
|
+
process.env.FLOW_AGENTS_FLOW_DEFS_DIR = unsafeDefinitions;
|
|
50
|
+
try {
|
|
51
|
+
assert.equal(resolveFlowFilePath("builder", "build", "builder.build", consumer), null);
|
|
52
|
+
} finally {
|
|
53
|
+
if (prior === undefined) delete process.env.FLOW_AGENTS_FLOW_DEFS_DIR;
|
|
54
|
+
else process.env.FLOW_AGENTS_FLOW_DEFS_DIR = prior;
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
29
58
|
test("effective definition compilation rejects uses_flow cycles", () => {
|
|
30
59
|
const definitions = fs.mkdtempSync(path.join(os.tmpdir(), "flow-agents-composition-cycle-"));
|
|
31
60
|
writeJson(path.join(definitions, "loop.one.flow.json"), {
|
package/src/lib/flow-resolver.ts
CHANGED
|
@@ -57,6 +57,33 @@ function isAgentWritableDir(resolvedDir: string): boolean {
|
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
function installedPackageRoot(): string | null {
|
|
61
|
+
let directory = path.dirname(fileURLToPath(import.meta.url));
|
|
62
|
+
while (true) {
|
|
63
|
+
if (fs.existsSync(path.join(directory, "package.json")) && fs.existsSync(path.join(directory, "kits"))) {
|
|
64
|
+
return directory;
|
|
65
|
+
}
|
|
66
|
+
const parent = path.dirname(directory);
|
|
67
|
+
if (parent === directory) return null;
|
|
68
|
+
directory = parent;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function packagedFlowFile(kitId: string, flowName: string, consumerRoot: string): string | null {
|
|
73
|
+
const packageRoot = installedPackageRoot();
|
|
74
|
+
if (!packageRoot || path.resolve(packageRoot) === path.resolve(consumerRoot)) return null;
|
|
75
|
+
const kitsRoot = path.resolve(packageRoot, "kits");
|
|
76
|
+
const candidate = path.resolve(kitsRoot, kitId, "flows", `${flowName}.flow.json`);
|
|
77
|
+
if (!candidate.startsWith(kitsRoot + path.sep)) return null;
|
|
78
|
+
try {
|
|
79
|
+
const realKitsRoot = fs.realpathSync.native(kitsRoot);
|
|
80
|
+
const realCandidate = fs.realpathSync.native(candidate);
|
|
81
|
+
return realCandidate.startsWith(realKitsRoot + path.sep) ? realCandidate : null;
|
|
82
|
+
} catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
60
87
|
/**
|
|
61
88
|
* Build and validate the FlowDefinition file path.
|
|
62
89
|
*
|
|
@@ -65,8 +92,8 @@ function isAgentWritableDir(resolvedDir: string): boolean {
|
|
|
65
92
|
* - FLOW_AGENTS_FLOW_DEFS_DIR resolves into a runtime artifact directory
|
|
66
93
|
* - The resolved path escapes the expected root (belt-and-suspenders)
|
|
67
94
|
*
|
|
68
|
-
*
|
|
69
|
-
* canonical
|
|
95
|
+
* An unsafe explicit override fails closed. Package fallback applies only to
|
|
96
|
+
* canonical lookup when no override was supplied.
|
|
70
97
|
*/
|
|
71
98
|
export function resolveFlowFilePath(
|
|
72
99
|
kitId: string,
|
|
@@ -82,14 +109,12 @@ export function resolveFlowFilePath(
|
|
|
82
109
|
|
|
83
110
|
let expectedRoot: string;
|
|
84
111
|
let flowFilePath: string;
|
|
112
|
+
let canonicalLookup = false;
|
|
85
113
|
|
|
86
114
|
if (override) {
|
|
87
115
|
const resolvedOverride = path.resolve(override);
|
|
88
116
|
if (isAgentWritableDir(resolvedOverride)) {
|
|
89
|
-
|
|
90
|
-
// the canonical kit root. The session will resolve the real kit flow.
|
|
91
|
-
expectedRoot = path.resolve(repoRoot, "kits");
|
|
92
|
-
flowFilePath = path.join(repoRoot, "kits", kitId, "flows", `${flowName}.flow.json`);
|
|
117
|
+
return null;
|
|
93
118
|
} else {
|
|
94
119
|
expectedRoot = resolvedOverride;
|
|
95
120
|
// flowId = kitId + "." + flowName; after slug validation this contains only
|
|
@@ -99,6 +124,7 @@ export function resolveFlowFilePath(
|
|
|
99
124
|
} else {
|
|
100
125
|
expectedRoot = path.resolve(repoRoot, "kits");
|
|
101
126
|
flowFilePath = path.join(repoRoot, "kits", kitId, "flows", `${flowName}.flow.json`);
|
|
127
|
+
canonicalLookup = true;
|
|
102
128
|
}
|
|
103
129
|
|
|
104
130
|
// Belt-and-suspenders: confirm the resolved path stays within the expected root.
|
|
@@ -120,6 +146,10 @@ export function resolveFlowFilePath(
|
|
|
120
146
|
}
|
|
121
147
|
return realPath;
|
|
122
148
|
} catch {
|
|
149
|
+
if (canonicalLookup) {
|
|
150
|
+
const packaged = packagedFlowFile(kitId, flowName, repoRoot);
|
|
151
|
+
if (packaged) return packaged;
|
|
152
|
+
}
|
|
123
153
|
return resolvedPath;
|
|
124
154
|
}
|
|
125
155
|
}
|