@kontourai/flow-agents 0.4.0 → 1.0.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/.github/workflows/kit-gates-demo.yml +171 -0
- package/CHANGELOG.md +43 -0
- package/CONTEXT.md +1 -1
- package/README.md +13 -2
- package/build/src/cli/flow-kit.js +175 -6
- package/build/src/cli/validate-source-tree.js +19 -2
- package/build/src/flow-kit/validate.js +98 -0
- package/build/src/runtime-adapters.js +1 -1
- package/build/src/tools/validate-source-tree.js +3 -2
- package/context/scripts/hooks/config-protection.js +217 -15
- package/docs/fixture-ownership.md +2 -1
- 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_activate_npx_context.sh +134 -0
- package/evals/integration/test_fixture_retirement_audit.sh +2 -2
- package/evals/integration/test_flow_kit_install_git.sh +163 -0
- 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 +162 -5
- package/src/cli/validate-source-tree.ts +7 -1
- package/src/flow-kit/validate.ts +127 -0
- package/src/runtime-adapters.ts +1 -1
- package/src/tools/validate-source-tree.ts +3 -2
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.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",
|
|
@@ -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
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import * as child_process from "node:child_process";
|
|
1
2
|
import * as crypto from "node:crypto";
|
|
2
3
|
import * as fs from "node:fs";
|
|
4
|
+
import * as os from "node:os";
|
|
3
5
|
import * as path from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
4
7
|
import { parseArgs, flagBool, flagString } from "../lib/args.js";
|
|
5
8
|
import { assertPathContained, copyDir, isoNow, readJson, walkFiles, writeJson } from "../lib/fs.js";
|
|
6
|
-
import { assertKitRepository } from "../flow-kit/validate.js";
|
|
9
|
+
import { assertKitRepository, deriveKitTargets } from "../flow-kit/validate.js";
|
|
7
10
|
import { activateCodexLocal, activateStrandsLocal } from "../runtime-adapters.js";
|
|
8
11
|
|
|
9
12
|
const REGISTRY_REL = path.join("kits", "local", "installed-kits.json");
|
|
@@ -31,6 +34,22 @@ function contentHash(root: string): string {
|
|
|
31
34
|
return `sha256:${hash.digest("hex")}`;
|
|
32
35
|
}
|
|
33
36
|
|
|
37
|
+
/** Content hash that excludes .git and other VCS/cache directories (for install-git clones). */
|
|
38
|
+
function kitContentHash(root: string): string {
|
|
39
|
+
const EXCLUDE_DIRS = new Set([".git", "__pycache__", ".pytest_cache"]);
|
|
40
|
+
const hash = crypto.createHash("sha256");
|
|
41
|
+
for (const file of walkFiles(root)) {
|
|
42
|
+
const parts = path.relative(root, file).split(path.sep);
|
|
43
|
+
if (parts.some((p) => EXCLUDE_DIRS.has(p))) continue;
|
|
44
|
+
const rel = parts.join("/");
|
|
45
|
+
hash.update(rel);
|
|
46
|
+
hash.update("\0");
|
|
47
|
+
hash.update(fs.readFileSync(file));
|
|
48
|
+
hash.update("\0");
|
|
49
|
+
}
|
|
50
|
+
return `sha256:${hash.digest("hex")}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
34
53
|
function installLocal(argv: string[]): number {
|
|
35
54
|
const args = parseArgs(argv);
|
|
36
55
|
const source = path.resolve(args.positionals[0] ?? "");
|
|
@@ -39,12 +58,10 @@ function installLocal(argv: string[]): number {
|
|
|
39
58
|
try {
|
|
40
59
|
manifest = assertKitRepository(source);
|
|
41
60
|
} catch (error) {
|
|
42
|
-
console.log("warning: Flow validation surface unavailable; local kit check uses the minimal Flow Definition fallback");
|
|
43
61
|
console.log("Flow Kit repository validation failed:");
|
|
44
62
|
for (const diagnostic of ((error as Error & { diagnostics?: string[] }).diagnostics ?? [(error as Error).message])) console.log(` - ${diagnostic}`);
|
|
45
63
|
return 1;
|
|
46
64
|
}
|
|
47
|
-
console.log("warning: Flow validation surface unavailable; local kit check uses the minimal Flow Definition fallback");
|
|
48
65
|
const kitId = String(manifest.id);
|
|
49
66
|
const hash = contentHash(source);
|
|
50
67
|
const registry = loadRegistry(dest);
|
|
@@ -132,14 +149,154 @@ function activate(argv: string[]): number {
|
|
|
132
149
|
return Array.isArray(result.errors) && result.errors.length ? 1 : 0;
|
|
133
150
|
}
|
|
134
151
|
|
|
152
|
+
/**
|
|
153
|
+
* inspect <kit-dir> [--json]
|
|
154
|
+
*
|
|
155
|
+
* Derives conformance level (K0/K1/K2) and consumer targets from a kit's
|
|
156
|
+
* observable asset classes. Exits 1 if the kit fails core container validation.
|
|
157
|
+
* Outputs stable JSON suitable for use by catalog tooling and CI.
|
|
158
|
+
*
|
|
159
|
+
* K-levels (issue #52):
|
|
160
|
+
* K0 valid core Flow Kit container — gates evaluable agentlessly by any Flow consumer.
|
|
161
|
+
* K1 K0 + Flow Agents extension assets present (skills/docs/adapters/evals/assets).
|
|
162
|
+
* K2 K1 + evals present (live evidence layer).
|
|
163
|
+
*
|
|
164
|
+
* Consumer targets derived from observable asset classes:
|
|
165
|
+
* flow always present at K0 (any Flow consumer: gates/definition-of-done)
|
|
166
|
+
* flow-agents present at K1+ (Flow Agents extension activated)
|
|
167
|
+
* <namespace> unknown top-level keys list verbatim as third-party consumer targets
|
|
168
|
+
*/
|
|
169
|
+
function inspect(argv: string[]): number {
|
|
170
|
+
const args = parseArgs(argv);
|
|
171
|
+
const kitDir = path.resolve(args.positionals[0] ?? ".");
|
|
172
|
+
const manifestPath = path.join(kitDir, "kit.json");
|
|
173
|
+
if (!fs.existsSync(manifestPath)) {
|
|
174
|
+
console.error(`inspect: kit.json not found at ${manifestPath}`);
|
|
175
|
+
return 1;
|
|
176
|
+
}
|
|
177
|
+
let manifest: Record<string, unknown>;
|
|
178
|
+
try {
|
|
179
|
+
manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")) as Record<string, unknown>;
|
|
180
|
+
} catch (err) {
|
|
181
|
+
console.error(`inspect: invalid JSON in ${manifestPath}: ${(err as Error).message}`);
|
|
182
|
+
return 1;
|
|
183
|
+
}
|
|
184
|
+
const result = deriveKitTargets(manifest);
|
|
185
|
+
console.log(JSON.stringify(result, null, 2));
|
|
186
|
+
return result.conformance.k0 ? 0 : 1;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* install-git <repo-url>[#ref] [--ref <branch|tag|sha>] [--dest <path>] [--force] [--update]
|
|
192
|
+
*
|
|
193
|
+
* Shallow-clones a remote git repository to a temporary directory, validates the kit
|
|
194
|
+
* container with the same logic used by install-local, then delegates to the existing
|
|
195
|
+
* install path. Supports an optional #ref fragment in the URL or a separate --ref flag.
|
|
196
|
+
*
|
|
197
|
+
* Implements kontourai/flow-agents#56 (git-ref install surface).
|
|
198
|
+
*/
|
|
199
|
+
function installGit(argv: string[]): number {
|
|
200
|
+
const args = parseArgs(argv);
|
|
201
|
+
const rawUrl = args.positionals[0] ?? "";
|
|
202
|
+
if (!rawUrl) {
|
|
203
|
+
console.error("install-git: missing <repo-url> argument");
|
|
204
|
+
console.error("usage: flow-kit install-git <repo-url>[#ref] [--ref <branch|tag|sha>] [--dest <path>]");
|
|
205
|
+
return 2;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Parse ref: #fragment in URL takes precedence over --ref flag.
|
|
209
|
+
let repoUrl = rawUrl;
|
|
210
|
+
let ref: string | null = null;
|
|
211
|
+
const hashIdx = rawUrl.indexOf("#");
|
|
212
|
+
if (hashIdx !== -1) {
|
|
213
|
+
repoUrl = rawUrl.slice(0, hashIdx);
|
|
214
|
+
ref = rawUrl.slice(hashIdx + 1) || null;
|
|
215
|
+
}
|
|
216
|
+
if (!ref) ref = flagString(args.flags, "ref") ?? null;
|
|
217
|
+
|
|
218
|
+
const dest = path.resolve(flagString(args.flags, "dest", ".") ?? ".");
|
|
219
|
+
const force = flagBool(args.flags, "force") ?? false;
|
|
220
|
+
const update = flagBool(args.flags, "update") ?? false;
|
|
221
|
+
|
|
222
|
+
// Shallow-clone into a temporary directory.
|
|
223
|
+
const tmpBase = fs.mkdtempSync(path.join(os.tmpdir(), "flow-kit-git-"));
|
|
224
|
+
try {
|
|
225
|
+
const cloneArgs = ["clone", "--depth", "1"];
|
|
226
|
+
if (ref) cloneArgs.push("--branch", ref);
|
|
227
|
+
cloneArgs.push("--", repoUrl, tmpBase);
|
|
228
|
+
try {
|
|
229
|
+
child_process.execFileSync("git", cloneArgs, { stdio: ["ignore", "pipe", "pipe"] });
|
|
230
|
+
} catch (err) {
|
|
231
|
+
const msg = err instanceof Error && (err as NodeJS.ErrnoException & { stderr?: Buffer }).stderr
|
|
232
|
+
? ((err as NodeJS.ErrnoException & { stderr?: Buffer }).stderr as Buffer).toString().trim()
|
|
233
|
+
: String(err);
|
|
234
|
+
console.error(`install-git: git clone failed: ${msg}`);
|
|
235
|
+
return 1;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Validate the cloned kit using the same logic as install-local.
|
|
239
|
+
let manifest: Record<string, unknown>;
|
|
240
|
+
try {
|
|
241
|
+
manifest = assertKitRepository(tmpBase);
|
|
242
|
+
} catch (error) {
|
|
243
|
+
console.log("Flow Kit repository validation failed:");
|
|
244
|
+
for (const diagnostic of ((error as Error & { diagnostics?: string[] }).diagnostics ?? [(error as Error).message])) {
|
|
245
|
+
console.log(` - ${diagnostic}`);
|
|
246
|
+
}
|
|
247
|
+
return 1;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Delegate to the shared install logic (copy + registry update).
|
|
251
|
+
const kitId = String(manifest.id);
|
|
252
|
+
const hash = kitContentHash(tmpBase);
|
|
253
|
+
const registry = loadRegistry(dest);
|
|
254
|
+
const existing = registry.kits.find((entry) => entry.id === kitId);
|
|
255
|
+
const target = installedPath(dest, kitId);
|
|
256
|
+
assertPathContained(dest, target);
|
|
257
|
+
const sourceText = repoUrl + (ref ? `#${ref}` : "");
|
|
258
|
+
if (existing && existing.source !== sourceText && !update) {
|
|
259
|
+
console.log(`conflict: kit '${kitId}' is already installed from ${existing.source}; rerun with --update to replace it`);
|
|
260
|
+
return 2;
|
|
261
|
+
}
|
|
262
|
+
if (existing && existing.source === sourceText && existing.hash === hash && fs.existsSync(target) && !force) {
|
|
263
|
+
console.log(`kit '${kitId}' is already installed from ${sourceText}`);
|
|
264
|
+
return 0;
|
|
265
|
+
}
|
|
266
|
+
copyDir(tmpBase, target);
|
|
267
|
+
const entry: Record<string, unknown> = {
|
|
268
|
+
id: kitId,
|
|
269
|
+
source: sourceText,
|
|
270
|
+
hash,
|
|
271
|
+
installed_at: existing && existing.source === sourceText && !update ? existing.installed_at : isoNow(),
|
|
272
|
+
installed_path: target,
|
|
273
|
+
state: "installed",
|
|
274
|
+
};
|
|
275
|
+
if (typeof manifest.version === "string" && manifest.version) entry.version = manifest.version;
|
|
276
|
+
registry.kits = existing ? registry.kits.map((item) => item.id === kitId ? entry : item) : [...registry.kits, entry];
|
|
277
|
+
writeJson(registryPath(dest), registry);
|
|
278
|
+
console.log(`${existing ? "updated" : "installed"} git kit '${kitId}' from ${sourceText} at ${target}`);
|
|
279
|
+
return 0;
|
|
280
|
+
} finally {
|
|
281
|
+
fs.rmSync(tmpBase, { recursive: true, force: true });
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
135
285
|
export function main(argv = process.argv.slice(2)): number {
|
|
136
286
|
const [command, ...rest] = argv;
|
|
137
287
|
if (command === "install-local") return installLocal(rest);
|
|
288
|
+
if (command === "install-git") return installGit(rest);
|
|
138
289
|
if (command === "list") return list(rest);
|
|
139
290
|
if (command === "status") return status(rest);
|
|
140
291
|
if (command === "activate") return activate(rest);
|
|
141
|
-
|
|
292
|
+
if (command === "inspect") return inspect(rest);
|
|
293
|
+
console.error("usage: flow-kit <install-local|install-git|list|status|activate|inspect> ...");
|
|
142
294
|
return 2;
|
|
143
295
|
}
|
|
144
296
|
|
|
145
|
-
|
|
297
|
+
// Use process.exitCode (not process.exit) to allow stdout to be flushed before exit.
|
|
298
|
+
// Resolve real paths to handle symlinks (e.g. /tmp -> /private/tmp on macOS) so the
|
|
299
|
+
// entry-point guard fires correctly when the module is loaded directly as a script.
|
|
300
|
+
const _selfRealPath = (() => { try { return fs.realpathSync(fileURLToPath(import.meta.url)); } catch { return fileURLToPath(import.meta.url); } })();
|
|
301
|
+
const _argv1RealPath = (() => { try { return fs.realpathSync(process.argv[1]); } catch { return process.argv[1]; } })();
|
|
302
|
+
if (_selfRealPath === _argv1RealPath) { process.exitCode = main(); }
|
|
@@ -27,4 +27,10 @@ export function main(argv = process.argv.slice(2)): number {
|
|
|
27
27
|
return 0;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
// Use process.exitCode (not process.exit) to allow stdout to be flushed before exit.
|
|
31
|
+
// Resolve real paths to handle symlinks (e.g. /tmp -> /private/tmp on macOS).
|
|
32
|
+
import * as _fsVST from "node:fs";
|
|
33
|
+
import { fileURLToPath as _ftpVST } from "node:url";
|
|
34
|
+
const _selfVST = (() => { try { return _fsVST.realpathSync(_ftpVST(import.meta.url)); } catch { return _ftpVST(import.meta.url); } })();
|
|
35
|
+
const _argv1VST = (() => { try { return _fsVST.realpathSync(process.argv[1]); } catch { return process.argv[1]; } })();
|
|
36
|
+
if (_selfVST === _argv1VST) { process.exitCode = main(); }
|