@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
- * When the override is unsafe, it is ignored and the resolver uses the
24
- * canonical repoRoot/kits/ source for legitimate flows.
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
- * When the override is unsafe, it is ignored and the resolver uses the
66
- * canonical repoRoot/kits/ source for legitimate flows.
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
- // Override targets an agent-writable runtime path; ignore it and use
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.0",
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"), {
@@ -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
- * When the override is unsafe, it is ignored and the resolver uses the
69
- * canonical repoRoot/kits/ source for legitimate flows.
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
- // Override targets an agent-writable runtime path; ignore it and use
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
  }