@kontourai/flow-agents 1.2.0 → 1.3.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/ci.yml +6 -1
- package/.github/workflows/kit-gates-demo.yml +6 -2
- package/CHANGELOG.md +25 -0
- package/CONTRIBUTING.md +30 -0
- package/agents/dev.json +1 -1
- package/agents/tool-planner.json +1 -1
- package/build/src/cli/workflow-sidecar.js +70 -5
- package/build/src/flow-kit/validate.js +32 -1
- package/build/src/tools/build-universal-bundles.js +14 -0
- package/console.telemetry.json +1 -1
- package/docs/adr/0004-gates-expect-surface-claims.md +7 -7
- package/docs/kit-authoring-guide.md +99 -6
- package/docs/operating-layers.md +2 -2
- package/docs/veritas-integration.md +4 -4
- package/docs/workflow-eval-strategy.md +2 -2
- package/docs/workflow-usage-guide.md +1 -1
- package/evals/acceptance/test_opencode_harness.sh +18 -10
- package/evals/acceptance/test_pi_harness.sh +10 -6
- package/evals/ci/run-baseline.sh +1 -1
- package/evals/fixtures/flow-kit-repository/mixed-runtime-kit/flows/runtime.flow.json +4 -4
- package/evals/fixtures/flow-kit-repository/valid-local-kit/flows/review.flow.json +4 -4
- package/evals/fixtures/kit-conformance-levels/k0-flows-only/flows/review.flow.json +4 -4
- package/evals/fixtures/kit-conformance-levels/k1-agent-extension/flows/build.flow.json +4 -4
- package/evals/fixtures/kit-conformance-levels/k2-with-evals/flows/synthesize.flow.json +4 -4
- package/evals/fixtures/kit-conformance-levels/third-party-extension/flows/review.flow.json +4 -4
- package/evals/fixtures/surface-trust/accepted-claim-trust-report.json +2 -2
- package/evals/fixtures/surface-trust/artifact-absent.json +2 -2
- package/evals/fixtures/surface-trust/integrity-mismatch-trust-report.json +2 -2
- package/evals/fixtures/surface-trust/missing-authority-trust-report.json +2 -2
- package/evals/fixtures/surface-trust/provider-absent.json +2 -2
- package/evals/fixtures/surface-trust/rejected-claim-trust-report.json +2 -2
- package/evals/fixtures/surface-trust/stale-claim-trust-snapshot.json +2 -2
- package/evals/integration/test_console_learning_projection.sh +1 -1
- package/evals/integration/test_goal_fit_hook.sh +144 -0
- package/evals/integration/test_kit_conformance_levels.sh +55 -1
- package/evals/integration/test_workflow_sidecar_writer.sh +9 -9
- package/evals/static/test_package.sh +3 -3
- package/evals/static/test_workflow_skills.sh +4 -4
- package/kits/builder/flows/build.flow.json +48 -48
- package/kits/builder/flows/shape.flow.json +36 -36
- package/kits/knowledge/adapters/obsidian-store/index.js +137 -26
- package/kits/knowledge/evals/contract-suite/suite.test.js +90 -0
- package/kits/knowledge/flows/compile.flow.json +12 -12
- package/kits/knowledge/flows/consolidate.flow.json +16 -16
- package/kits/knowledge/flows/ingest.flow.json +12 -12
- package/kits/knowledge/flows/retire.flow.json +16 -16
- package/kits/knowledge/flows/store-contract.flow.json +12 -12
- package/kits/knowledge/flows/synthesize.flow.json +16 -16
- package/kits/release-evidence/flows/release-evidence.flow.json +3 -3
- package/package.json +5 -2
- package/schemas/workflow-evidence.schema.json +2 -1
- package/scripts/hooks/stop-goal-fit.js +66 -18
- package/src/cli/workflow-sidecar.ts +62 -4
- package/src/flow-kit/validate.ts +55 -1
- package/src/tools/build-universal-bundles.ts +14 -0
|
@@ -14,12 +14,12 @@
|
|
|
14
14
|
"expects": [
|
|
15
15
|
{
|
|
16
16
|
"id": "similar-sources-found",
|
|
17
|
-
"kind": "
|
|
17
|
+
"kind": "trust.bundle",
|
|
18
18
|
"required": true,
|
|
19
19
|
"description": "At least one compiled record similar to the target concept has been identified via the similarity detector. Similarity v1 uses category match + link-overlap heuristic. The result is a cluster of source record IDs to synthesize from.",
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"
|
|
20
|
+
"bundle_claim": {
|
|
21
|
+
"claimType": "knowledge.synthesize.cluster",
|
|
22
|
+
"subjectType": "artifact",
|
|
23
23
|
"accepted_statuses": ["trusted", "accepted"]
|
|
24
24
|
}
|
|
25
25
|
}
|
|
@@ -30,12 +30,12 @@
|
|
|
30
30
|
"expects": [
|
|
31
31
|
{
|
|
32
32
|
"id": "proposal-recorded",
|
|
33
|
-
"kind": "
|
|
33
|
+
"kind": "trust.bundle",
|
|
34
34
|
"required": true,
|
|
35
35
|
"description": "A proposal carrying source refs (proposer_id + source_ids in evidence) has been recorded via the store propose op. The concept body is NOT modified at this step — only a proposes link and mutation log entry are created.",
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
36
|
+
"bundle_claim": {
|
|
37
|
+
"claimType": "knowledge.synthesize.proposal",
|
|
38
|
+
"subjectType": "artifact",
|
|
39
39
|
"accepted_statuses": ["trusted", "accepted"]
|
|
40
40
|
}
|
|
41
41
|
}
|
|
@@ -46,12 +46,12 @@
|
|
|
46
46
|
"expects": [
|
|
47
47
|
{
|
|
48
48
|
"id": "proposal-carries-source-refs",
|
|
49
|
-
"kind": "
|
|
49
|
+
"kind": "trust.bundle",
|
|
50
50
|
"required": true,
|
|
51
51
|
"description": "The proposal evidence includes source_ids referencing every compiled record that contributed to the proposed summary. Gate rejects if source_ids is empty or any referenced record does not exist.",
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"
|
|
52
|
+
"bundle_claim": {
|
|
53
|
+
"claimType": "knowledge.synthesize.evidence",
|
|
54
|
+
"subjectType": "artifact",
|
|
55
55
|
"accepted_statuses": ["trusted", "accepted"]
|
|
56
56
|
}
|
|
57
57
|
}
|
|
@@ -62,12 +62,12 @@
|
|
|
62
62
|
"expects": [
|
|
63
63
|
{
|
|
64
64
|
"id": "mutation-gate-decision",
|
|
65
|
-
"kind": "
|
|
65
|
+
"kind": "trust.bundle",
|
|
66
66
|
"required": true,
|
|
67
67
|
"description": "A gate decision (apply or reject) has been recorded. If applied: concept body is updated via the store apply op with rationale and all contributing source_ids in provenance. If rejected: the store reject op is called, the concept body is byte-identical to its pre-proposal state, and only a rejection log entry is appended.",
|
|
68
|
-
"
|
|
69
|
-
"
|
|
70
|
-
"
|
|
68
|
+
"bundle_claim": {
|
|
69
|
+
"claimType": "knowledge.synthesize.gate-decision",
|
|
70
|
+
"subjectType": "artifact",
|
|
71
71
|
"accepted_statuses": ["trusted", "accepted"]
|
|
72
72
|
}
|
|
73
73
|
}
|
|
@@ -13,12 +13,12 @@
|
|
|
13
13
|
"expects": [
|
|
14
14
|
{
|
|
15
15
|
"id": "release-claim-present",
|
|
16
|
-
"kind": "
|
|
16
|
+
"kind": "trust.bundle",
|
|
17
17
|
"required": true,
|
|
18
18
|
"description": "A trusted release.evidence claim must be attached before this gate can pass.",
|
|
19
19
|
"explore_hint": "Attach a trust-report JSON file with claim type release.evidence and status trusted.",
|
|
20
|
-
"
|
|
21
|
-
"
|
|
20
|
+
"bundle_claim": {
|
|
21
|
+
"claimType": "release.evidence",
|
|
22
22
|
"accepted_statuses": [
|
|
23
23
|
"trusted"
|
|
24
24
|
]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kontourai/flow-agents",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.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",
|
|
@@ -133,6 +133,9 @@
|
|
|
133
133
|
"typescript": "^6.0.3"
|
|
134
134
|
},
|
|
135
135
|
"dependencies": {
|
|
136
|
-
"@kontourai/flow": "
|
|
136
|
+
"@kontourai/flow": "~1.3.0"
|
|
137
|
+
},
|
|
138
|
+
"optionalDependencies": {
|
|
139
|
+
"hachure": "^0.4.0"
|
|
137
140
|
}
|
|
138
141
|
}
|
|
@@ -239,7 +239,8 @@
|
|
|
239
239
|
"properties": {
|
|
240
240
|
"artifact_kind": {
|
|
241
241
|
"type": "string",
|
|
242
|
-
"
|
|
242
|
+
"description": "Hachure-aligned artifact kind. trust.bundle is the canonical value; TrustReport and Trust Snapshot are legacy aliases.",
|
|
243
|
+
"enum": ["trust.bundle", "TrustReport", "Trust Snapshot"]
|
|
243
244
|
},
|
|
244
245
|
"artifact_ref": {
|
|
245
246
|
"type": "string",
|
|
@@ -80,6 +80,15 @@ function hasSidecars(dir) {
|
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Returns true if a line of validator output looks like a validator-environment
|
|
85
|
+
* error (shell/npm error, tsc missing, spawn failure) rather than a real
|
|
86
|
+
* artifact validation message. Environment errors must never block goal-fit.
|
|
87
|
+
*/
|
|
88
|
+
function isEnvironmentError(line) {
|
|
89
|
+
return /tsc[:\s]|command not found|npm ERR!|npm error|ENOENT|EACCES|Cannot find module|node_modules\/.bin|TypeScript version|version conflict|error TS[0-9]/i.test(line);
|
|
90
|
+
}
|
|
91
|
+
|
|
83
92
|
function sidecarValidation(root, artifactDir) {
|
|
84
93
|
const requireSidecars = String(process.env.FLOW_AGENTS_REQUIRE_SIDECARS || '').toLowerCase() === 'true';
|
|
85
94
|
const requireCritique = String(process.env.FLOW_AGENTS_REQUIRE_CRITIQUE || '').toLowerCase() === 'true';
|
|
@@ -88,8 +97,6 @@ function sidecarValidation(root, artifactDir) {
|
|
|
88
97
|
const packageRoot = fs.existsSync(path.join(root, 'package.json'))
|
|
89
98
|
? root
|
|
90
99
|
: path.resolve(__dirname, '..', '..');
|
|
91
|
-
const packageJson = path.join(packageRoot, 'package.json');
|
|
92
|
-
if (!fs.existsSync(packageJson)) return [`${relative(root, artifactDir)} sidecar validation: package.json is missing; cannot run TypeScript workflow validator.`];
|
|
93
100
|
|
|
94
101
|
let sidecarFiles = [];
|
|
95
102
|
try {
|
|
@@ -112,26 +119,67 @@ function sidecarValidation(root, artifactDir) {
|
|
|
112
119
|
|
|
113
120
|
if (sidecarFiles.length === 0) return [];
|
|
114
121
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
if
|
|
118
|
-
|
|
119
|
-
|
|
122
|
+
// Part 1 fix: invoke the already-built validator directly via `node`, bypassing
|
|
123
|
+
// `npm run build` (tsc). npm-installed packages ship build/ in the package files,
|
|
124
|
+
// so the compiled JS is always available. Only fall back to npm run if build/ is
|
|
125
|
+
// absent (a raw dev checkout that hasn't been built yet).
|
|
126
|
+
const builtValidator = path.join(packageRoot, 'build', 'src', 'cli', 'validate-workflow-artifacts.js');
|
|
127
|
+
const hasBuild = fs.existsSync(builtValidator);
|
|
128
|
+
|
|
129
|
+
const validatorArgs = ['--skip-markdown-validation'];
|
|
130
|
+
if (requireSidecars) validatorArgs.push('--require-sidecars');
|
|
131
|
+
if (requireCritique) validatorArgs.push('--require-critique');
|
|
132
|
+
validatorArgs.push(artifactDir);
|
|
133
|
+
|
|
134
|
+
let result;
|
|
135
|
+
if (hasBuild) {
|
|
136
|
+
// Direct node invocation: no tsc, no npm build step, works from any npm install.
|
|
137
|
+
result = spawnSync(process.execPath, [builtValidator, ...validatorArgs], {
|
|
138
|
+
cwd: packageRoot,
|
|
139
|
+
encoding: 'utf8',
|
|
140
|
+
timeout: 30000,
|
|
141
|
+
});
|
|
142
|
+
} else {
|
|
143
|
+
// Dev checkout without build/: fall back to npm run (may trigger tsc).
|
|
144
|
+
// If this also fails due to environment issues, Part 2 handles it below.
|
|
145
|
+
const npmArgs = ['run', 'workflow:validate-artifacts', '--silent', '--', ...validatorArgs];
|
|
146
|
+
result = spawnSync('npm', npmArgs, {
|
|
147
|
+
cwd: packageRoot,
|
|
148
|
+
encoding: 'utf8',
|
|
149
|
+
timeout: 30000,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
120
152
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
153
|
+
// Part 2 fix: treat validator-environment failures as SKIP, never as blocking.
|
|
154
|
+
// A spawn error (ENOENT, timeout) means the validator couldn't run at all.
|
|
155
|
+
if (result.error) {
|
|
156
|
+
// Validator couldn't be launched — environment issue, not a goal-fit failure.
|
|
157
|
+
return [`${relative(root, artifactDir)} sidecar validation skipped: validator could not run (${result.error.code || result.error.message})`];
|
|
158
|
+
}
|
|
126
159
|
|
|
127
160
|
if (result.status === 0) return [];
|
|
128
|
-
|
|
161
|
+
|
|
162
|
+
// Validator ran and exited non-zero. Separate real validation errors from
|
|
163
|
+
// environment errors (tsc missing, npm ERR!, shell errors) so that a broken
|
|
164
|
+
// validator environment never blocks goal-fit.
|
|
165
|
+
const allLines = `${result.stdout || ''}\n${result.stderr || ''}`
|
|
129
166
|
.split('\n')
|
|
130
167
|
.map(line => line.trim())
|
|
131
|
-
.filter(Boolean)
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
168
|
+
.filter(Boolean);
|
|
169
|
+
|
|
170
|
+
const envLines = allLines.filter(isEnvironmentError);
|
|
171
|
+
const validationLines = allLines.filter(line => !isEnvironmentError(line));
|
|
172
|
+
|
|
173
|
+
if (envLines.length > 0 && validationLines.length === 0) {
|
|
174
|
+
// Pure environment failure — skip, do not block.
|
|
175
|
+
return [`${relative(root, artifactDir)} sidecar validation skipped: validator environment error (${envLines[0].slice(0, 120)})`];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Real validation errors (possibly mixed with a few env noise lines).
|
|
179
|
+
const output = validationLines.length > 0 ? validationLines : allLines;
|
|
180
|
+
const trimmed = output.slice(0, 12);
|
|
181
|
+
if (trimmed.length === 0) trimmed.push(`validator exited with status ${result.status ?? 'unknown'}`);
|
|
182
|
+
return trimmed.map(line => `${relative(root, artifactDir)} sidecar validation: ${line}`);
|
|
135
183
|
}
|
|
136
184
|
|
|
137
185
|
function isWorkflowArtifact(artifact) {
|
|
@@ -295,7 +343,7 @@ function analyze(root, now = Date.now()) {
|
|
|
295
343
|
}
|
|
296
344
|
warnings.push(...sidecarGuidance(root, path.dirname(latest.file)));
|
|
297
345
|
|
|
298
|
-
const blocking = warnings.some(w => /status:|Definition Of Done|Goal Fit|sidecar validation
|
|
346
|
+
const blocking = warnings.some(w => /status:|Definition Of Done|Goal Fit|sidecar validation:|contradicts evidence\.json|workflow state|evidence verdict|evidence check|NOT_VERIFIED gap|critique status|critique open|next action/.test(w));
|
|
299
347
|
return { warnings, blocking };
|
|
300
348
|
}
|
|
301
349
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { execFileSync } from "node:child_process";
|
|
5
|
+
import { createRequire } from "node:module";
|
|
5
6
|
|
|
6
7
|
type AnyObj = Record<string, any>;
|
|
7
8
|
|
|
@@ -24,6 +25,46 @@ function appendJsonl(file: string, payload: AnyObj): void {
|
|
|
24
25
|
function die(message: string): never { throw new Error(message); }
|
|
25
26
|
function slugify(value: string, fallback: string): string { return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || fallback; }
|
|
26
27
|
|
|
28
|
+
// Optional Hachure trust-bundle validation. No-ops gracefully when hachure is not installed.
|
|
29
|
+
// Install hachure (^0.4.0) as an optional dependency to enable schema validation.
|
|
30
|
+
function tryLoadHachureValidator(): ((bundle: unknown) => { valid: boolean; errors: string[] }) | null {
|
|
31
|
+
try {
|
|
32
|
+
const _require = createRequire(import.meta.url);
|
|
33
|
+
const hachureDir = path.dirname(_require.resolve("hachure"));
|
|
34
|
+
const schemasDir = path.join(hachureDir, "schemas");
|
|
35
|
+
const Ajv = _require("ajv/dist/2020");
|
|
36
|
+
const schemas: Record<string, any> = {};
|
|
37
|
+
for (const file of fs.readdirSync(schemasDir)) {
|
|
38
|
+
if (!file.endsWith(".schema.json")) continue;
|
|
39
|
+
schemas[file] = JSON.parse(fs.readFileSync(path.join(schemasDir, file), "utf8"));
|
|
40
|
+
}
|
|
41
|
+
const ajv = new Ajv({ strict: false, allErrors: true });
|
|
42
|
+
for (const [filename, schema] of Object.entries(schemas)) {
|
|
43
|
+
if (filename === "trust-bundle.schema.json") continue;
|
|
44
|
+
ajv.addSchema(schema, filename);
|
|
45
|
+
}
|
|
46
|
+
const trustBundleSchema = schemas["trust-bundle.schema.json"];
|
|
47
|
+
if (!trustBundleSchema) return null;
|
|
48
|
+
const validate = ajv.compile(trustBundleSchema);
|
|
49
|
+
return (bundle: unknown) => {
|
|
50
|
+
const valid = validate(bundle);
|
|
51
|
+
if (valid) return { valid: true, errors: [] };
|
|
52
|
+
const errors = ((validate as any).errors ?? []).map((err: any) => {
|
|
53
|
+
const loc = err.instancePath || err.schemaPath || "";
|
|
54
|
+
return `${loc} ${err.message ?? "invalid"}`.trim();
|
|
55
|
+
});
|
|
56
|
+
return { valid: false, errors };
|
|
57
|
+
};
|
|
58
|
+
} catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
let _hachureValidator: ReturnType<typeof tryLoadHachureValidator> | undefined;
|
|
63
|
+
function getHachureValidator(): ReturnType<typeof tryLoadHachureValidator> {
|
|
64
|
+
if (_hachureValidator === undefined) _hachureValidator = tryLoadHachureValidator();
|
|
65
|
+
return _hachureValidator;
|
|
66
|
+
}
|
|
67
|
+
|
|
27
68
|
function safeRepoIdentifier(value: string): string {
|
|
28
69
|
const trimmed = value.trim().replace(/\.git$/, "");
|
|
29
70
|
if (!trimmed || trimmed.length > 120) return "";
|
|
@@ -372,11 +413,27 @@ function normalizeCheck(raw: AnyObj): AnyObj {
|
|
|
372
413
|
}
|
|
373
414
|
function normalizeSurfaceRefs(refs: any): AnyObj[] {
|
|
374
415
|
if (!Array.isArray(refs)) die("surface_trust_refs must be an array");
|
|
416
|
+
const hachureValidate = getHachureValidator();
|
|
375
417
|
return refs.map((ref) => {
|
|
376
418
|
const keys = JSON.stringify(ref).match(/"([^"]+)":/g) ?? [];
|
|
377
419
|
for (const key of keys.map((k) => k.slice(1, -2))) if (key.toLowerCase().includes("veritas")) die(`unsupported field in Surface trust ref: ${key}`);
|
|
378
420
|
const out = { ...ref };
|
|
379
|
-
|
|
421
|
+
// trust.bundle is the canonical Hachure-aligned artifact kind; TrustReport/Trust Snapshot are legacy aliases
|
|
422
|
+
if (!["trust.bundle", "TrustReport", "Trust Snapshot"].includes(out.artifact_kind)) die("artifact_kind must be one of: trust.bundle, TrustReport, Trust Snapshot");
|
|
423
|
+
// When hachure is installed, validate the referenced trust artifact if it is a local file
|
|
424
|
+
if (hachureValidate && out.artifact_ref && typeof out.artifact_ref === "string" && fs.existsSync(out.artifact_ref)) {
|
|
425
|
+
try {
|
|
426
|
+
const bundle = JSON.parse(fs.readFileSync(out.artifact_ref, "utf8"));
|
|
427
|
+
const result = hachureValidate(bundle);
|
|
428
|
+
if (!result.valid) {
|
|
429
|
+
const errorSummary = result.errors.slice(0, 3).join("; ");
|
|
430
|
+
die(`trust.bundle artifact at ${out.artifact_ref} failed Hachure schema validation: ${errorSummary}`);
|
|
431
|
+
}
|
|
432
|
+
} catch (err) {
|
|
433
|
+
if (err instanceof Error && err.message.includes("failed Hachure schema validation")) throw err;
|
|
434
|
+
// File read or parse errors are not re-thrown: the artifact_ref validation path is advisory
|
|
435
|
+
}
|
|
436
|
+
}
|
|
380
437
|
const status = deriveSurfaceStatus(out);
|
|
381
438
|
if (out.status === "pass" && status !== "pass") die("surface_trust_refs contradicts Surface trust facts");
|
|
382
439
|
return out;
|
|
@@ -394,15 +451,16 @@ function surfaceCheckFromArtifact(file: string, index: number): AnyObj {
|
|
|
394
451
|
const lower = JSON.stringify(raw).toLowerCase();
|
|
395
452
|
let ref: AnyObj;
|
|
396
453
|
if (lower.includes("provider") && lower.includes("absent")) {
|
|
397
|
-
ref = { artifact_kind: "
|
|
454
|
+
ref = { artifact_kind: "trust.bundle", artifact_ref: file, gate_id: "provider.unavailable", claim_type: "builder.trust.bundle", claim_status: "unknown", subject: "builder-kit", freshness: { status: "unknown", summary: "No trust provider is configured" }, authority: { producer: "unknown", summary: "No trust provider is configured" }, integrity: { status: "unknown", summary: "Unknown" }, status: "not_verified", summary: "No trust provider is configured" };
|
|
398
455
|
} else if (lower.includes("artifact") && lower.includes("absent")) {
|
|
399
|
-
ref = { artifact_kind: "
|
|
456
|
+
ref = { artifact_kind: "trust.bundle", artifact_ref: file, gate_id: "artifact.unavailable", claim_type: "builder.trust.bundle", claim_status: "unknown", subject: "builder-kit", freshness: { status: "unknown", summary: "Artifact not readable" }, authority: { producer: "unknown", summary: "Artifact not readable" }, integrity: { status: "unknown", summary: "Artifact not readable" }, status: "not_verified", summary: "artifact not readable" };
|
|
400
457
|
} else {
|
|
401
458
|
const claimStatus = lower.includes("rejected") ? "rejected" : "accepted";
|
|
402
459
|
const freshness = lower.includes("stale") ? "stale" : "fresh";
|
|
403
460
|
const producer = lower.includes("missing-authority") ? "unknown" : "surface-local";
|
|
404
461
|
const integrity = lower.includes("mismatch") ? "mismatch" : "matched";
|
|
405
|
-
|
|
462
|
+
// Use trust.bundle as the canonical Hachure-aligned artifact_kind for all trust-backed evidence refs
|
|
463
|
+
ref = { artifact_kind: "trust.bundle", artifact_ref: file, gate_id: "builder.trust.bundle", claim_type: "builder.trust.bundle", claim_status: claimStatus, subject: "builder-kit", freshness: { status: freshness, summary: freshness === "fresh" ? "fresh" : "not currently verifiable" }, authority: { producer, summary: producer === "unknown" ? "missing authority" : "Local Surface trust producer." }, integrity: { status: integrity, summary: integrity === "matched" ? "matched" : "integrity mismatch" } };
|
|
406
464
|
ref.status = deriveSurfaceStatus(ref);
|
|
407
465
|
ref.summary = ref.status === "pass" ? "accepted" : ref.status === "not_verified" ? "not currently verifiable" : (claimStatus === "rejected" ? "rejected" : producer === "unknown" ? "missing authority" : "integrity mismatch");
|
|
408
466
|
}
|
package/src/flow-kit/validate.ts
CHANGED
|
@@ -24,6 +24,47 @@ export interface KitConformanceLevel {
|
|
|
24
24
|
k2: boolean;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Kit trust level — WHO vouches for a kit, orthogonal to the K-level capability axis.
|
|
29
|
+
*
|
|
30
|
+
* - "first-party": the kit is authored and published by Kontour (kontourai); its id is in the
|
|
31
|
+
* FIRST_PARTY_KIT_IDS allowlist maintained in this repository. These kits are built, tested,
|
|
32
|
+
* and distributed with the flow-agents package.
|
|
33
|
+
* - "verified": reserved for a future third-party verification process (e.g. self-certification
|
|
34
|
+
* via the conformance kit + cryptographic attestation / Veritas claims). Not yet implemented.
|
|
35
|
+
* - "unverified": default for all kits not in the first-party allowlist. This says nothing about
|
|
36
|
+
* the kit's quality — it only means Kontour has not vouched for it.
|
|
37
|
+
*
|
|
38
|
+
* The v2 path for "verified": cryptographic signing / attestation against the conformance kit
|
|
39
|
+
* and Veritas claims substrate is the natural next step and is intentionally deferred.
|
|
40
|
+
*/
|
|
41
|
+
export type KitTrustLevel = "first-party" | "verified" | "unverified";
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Allowlist of kit IDs that Kontour authors, tests, and ships with the flow-agents package.
|
|
45
|
+
*
|
|
46
|
+
* Criteria for inclusion:
|
|
47
|
+
* 1. The kit directory lives under kits/ in the kontourai/flow-agents repository.
|
|
48
|
+
* 2. The kit is published by @kontourai (npm package @kontourai/flow-agents).
|
|
49
|
+
* 3. Kontour owns and maintains the kit's content and release lifecycle.
|
|
50
|
+
*
|
|
51
|
+
* To add a new first-party kit: add its id here AND ensure it lives under kits/ in this repo.
|
|
52
|
+
* Third-party forks or community kits published elsewhere are NOT first-party, even if they
|
|
53
|
+
* share a similar id — first-party is tied to provenance in this specific repository.
|
|
54
|
+
*/
|
|
55
|
+
export const FIRST_PARTY_KIT_IDS: ReadonlySet<string> = new Set(["builder", "knowledge"]);
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Derive the trust level for a kit id.
|
|
59
|
+
*
|
|
60
|
+
* v1 determination: allowlist check against FIRST_PARTY_KIT_IDS.
|
|
61
|
+
* "verified" is reserved for future third-party verification (not yet granted to any kit).
|
|
62
|
+
*/
|
|
63
|
+
export function deriveKitTrust(kitId: string): KitTrustLevel {
|
|
64
|
+
if (FIRST_PARTY_KIT_IDS.has(kitId)) return "first-party";
|
|
65
|
+
return "unverified";
|
|
66
|
+
}
|
|
67
|
+
|
|
27
68
|
export interface KitTargetsResult {
|
|
28
69
|
kit_id: string;
|
|
29
70
|
kit_name: string;
|
|
@@ -32,6 +73,11 @@ export interface KitTargetsResult {
|
|
|
32
73
|
targets: KitTargetConsumer[];
|
|
33
74
|
/** Extension field namespaces that are not Flow or Flow Agents-owned. */
|
|
34
75
|
third_party_extensions: string[];
|
|
76
|
+
/**
|
|
77
|
+
* Trust level: who vouches for this kit. Orthogonal to the K-level capability axis.
|
|
78
|
+
* "first-party" = Kontour-published; "verified" = reserved (future); "unverified" = default.
|
|
79
|
+
*/
|
|
80
|
+
trust: KitTrustLevel;
|
|
35
81
|
}
|
|
36
82
|
|
|
37
83
|
// Lazy-loaded cache for validateKitContainer from @kontourai/flow.
|
|
@@ -83,7 +129,7 @@ async function delegateCoreContainerValidation(kitDir: string, manifest: Record<
|
|
|
83
129
|
}
|
|
84
130
|
|
|
85
131
|
/**
|
|
86
|
-
* Derives the consumer-target level (K0/K1/K2)
|
|
132
|
+
* Derives the consumer-target level (K0/K1/K2), target audience list, and trust level from
|
|
87
133
|
* observable asset classes in the kit manifest. Does not require file I/O.
|
|
88
134
|
*
|
|
89
135
|
* Derivation rules (from kontourai/flow-agents#52 and Brian's layering review):
|
|
@@ -94,6 +140,10 @@ async function delegateCoreContainerValidation(kitDir: string, manifest: Record<
|
|
|
94
140
|
* - targets.flow-agents: present when K1 (agent extension assets activate in >=1 harness).
|
|
95
141
|
* - third-party: any top-level keys that are not core fields and not Flow Agents extension classes.
|
|
96
142
|
*
|
|
143
|
+
* Trust derivation (from kontourai/flow-agents#79):
|
|
144
|
+
* - "first-party": kit id is in FIRST_PARTY_KIT_IDS (Kontour-authored kits in this repo).
|
|
145
|
+
* - "unverified": all other kits (default; "verified" is reserved for a future process).
|
|
146
|
+
*
|
|
97
147
|
* @param manifest The kit.json manifest object.
|
|
98
148
|
* @param kitDir Kit directory for flow file-existence checks. Defaults to "" (structural-only).
|
|
99
149
|
* Pass the real kit directory from `inspect` to get authoritative K0 validation.
|
|
@@ -125,12 +175,16 @@ export async function deriveKitTargets(manifest: Record<string, unknown>, kitDir
|
|
|
125
175
|
if (k1) targets.push("flow-agents");
|
|
126
176
|
for (const ns of thirdPartyExtensions) targets.push(ns);
|
|
127
177
|
|
|
178
|
+
// Derive trust level orthogonally to the K-level capability axis.
|
|
179
|
+
const trust = deriveKitTrust(kitId);
|
|
180
|
+
|
|
128
181
|
return {
|
|
129
182
|
kit_id: kitId,
|
|
130
183
|
kit_name: kitName,
|
|
131
184
|
conformance: { k0, k1, k2 },
|
|
132
185
|
targets,
|
|
133
186
|
third_party_extensions: thirdPartyExtensions,
|
|
187
|
+
trust,
|
|
134
188
|
};
|
|
135
189
|
}
|
|
136
190
|
|
|
@@ -439,6 +439,7 @@ function exportOpencodePlugin(): string {
|
|
|
439
439
|
|
|
440
440
|
import { spawnSync } from 'node:child_process';
|
|
441
441
|
import { join, basename } from 'node:path';
|
|
442
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
442
443
|
|
|
443
444
|
// opencode runs plugins inside its own compiled (Bun-based) binary, so
|
|
444
445
|
// process.execPath points at opencode itself — spawning it with a script
|
|
@@ -449,6 +450,19 @@ const NODE_BIN = basename(process.execPath).startsWith('node') ? process.execPat
|
|
|
449
450
|
export const FlowAgentsPlugin = async ({ project, client, $, directory, worktree }) => {
|
|
450
451
|
const root = directory || process.cwd();
|
|
451
452
|
|
|
453
|
+
// Deterministic load marker. opencode invokes this factory at startup but
|
|
454
|
+
// does not reliably surface plugin console output to its log file, and its
|
|
455
|
+
// internal "loading plugin" message was dropped in opencode 1.17.x. Write a
|
|
456
|
+
// marker into the workspace telemetry dir so acceptance tests can confirm the
|
|
457
|
+
// plugin loaded without depending on opencode internals. Best-effort only.
|
|
458
|
+
try {
|
|
459
|
+
const telemetryDir = join(root, '.telemetry');
|
|
460
|
+
mkdirSync(telemetryDir, { recursive: true });
|
|
461
|
+
writeFileSync(join(telemetryDir, 'opencode-plugin.loaded'), 'flow-agents');
|
|
462
|
+
} catch (_err) {
|
|
463
|
+
// Marker is diagnostic only; never block plugin load on a write failure.
|
|
464
|
+
}
|
|
465
|
+
|
|
452
466
|
// The hook scripts read the event payload from stdin; an empty stdin makes
|
|
453
467
|
// the telemetry pipeline silently skip the emit (fail-open), so every spawn
|
|
454
468
|
// must pass a payload (caught by live acceptance smoke 2026-06-11).
|