@kontourai/flow-agents 0.4.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/kit-gates-demo.yml +171 -0
- package/CHANGELOG.md +35 -0
- package/CONTEXT.md +1 -1
- package/README.md +13 -2
- package/build/src/cli/flow-kit.js +41 -2
- package/build/src/flow-kit/validate.js +98 -0
- package/build/src/tools/validate-source-tree.js +2 -1
- package/context/scripts/hooks/config-protection.js +217 -15
- package/docs/fixture-ownership.md +1 -0
- package/docs/index.md +9 -1
- package/docs/kit-authoring-guide.md +126 -0
- package/docs/knowledge-kit.md +69 -0
- package/docs/vision.md +22 -0
- package/evals/fixtures/kit-conformance-levels/k0-flows-only/flows/review.flow.json +26 -0
- package/evals/fixtures/kit-conformance-levels/k0-flows-only/kit.json +13 -0
- package/evals/fixtures/kit-conformance-levels/k1-agent-extension/docs/README.md +3 -0
- package/evals/fixtures/kit-conformance-levels/k1-agent-extension/flows/build.flow.json +26 -0
- package/evals/fixtures/kit-conformance-levels/k1-agent-extension/kit.json +20 -0
- package/evals/fixtures/kit-conformance-levels/k2-with-evals/docs/README.md +3 -0
- package/evals/fixtures/kit-conformance-levels/k2-with-evals/eval-suites/contract-suite/suite.test.js +1 -0
- package/evals/fixtures/kit-conformance-levels/k2-with-evals/flows/synthesize.flow.json +26 -0
- package/evals/fixtures/kit-conformance-levels/k2-with-evals/kit.json +27 -0
- package/evals/fixtures/kit-conformance-levels/third-party-extension/flows/review.flow.json +26 -0
- package/evals/fixtures/kit-conformance-levels/third-party-extension/kit.json +19 -0
- package/evals/integration/test_fixture_retirement_audit.sh +2 -2
- package/evals/integration/test_hook_category_behaviors.sh +51 -0
- package/evals/integration/test_kit_conformance_levels.sh +209 -0
- package/evals/run.sh +2 -0
- package/kits/catalog.json +6 -0
- package/kits/knowledge/adapters/default-store/index.js +2 -2
- package/kits/knowledge/adapters/flow-runner/entity-extractor.js +194 -0
- package/kits/knowledge/adapters/flow-runner/index.js +349 -0
- package/kits/knowledge/adapters/obsidian-store/README.md +141 -0
- package/kits/knowledge/adapters/obsidian-store/demo.js +181 -0
- package/kits/knowledge/adapters/obsidian-store/index.js +868 -0
- package/kits/knowledge/adapters/shared/codec.js +325 -0
- package/kits/knowledge/docs/store-contract.md +72 -0
- package/kits/knowledge/evals/entities/demo-acme.js +125 -0
- package/kits/knowledge/evals/entities/suite.test.js +722 -0
- package/kits/knowledge/kit.json +10 -0
- package/kits/release-evidence/fixtures/claims/README.md +14 -0
- package/kits/release-evidence/fixtures/claims/fail-rejected-release.trust.json +22 -0
- package/kits/release-evidence/fixtures/claims/pass-trusted-release.trust.json +22 -0
- package/kits/release-evidence/flows/release-evidence.flow.json +38 -0
- package/kits/release-evidence/kit.json +13 -0
- package/package.json +1 -1
- package/packaging/conformance/fixtures/config-protection--allow-no-verify-in-string.json +20 -0
- package/packaging/conformance/fixtures/config-protection--block-git-no-verify.json +23 -0
- package/scripts/hooks/config-protection.js +217 -15
- package/src/cli/flow-kit.ts +40 -2
- package/src/flow-kit/validate.ts +127 -0
- package/src/tools/validate-source-tree.ts +2 -1
package/kits/knowledge/kit.json
CHANGED
|
@@ -61,6 +61,11 @@
|
|
|
61
61
|
"id": "knowledge.similarity-vector",
|
|
62
62
|
"path": "adapters/similarity-vector/index.js",
|
|
63
63
|
"description": "Vector similarity detector: createVectorSimilarityDetector(options) returns a drop-in SimilarityDetector backed by dense embeddings and cosine similarity. Supports injectable embed fn (tests) or ollama /api/embed (default). Fail-closed: throws EMBED_FAILURE on infrastructure errors rather than silently masking them as empty clusters."
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"id": "knowledge.obsidian-store",
|
|
67
|
+
"path": "adapters/obsidian-store/index.js",
|
|
68
|
+
"description": "Obsidian store adapter: each record is one human-canonical Obsidian markdown note. Frontmatter carries all contract fields; body rendered with callouts (raw), readable prose + Sources/Related wikilink sections (compiled/concept/snapshot). Category dots map to folder hierarchy; title-slugified filenames with collision suffix; superseded records move to archive/ (supersede-not-delete invariant). Backed by shared codec with default-store."
|
|
64
69
|
}
|
|
65
70
|
],
|
|
66
71
|
"evals": [
|
|
@@ -93,6 +98,11 @@
|
|
|
93
98
|
"id": "knowledge.retirement-suite",
|
|
94
99
|
"path": "evals/retirement/suite.test.js",
|
|
95
100
|
"description": "Eval cases for retire: AC1 (retirement only via approved proposal with rationale/ref, transition table enforcement); AC2 (retired excluded from listByType/listByCategory/similarity defaults, returned with includeRetired flag, provenance intact via get()); AC3 (consolidation after retirement reflects pruned working set; retired record still reachable from snapshot provenance history); rejection leaves status byte-identical; gate telemetry."
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"id": "knowledge.entity-cards-suite",
|
|
104
|
+
"path": "evals/entities/suite.test.js",
|
|
105
|
+
"description": "Eval cases for person/entity cards (issue #48): AC1-AC4 \u2014 entity extraction from Attendees lines, exact-match resolution, possible-duplicate detection, merge via propose/apply/reject (union aliases+backlinks, supersede duplicate), Obsidian people/ folder rendering, and extended contract suite (person type validity) on both adapters."
|
|
96
106
|
}
|
|
97
107
|
]
|
|
98
108
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Claim Fixtures
|
|
2
|
+
|
|
3
|
+
These trust-report JSON files are used by `.github/workflows/kit-gates-demo.yml` to prove
|
|
4
|
+
agentless gate evaluation against the `release-evidence` Flow Kit.
|
|
5
|
+
|
|
6
|
+
| File | Purpose |
|
|
7
|
+
| --- | --- |
|
|
8
|
+
| `pass-trusted-release.trust.json` | Passing case: `release.evidence` claim with status `trusted`. Gate verdict: `pass`. |
|
|
9
|
+
| `fail-rejected-release.trust.json` | Failing case: `release.evidence` claim with status `rejected`. Gate verdict: `route-back` or `block`. The workflow asserts the failure is detected (non-zero exit) rather than letting it silently pass. |
|
|
10
|
+
|
|
11
|
+
## Format
|
|
12
|
+
|
|
13
|
+
Each file is a Surface trust-report artifact (`artifact_type: "trust-report"`).
|
|
14
|
+
The Flow CLI normalizes these via `flow attach-evidence --trust-artifact` before gate evaluation.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": "0.1",
|
|
3
|
+
"artifact_type": "trust-report",
|
|
4
|
+
"subject": "ci/release-evidence-gate",
|
|
5
|
+
"producer": "ci/release-evidence-probe",
|
|
6
|
+
"status": "rejected",
|
|
7
|
+
"issued_at": "2026-06-12T00:00:00.000Z",
|
|
8
|
+
"authority_traces": [
|
|
9
|
+
"github:main"
|
|
10
|
+
],
|
|
11
|
+
"claims": [
|
|
12
|
+
{
|
|
13
|
+
"type": "release.evidence",
|
|
14
|
+
"subject": "ci/release-evidence-gate",
|
|
15
|
+
"status": "rejected",
|
|
16
|
+
"issued_at": "2026-06-12T00:00:00.000Z"
|
|
17
|
+
}
|
|
18
|
+
],
|
|
19
|
+
"integrity": {
|
|
20
|
+
"verified": true
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": "0.1",
|
|
3
|
+
"artifact_type": "trust-report",
|
|
4
|
+
"subject": "ci/release-evidence-gate",
|
|
5
|
+
"producer": "ci/release-evidence-probe",
|
|
6
|
+
"status": "trusted",
|
|
7
|
+
"issued_at": "2026-06-12T00:00:00.000Z",
|
|
8
|
+
"authority_traces": [
|
|
9
|
+
"github:main"
|
|
10
|
+
],
|
|
11
|
+
"claims": [
|
|
12
|
+
{
|
|
13
|
+
"type": "release.evidence",
|
|
14
|
+
"subject": "ci/release-evidence-gate",
|
|
15
|
+
"status": "trusted",
|
|
16
|
+
"issued_at": "2026-06-12T00:00:00.000Z"
|
|
17
|
+
}
|
|
18
|
+
],
|
|
19
|
+
"integrity": {
|
|
20
|
+
"verified": true
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "release-evidence",
|
|
3
|
+
"version": "1.0",
|
|
4
|
+
"steps": [
|
|
5
|
+
{
|
|
6
|
+
"id": "gate-check",
|
|
7
|
+
"next": null
|
|
8
|
+
}
|
|
9
|
+
],
|
|
10
|
+
"gates": {
|
|
11
|
+
"release-evidence-gate": {
|
|
12
|
+
"step": "gate-check",
|
|
13
|
+
"expects": [
|
|
14
|
+
{
|
|
15
|
+
"id": "release-claim-present",
|
|
16
|
+
"kind": "surface.claim",
|
|
17
|
+
"required": true,
|
|
18
|
+
"description": "A trusted release.evidence claim must be attached before this gate can pass.",
|
|
19
|
+
"explore_hint": "Attach a trust-report JSON file with claim type release.evidence and status trusted.",
|
|
20
|
+
"claim": {
|
|
21
|
+
"type": "release.evidence",
|
|
22
|
+
"accepted_statuses": [
|
|
23
|
+
"trusted"
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
],
|
|
28
|
+
"on_route_back": {
|
|
29
|
+
"missing_evidence": "gate-check",
|
|
30
|
+
"default": "gate-check"
|
|
31
|
+
},
|
|
32
|
+
"route_back_policy": {
|
|
33
|
+
"max_attempts": 3,
|
|
34
|
+
"on_exceeded": "block"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": "1.0",
|
|
3
|
+
"id": "release-evidence",
|
|
4
|
+
"name": "Release Evidence Kit",
|
|
5
|
+
"description": "A minimal flows-only Flow Kit for proving agentless gate evaluation over surface claims in CI. One gate expects a trusted release.evidence claim; CI attaches a claim file and calls flow evaluate.",
|
|
6
|
+
"flows": [
|
|
7
|
+
{
|
|
8
|
+
"id": "release-evidence",
|
|
9
|
+
"path": "flows/release-evidence.flow.json",
|
|
10
|
+
"description": "Single-gate flow that requires a trusted release.evidence claim before the gate-check step can pass."
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kontourai/flow-agents",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
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",
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"description": "config-protection allows a command whose string content merely mentions the bypass flag",
|
|
3
|
+
"policy_class": "config-protection",
|
|
4
|
+
"canonical_event": "preToolUse",
|
|
5
|
+
"conformance_level": "L2",
|
|
6
|
+
"hook_id": "config-protection",
|
|
7
|
+
"hook_script": "config-protection.js",
|
|
8
|
+
"payload": {
|
|
9
|
+
"hook_event_name": "PreToolUse",
|
|
10
|
+
"tool_name": "Bash",
|
|
11
|
+
"tool_input": {
|
|
12
|
+
"command": "gh issue create --body \"git commit --no-verify is blocked by the hook\""
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"expected": {
|
|
16
|
+
"exit_code": 0,
|
|
17
|
+
"stderr_is_empty": true,
|
|
18
|
+
"stdout_echoes_input": true
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"description": "config-protection blocks a Bash command carrying the bypass flag in actual flag position",
|
|
3
|
+
"policy_class": "config-protection",
|
|
4
|
+
"canonical_event": "preToolUse",
|
|
5
|
+
"conformance_level": "L2",
|
|
6
|
+
"hook_id": "config-protection",
|
|
7
|
+
"hook_script": "config-protection.js",
|
|
8
|
+
"payload": {
|
|
9
|
+
"hook_event_name": "PreToolUse",
|
|
10
|
+
"tool_name": "Bash",
|
|
11
|
+
"tool_input": {
|
|
12
|
+
"command": "git commit --no-verify -m fix"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"expected": {
|
|
16
|
+
"exit_code": 2,
|
|
17
|
+
"stderr_contains": [
|
|
18
|
+
"BLOCKED",
|
|
19
|
+
"verification hooks"
|
|
20
|
+
],
|
|
21
|
+
"stdout_is_empty": true
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
* Blocks modifications to linter/formatter config files.
|
|
6
6
|
* Steers the agent to fix source code instead of weakening configs.
|
|
7
7
|
*
|
|
8
|
+
* Also blocks git verification-bypass flags in actual flag positions only.
|
|
9
|
+
* Text that merely mentions the flag inside quoted strings or prose is allowed.
|
|
10
|
+
*
|
|
8
11
|
* Exit codes: 0 = allow, 2 = block
|
|
9
12
|
*/
|
|
10
13
|
|
|
@@ -25,6 +28,195 @@ const PROTECTED_FILES = new Set([
|
|
|
25
28
|
'.markdownlint.json', '.markdownlint.yaml', '.markdownlintrc',
|
|
26
29
|
]);
|
|
27
30
|
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Shell-aware tokenizer
|
|
33
|
+
//
|
|
34
|
+
// Splits a shell command string into tokens, respecting single/double quotes
|
|
35
|
+
// and backslash escapes. Quoted content stays inside its parent token so
|
|
36
|
+
// flag text inside a -m argument string is never matched as a flag.
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* tokenize(cmd) -- shell-aware tokenizer for a single command segment.
|
|
41
|
+
* Returns an array of unquoted token strings.
|
|
42
|
+
*/
|
|
43
|
+
function tokenize(cmd) {
|
|
44
|
+
const tokens = [];
|
|
45
|
+
let i = 0;
|
|
46
|
+
const len = cmd.length;
|
|
47
|
+
|
|
48
|
+
while (i < len) {
|
|
49
|
+
// Skip whitespace between tokens.
|
|
50
|
+
while (i < len && /\s/.test(cmd[i])) i++;
|
|
51
|
+
if (i >= len) break;
|
|
52
|
+
|
|
53
|
+
let token = '';
|
|
54
|
+
|
|
55
|
+
// Consume one token -- stop at unquoted whitespace.
|
|
56
|
+
while (i < len) {
|
|
57
|
+
const ch = cmd[i];
|
|
58
|
+
|
|
59
|
+
if (ch === '\\') {
|
|
60
|
+
// Backslash escape outside of quotes.
|
|
61
|
+
i++;
|
|
62
|
+
if (i < len) token += cmd[i++];
|
|
63
|
+
} else if (ch === "'") {
|
|
64
|
+
// Single-quoted string: no escape processing, read until closing quote.
|
|
65
|
+
i++;
|
|
66
|
+
while (i < len && cmd[i] !== "'") token += cmd[i++];
|
|
67
|
+
i++; // consume closing quote
|
|
68
|
+
} else if (ch === '"') {
|
|
69
|
+
// Double-quoted string: honour \" and \\ escape sequences.
|
|
70
|
+
i++;
|
|
71
|
+
while (i < len && cmd[i] !== '"') {
|
|
72
|
+
if (cmd[i] === '\\' && i + 1 < len && (cmd[i + 1] === '"' || cmd[i + 1] === '\\')) {
|
|
73
|
+
i++; // skip the backslash
|
|
74
|
+
token += cmd[i++];
|
|
75
|
+
} else {
|
|
76
|
+
token += cmd[i++];
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
i++; // consume closing quote
|
|
80
|
+
} else if (/\s/.test(ch)) {
|
|
81
|
+
break; // end of token
|
|
82
|
+
} else {
|
|
83
|
+
token += ch;
|
|
84
|
+
i++;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (token.length > 0) tokens.push(token);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return tokens;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* splitSegments(cmd) -- split on shell connectors && || ; | outside of quotes.
|
|
96
|
+
*/
|
|
97
|
+
function splitSegments(cmd) {
|
|
98
|
+
const segments = [];
|
|
99
|
+
let i = 0;
|
|
100
|
+
const len = cmd.length;
|
|
101
|
+
let segStart = 0;
|
|
102
|
+
|
|
103
|
+
while (i < len) {
|
|
104
|
+
const ch = cmd[i];
|
|
105
|
+
|
|
106
|
+
if (ch === '\\') {
|
|
107
|
+
i += 2; // skip escaped character
|
|
108
|
+
} else if (ch === "'") {
|
|
109
|
+
i++;
|
|
110
|
+
while (i < len && cmd[i] !== "'") i++;
|
|
111
|
+
i++; // skip closing quote
|
|
112
|
+
} else if (ch === '"') {
|
|
113
|
+
i++;
|
|
114
|
+
while (i < len) {
|
|
115
|
+
if (cmd[i] === '\\' && i + 1 < len) { i += 2; continue; }
|
|
116
|
+
if (cmd[i] === '"') { i++; break; }
|
|
117
|
+
i++;
|
|
118
|
+
}
|
|
119
|
+
} else if (ch === '&' && i + 1 < len && cmd[i + 1] === '&') {
|
|
120
|
+
segments.push(cmd.slice(segStart, i).trim());
|
|
121
|
+
i += 2; segStart = i;
|
|
122
|
+
} else if (ch === '|' && i + 1 < len && cmd[i + 1] === '|') {
|
|
123
|
+
segments.push(cmd.slice(segStart, i).trim());
|
|
124
|
+
i += 2; segStart = i;
|
|
125
|
+
} else if (ch === ';') {
|
|
126
|
+
segments.push(cmd.slice(segStart, i).trim());
|
|
127
|
+
i++; segStart = i;
|
|
128
|
+
} else if (ch === '|') {
|
|
129
|
+
// single pipe
|
|
130
|
+
segments.push(cmd.slice(segStart, i).trim());
|
|
131
|
+
i++; segStart = i;
|
|
132
|
+
} else {
|
|
133
|
+
i++;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const tail = cmd.slice(segStart).trim();
|
|
138
|
+
if (tail.length > 0) segments.push(tail);
|
|
139
|
+
return segments.filter(s => s.length > 0);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Git global flags that consume a following argument value.
|
|
143
|
+
const GIT_GLOBAL_FLAGS_WITH_ARG = new Set(['-C', '-c', '--exec-path', '--git-dir', '--work-tree', '--namespace']);
|
|
144
|
+
// Git global flags that are standalone (no following argument).
|
|
145
|
+
const GIT_GLOBAL_FLAGS_STANDALONE = new Set([
|
|
146
|
+
'--version', '--help', '--html-path', '--man-path', '--info-path',
|
|
147
|
+
'-p', '--paginate', '-P', '--no-pager', '--no-replace-objects',
|
|
148
|
+
'--bare', '--literal-pathspecs', '--glob-pathspecs', '--noglob-pathspecs',
|
|
149
|
+
'--icase-pathspecs', '--no-optional-locks', '--list-cmds',
|
|
150
|
+
]);
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* resolveGitSubcommand(tokens) -- walk past global git flags and return the
|
|
154
|
+
* subcommand token, or null if not determinable.
|
|
155
|
+
*/
|
|
156
|
+
function resolveGitSubcommand(tokens) {
|
|
157
|
+
let i = 1;
|
|
158
|
+
while (i < tokens.length) {
|
|
159
|
+
const t = tokens[i];
|
|
160
|
+
if (GIT_GLOBAL_FLAGS_WITH_ARG.has(t)) {
|
|
161
|
+
i += 2;
|
|
162
|
+
} else if (GIT_GLOBAL_FLAGS_STANDALONE.has(t)) {
|
|
163
|
+
i += 1;
|
|
164
|
+
} else if (t.startsWith('-')) {
|
|
165
|
+
i += 1; // unknown global flag -- skip conservatively
|
|
166
|
+
} else {
|
|
167
|
+
return { subcommand: t, flagsStart: i + 1 };
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Flags for git commit that consume the immediately following token as a value.
|
|
174
|
+
const COMMIT_FLAGS_WITH_VALUE = new Set([
|
|
175
|
+
'-m', '--message', '-F', '--file', '-C', '-c',
|
|
176
|
+
'--author', '--date', '--fixup', '--squash', '--pathspec-from-file',
|
|
177
|
+
]);
|
|
178
|
+
|
|
179
|
+
// Flags for git push that consume the following token as a value.
|
|
180
|
+
const PUSH_FLAGS_WITH_VALUE = new Set([
|
|
181
|
+
'--receive-pack', '--repo', '--push-option', '-o', '--recurse-submodules',
|
|
182
|
+
]);
|
|
183
|
+
|
|
184
|
+
const BYPASS_NV = '--no-verify';
|
|
185
|
+
const BYPASS_N = '-n'; // short alias (commit only; on push -n means --dry-run)
|
|
186
|
+
|
|
187
|
+
function checkSegmentForBypass(tokens) {
|
|
188
|
+
if (tokens.length === 0 || tokens[0] !== 'git') return null;
|
|
189
|
+
const resolved = resolveGitSubcommand(tokens);
|
|
190
|
+
if (!resolved) return null;
|
|
191
|
+
const { subcommand, flagsStart } = resolved;
|
|
192
|
+
if (subcommand === 'commit') {
|
|
193
|
+
for (let i = flagsStart; i < tokens.length; i++) {
|
|
194
|
+
const t = tokens[i];
|
|
195
|
+
if (t === BYPASS_NV || t === BYPASS_N) return `git ${subcommand} ${t}`;
|
|
196
|
+
if (COMMIT_FLAGS_WITH_VALUE.has(t)) i++;
|
|
197
|
+
}
|
|
198
|
+
} else if (subcommand === 'push') {
|
|
199
|
+
for (let i = flagsStart; i < tokens.length; i++) {
|
|
200
|
+
const t = tokens[i];
|
|
201
|
+
if (t === BYPASS_NV) return `git ${subcommand} ${t}`;
|
|
202
|
+
if (PUSH_FLAGS_WITH_VALUE.has(t)) i++;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function checkCommandForBypass(command) {
|
|
209
|
+
if (typeof command !== 'string' || !command) return null;
|
|
210
|
+
if (!command.includes('git')) return null;
|
|
211
|
+
const segments = splitSegments(command);
|
|
212
|
+
for (const seg of segments) {
|
|
213
|
+
const tokens = tokenize(seg);
|
|
214
|
+
const result = checkSegmentForBypass(tokens);
|
|
215
|
+
if (result) return result;
|
|
216
|
+
}
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
|
|
28
220
|
function run(inputOrRaw, options = {}) {
|
|
29
221
|
if (options.truncated) {
|
|
30
222
|
return {
|
|
@@ -33,30 +225,40 @@ function run(inputOrRaw, options = {}) {
|
|
|
33
225
|
'Refusing to bypass config-protection on a truncated payload.',
|
|
34
226
|
};
|
|
35
227
|
}
|
|
36
|
-
|
|
37
228
|
let input;
|
|
38
229
|
try {
|
|
39
230
|
input = typeof inputOrRaw === 'string' ? JSON.parse(inputOrRaw) : inputOrRaw;
|
|
40
231
|
} catch { return { exitCode: 0 }; }
|
|
41
|
-
|
|
42
232
|
const filePath = input?.tool_input?.path || input?.tool_input?.file_path || '';
|
|
43
|
-
if (
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
233
|
+
if (filePath) {
|
|
234
|
+
const basename = path.basename(filePath);
|
|
235
|
+
if (PROTECTED_FILES.has(basename)) {
|
|
236
|
+
return {
|
|
237
|
+
exitCode: 2,
|
|
238
|
+
stderr: `BLOCKED: Modifying ${basename} is not allowed. ` +
|
|
239
|
+
'Fix the source code to satisfy linter/formatter rules instead of ' +
|
|
240
|
+
'weakening the config. If this is a legitimate config change, ' +
|
|
241
|
+
'disable the config-protection hook temporarily.',
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
const command = input?.tool_input?.command || '';
|
|
246
|
+
if (command) {
|
|
247
|
+
const bypass = checkCommandForBypass(command);
|
|
248
|
+
if (bypass) {
|
|
249
|
+
return {
|
|
250
|
+
exitCode: 2,
|
|
251
|
+
stderr: `BLOCKED: "${bypass}" bypasses git verification hooks. ` +
|
|
252
|
+
'Verification hooks enforce project quality gates and must not be opted out. ' +
|
|
253
|
+
'Fix the failing check instead of skipping it. ' +
|
|
254
|
+
'If the hook is genuinely misconfigured, correct the hook configuration directly.',
|
|
255
|
+
};
|
|
256
|
+
}
|
|
54
257
|
}
|
|
55
|
-
|
|
56
258
|
return { exitCode: 0 };
|
|
57
259
|
}
|
|
58
260
|
|
|
59
|
-
module.exports = { run };
|
|
261
|
+
module.exports = { run, tokenize, splitSegments, checkCommandForBypass };
|
|
60
262
|
|
|
61
263
|
// Stdin fallback for spawnSync execution
|
|
62
264
|
if (require.main === module) {
|
package/src/cli/flow-kit.ts
CHANGED
|
@@ -3,7 +3,7 @@ import * as fs from "node:fs";
|
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { parseArgs, flagBool, flagString } from "../lib/args.js";
|
|
5
5
|
import { assertPathContained, copyDir, isoNow, readJson, walkFiles, writeJson } from "../lib/fs.js";
|
|
6
|
-
import { assertKitRepository } from "../flow-kit/validate.js";
|
|
6
|
+
import { assertKitRepository, deriveKitTargets } from "../flow-kit/validate.js";
|
|
7
7
|
import { activateCodexLocal, activateStrandsLocal } from "../runtime-adapters.js";
|
|
8
8
|
|
|
9
9
|
const REGISTRY_REL = path.join("kits", "local", "installed-kits.json");
|
|
@@ -132,13 +132,51 @@ function activate(argv: string[]): number {
|
|
|
132
132
|
return Array.isArray(result.errors) && result.errors.length ? 1 : 0;
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
+
/**
|
|
136
|
+
* inspect <kit-dir> [--json]
|
|
137
|
+
*
|
|
138
|
+
* Derives conformance level (K0/K1/K2) and consumer targets from a kit's
|
|
139
|
+
* observable asset classes. Exits 1 if the kit fails core container validation.
|
|
140
|
+
* Outputs stable JSON suitable for use by catalog tooling and CI.
|
|
141
|
+
*
|
|
142
|
+
* K-levels (issue #52):
|
|
143
|
+
* K0 valid core Flow Kit container — gates evaluable agentlessly by any Flow consumer.
|
|
144
|
+
* K1 K0 + Flow Agents extension assets present (skills/docs/adapters/evals/assets).
|
|
145
|
+
* K2 K1 + evals present (live evidence layer).
|
|
146
|
+
*
|
|
147
|
+
* Consumer targets derived from observable asset classes:
|
|
148
|
+
* flow always present at K0 (any Flow consumer: gates/definition-of-done)
|
|
149
|
+
* flow-agents present at K1+ (Flow Agents extension activated)
|
|
150
|
+
* <namespace> unknown top-level keys list verbatim as third-party consumer targets
|
|
151
|
+
*/
|
|
152
|
+
function inspect(argv: string[]): number {
|
|
153
|
+
const args = parseArgs(argv);
|
|
154
|
+
const kitDir = path.resolve(args.positionals[0] ?? ".");
|
|
155
|
+
const manifestPath = path.join(kitDir, "kit.json");
|
|
156
|
+
if (!fs.existsSync(manifestPath)) {
|
|
157
|
+
console.error(`inspect: kit.json not found at ${manifestPath}`);
|
|
158
|
+
return 1;
|
|
159
|
+
}
|
|
160
|
+
let manifest: Record<string, unknown>;
|
|
161
|
+
try {
|
|
162
|
+
manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")) as Record<string, unknown>;
|
|
163
|
+
} catch (err) {
|
|
164
|
+
console.error(`inspect: invalid JSON in ${manifestPath}: ${(err as Error).message}`);
|
|
165
|
+
return 1;
|
|
166
|
+
}
|
|
167
|
+
const result = deriveKitTargets(manifest);
|
|
168
|
+
console.log(JSON.stringify(result, null, 2));
|
|
169
|
+
return result.conformance.k0 ? 0 : 1;
|
|
170
|
+
}
|
|
171
|
+
|
|
135
172
|
export function main(argv = process.argv.slice(2)): number {
|
|
136
173
|
const [command, ...rest] = argv;
|
|
137
174
|
if (command === "install-local") return installLocal(rest);
|
|
138
175
|
if (command === "list") return list(rest);
|
|
139
176
|
if (command === "status") return status(rest);
|
|
140
177
|
if (command === "activate") return activate(rest);
|
|
141
|
-
|
|
178
|
+
if (command === "inspect") return inspect(rest);
|
|
179
|
+
console.error("usage: flow-kit <install-local|list|status|activate|inspect> ...");
|
|
142
180
|
return 2;
|
|
143
181
|
}
|
|
144
182
|
|
package/src/flow-kit/validate.ts
CHANGED
|
@@ -4,6 +4,121 @@ import { readJson } from "../lib/fs.js";
|
|
|
4
4
|
|
|
5
5
|
const ASSET_CLASSES = ["flows", "skills", "docs", "adapters", "evals", "assets"] as const;
|
|
6
6
|
|
|
7
|
+
// Core container fields owned by kontourai/flow (flow-kit-container.schema.json).
|
|
8
|
+
// agent-extension fields are skills, docs, adapters, evals, assets.
|
|
9
|
+
const CORE_CONTAINER_FIELDS = new Set(["schema_version", "id", "name", "description", "product_name", "flows"]);
|
|
10
|
+
const AGENT_EXTENSION_CLASSES = new Set(["skills", "docs", "adapters", "evals", "assets"]);
|
|
11
|
+
|
|
12
|
+
export type KitTargetConsumer =
|
|
13
|
+
| "flow"
|
|
14
|
+
| "flow-agents"
|
|
15
|
+
| string; // third-party extension namespaces listed verbatim
|
|
16
|
+
|
|
17
|
+
export interface KitConformanceLevel {
|
|
18
|
+
/** K0: valid core Flow Kit container with at least one flow (gates evaluable agentlessly). */
|
|
19
|
+
k0: boolean;
|
|
20
|
+
/** K1: K0 + at least one Flow Agents extension asset class present (skills/docs/adapters/evals/assets). */
|
|
21
|
+
k1: boolean;
|
|
22
|
+
/** K2: K1 + evals present (live evidence layer). */
|
|
23
|
+
k2: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface KitTargetsResult {
|
|
27
|
+
kit_id: string;
|
|
28
|
+
kit_name: string;
|
|
29
|
+
conformance: KitConformanceLevel;
|
|
30
|
+
/** Derived consumer targets based on observable asset classes. */
|
|
31
|
+
targets: KitTargetConsumer[];
|
|
32
|
+
/** Extension field namespaces that are not Flow or Flow Agents-owned. */
|
|
33
|
+
third_party_extensions: string[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Validates that the manifest satisfies the core Flow Kit container contract
|
|
38
|
+
* (as specified by kontourai/flow PR #67) with all agent-extension fields stripped.
|
|
39
|
+
* Returns a list of violation messages (empty = valid).
|
|
40
|
+
*
|
|
41
|
+
* The degradation invariant: every Flow Agents Kit MUST remain a valid core
|
|
42
|
+
* Flow Kit container when agent-extension fields are ignored.
|
|
43
|
+
*/
|
|
44
|
+
export function validateCoreContainer(manifest: Record<string, unknown>, label: string): string[] {
|
|
45
|
+
const errors: string[] = [];
|
|
46
|
+
if (manifest.schema_version !== "1.0") {
|
|
47
|
+
errors.push(`${label}: .schema_version must be "1.0"`);
|
|
48
|
+
}
|
|
49
|
+
if (typeof manifest.id !== "string" || !/^[a-z0-9][a-z0-9-]*$/.test(manifest.id)) {
|
|
50
|
+
errors.push(`${label}: .id must be a stable kebab-case string`);
|
|
51
|
+
}
|
|
52
|
+
if (typeof manifest.name !== "string" || !manifest.name.trim()) {
|
|
53
|
+
errors.push(`${label}: .name must be a non-empty string`);
|
|
54
|
+
}
|
|
55
|
+
if (!Array.isArray(manifest.flows) || manifest.flows.length === 0) {
|
|
56
|
+
errors.push(`${label}: .flows must be a non-empty list`);
|
|
57
|
+
} else {
|
|
58
|
+
manifest.flows.forEach((entry: unknown, index: number) => {
|
|
59
|
+
if (typeof entry !== "object" || entry === null) {
|
|
60
|
+
errors.push(`${label}: flows[${index}] must be an object`);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const flow = entry as Record<string, unknown>;
|
|
64
|
+
if (typeof flow.id !== "string" || !flow.id) {
|
|
65
|
+
errors.push(`${label}: flows[${index}].id must be a string`);
|
|
66
|
+
}
|
|
67
|
+
if (typeof flow.path !== "string" || !flow.path) {
|
|
68
|
+
errors.push(`${label}: flows[${index}].path must be a string`);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
return errors;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Derives the consumer-target level (K0/K1/K2) and target audience list from
|
|
77
|
+
* observable asset classes in the kit manifest. Does not require file I/O.
|
|
78
|
+
*
|
|
79
|
+
* Derivation rules (from kontourai/flow-agents#52 and Brian's layering review):
|
|
80
|
+
* - K0: valid core container (schema_version, id, name, flows non-empty).
|
|
81
|
+
* - K1: K0 + any Flow Agents extension field present (skills/docs/adapters/evals/assets).
|
|
82
|
+
* - K2: K1 + evals present.
|
|
83
|
+
* - targets.flow: always present when K0 (any Flow consumer can evaluate gates).
|
|
84
|
+
* - targets.flow-agents: present when K1 (agent extension assets activate in >=1 harness).
|
|
85
|
+
* - third-party: any top-level keys that are not core fields and not Flow Agents extension classes.
|
|
86
|
+
*/
|
|
87
|
+
export function deriveKitTargets(manifest: Record<string, unknown>): KitTargetsResult {
|
|
88
|
+
const kitId = typeof manifest.id === "string" ? manifest.id : "<unknown>";
|
|
89
|
+
const kitName = typeof manifest.name === "string" ? manifest.name : "<unknown>";
|
|
90
|
+
|
|
91
|
+
const coreErrors = validateCoreContainer(manifest, "kit.json");
|
|
92
|
+
const k0 = coreErrors.length === 0;
|
|
93
|
+
|
|
94
|
+
const hasAgentExtension = AGENT_EXTENSION_CLASSES.size > 0 &&
|
|
95
|
+
[...AGENT_EXTENSION_CLASSES].some((cls) => Array.isArray(manifest[cls]) && (manifest[cls] as unknown[]).length > 0);
|
|
96
|
+
|
|
97
|
+
const hasEvals = Array.isArray(manifest["evals"]) && (manifest["evals"] as unknown[]).length > 0;
|
|
98
|
+
|
|
99
|
+
const k1 = k0 && hasAgentExtension;
|
|
100
|
+
const k2 = k1 && hasEvals;
|
|
101
|
+
|
|
102
|
+
// Detect third-party extension namespaces: top-level keys that are neither
|
|
103
|
+
// core fields nor Flow Agents extension classes.
|
|
104
|
+
const thirdPartyExtensions: string[] = Object.keys(manifest)
|
|
105
|
+
.filter((key) => !CORE_CONTAINER_FIELDS.has(key) && !AGENT_EXTENSION_CLASSES.has(key))
|
|
106
|
+
.sort();
|
|
107
|
+
|
|
108
|
+
const targets: KitTargetConsumer[] = [];
|
|
109
|
+
if (k0) targets.push("flow");
|
|
110
|
+
if (k1) targets.push("flow-agents");
|
|
111
|
+
for (const ns of thirdPartyExtensions) targets.push(ns);
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
kit_id: kitId,
|
|
115
|
+
kit_name: kitName,
|
|
116
|
+
conformance: { k0, k1, k2 },
|
|
117
|
+
targets,
|
|
118
|
+
third_party_extensions: thirdPartyExtensions,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
7
122
|
export function validateKitRepository(kitDir: string): string[] {
|
|
8
123
|
const errors: string[] = [];
|
|
9
124
|
const manifestPath = path.join(kitDir, "kit.json");
|
|
@@ -20,6 +135,18 @@ export function validateKitRepository(kitDir: string): string[] {
|
|
|
20
135
|
}
|
|
21
136
|
if (typeof manifest.name !== "string" || !manifest.name.trim()) errors.push(`${manifestPath}: .name must be a non-empty string`);
|
|
22
137
|
|
|
138
|
+
// Degradation invariant: every Flow Agents Kit must remain a valid core Flow Kit container
|
|
139
|
+
// when agent-extension fields are stripped. Strip extensions and re-validate core contract.
|
|
140
|
+
const coreManifest: Record<string, unknown> = {};
|
|
141
|
+
for (const key of Object.keys(manifest)) {
|
|
142
|
+
if (CORE_CONTAINER_FIELDS.has(key)) coreManifest[key] = manifest[key];
|
|
143
|
+
}
|
|
144
|
+
const coreErrors = validateCoreContainer(coreManifest, manifestPath);
|
|
145
|
+
for (const err of coreErrors) {
|
|
146
|
+
// Deduplicate: only add if not already covered by top-level checks above.
|
|
147
|
+
if (!errors.some((existing) => existing === err)) errors.push(err);
|
|
148
|
+
}
|
|
149
|
+
|
|
23
150
|
for (const section of ASSET_CLASSES) {
|
|
24
151
|
const entries = manifest[section];
|
|
25
152
|
if (entries === undefined) continue;
|