knodin 0.6.0 → 0.7.4
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/README.md +26 -3
- package/dist/bin/cli.js +168 -12
- package/dist/bin/launcher.js +11 -0
- package/dist/src/agent-integration.js +3 -1
- package/dist/src/cli-args.js +8 -1
- package/dist/src/cli-model.js +35 -1
- package/dist/src/codeflow-replay.js +80 -0
- package/dist/src/competitive-cold-mcp.js +40 -0
- package/dist/src/competitive-manifest.js +106 -25
- package/dist/src/competitive-runner.js +37 -3
- package/dist/src/competitive-sandbox.js +1 -1
- package/dist/src/diagnostics.js +449 -0
- package/dist/src/engine/git-history.js +289 -0
- package/dist/src/engine/index.js +417 -70
- package/dist/src/engine/scip-import.js +408 -0
- package/dist/src/execution-profile.js +203 -0
- package/dist/src/failure-diagnosis.js +69 -10
- package/dist/src/hook-manager-integration.js +156 -0
- package/dist/src/init.js +319 -33
- package/dist/src/lifecycle-health.js +42 -4
- package/dist/src/output-telemetry.js +4 -0
- package/dist/src/progressive-evidence.js +473 -0
- package/dist/src/pure-compression-cli.js +101 -0
- package/dist/src/release-preflight.js +510 -0
- package/dist/src/repository-management.js +142 -0
- package/dist/src/response-budget.js +11 -1
- package/dist/src/server.js +22 -2
- package/dist/src/structural-fast-path.js +338 -0
- package/dist/src/structural-snapshot.js +33 -0
- package/dist/src/tools/knodin-tools.js +105 -14
- package/dist/src/update-ceremony.js +158 -0
- package/docs/CLI.md +22 -0
- package/docs/COMMAND-OUTPUT-COMPRESSION.md +31 -15
- package/docs/CONTAINED-EXECUTION.md +77 -0
- package/docs/DIAGNOSTICS.md +45 -0
- package/docs/DOCTOR-AND-UPDATES.md +5 -2
- package/docs/GIT-HISTORY-REVIEW.md +39 -0
- package/docs/MCP.md +15 -0
- package/docs/PROGRESSIVE-EVIDENCE.md +37 -0
- package/docs/REPOSITORIES-AND-WORKTREES.md +30 -0
- package/docs/SCIP-IMPORT.md +57 -0
- package/docs/SIGNED-UPDATES.md +5 -0
- package/docs/TELEMETRY.md +4 -0
- package/docs/releases/0.7.0.md +24 -0
- package/docs/releases/0.7.1.md +21 -0
- package/docs/releases/0.7.2.md +21 -0
- package/docs/releases/0.7.3.md +23 -0
- package/docs/releases/0.7.4.md +17 -0
- package/package.json +34 -2
|
@@ -16,13 +16,15 @@ import { getDocSection, listDocTopics } from "../docs-sections.js";
|
|
|
16
16
|
import { diagnoseInstallation } from "../doctor.js";
|
|
17
17
|
import { createEngine, REPO_WIDE_QUERY_PATTERNS, } from "../engine/index.js";
|
|
18
18
|
import { measurePerfPhaseSync } from "../engine/perf.js";
|
|
19
|
+
import { runExecutionProfile } from "../execution-profile.js";
|
|
19
20
|
import { diagnoseFailure, } from "../failure-diagnosis.js";
|
|
20
21
|
import { decorateGraphQueryResult, inspectGraphQueryHealth } from "../graph-query-health.js";
|
|
21
22
|
import { inspectRepositoryIntegrationStatus } from "../init.js";
|
|
22
|
-
import { attachLifecycleHealth } from "../lifecycle-health.js";
|
|
23
|
+
import { attachLifecycleHealth, attachRepairLifecycle } from "../lifecycle-health.js";
|
|
23
24
|
import { compressOutput, compressOutputFile, deleteOutputArtifact, readOutputArtifact, } from "../output-compression.js";
|
|
24
25
|
import { appendTelemetryRecord, clearTelemetry, countOutputTokens, exportTelemetry, measureOutput, readTelemetryRecords, telemetryStatus, writeTelemetryReport, } from "../output-telemetry.js";
|
|
25
26
|
import { auditPullRequests, ghUnavailableReason, triagePrDetail } from "../pr-triage.js";
|
|
27
|
+
import { deliverProgressiveEvidence, } from "../progressive-evidence.js";
|
|
26
28
|
import { createRepairPlan } from "../repair-progress.js";
|
|
27
29
|
import { runRepositoryInitializationProcess } from "../repository-init-process.js";
|
|
28
30
|
import { discoverRepositories, initializeRepositories, inventoryRepository, searchRepositories, } from "../repository-management.js";
|
|
@@ -31,7 +33,18 @@ import { enrichSystemRelationships, incorporateSystemQueryEvidence, indexModeFor
|
|
|
31
33
|
import { trustedUpdateStatus } from "../update-policy.js";
|
|
32
34
|
import { waitForFresh } from "../wait-for-fresh.js";
|
|
33
35
|
import { inspectWorktrees, reconcileWorktrees, removeManagedWorktree, } from "../worktree-lifecycle.js";
|
|
34
|
-
|
|
36
|
+
let initializedEngine = null;
|
|
37
|
+
function gatewayEngine() {
|
|
38
|
+
initializedEngine ??= createEngine();
|
|
39
|
+
return initializedEngine;
|
|
40
|
+
}
|
|
41
|
+
/** Lazy engine facade: schema/docs/compression startup does not open graph state. */
|
|
42
|
+
const engine = new Proxy({}, {
|
|
43
|
+
get(_target, property) {
|
|
44
|
+
const value = gatewayEngine()[property];
|
|
45
|
+
return typeof value === "function" ? value.bind(gatewayEngine()) : value;
|
|
46
|
+
},
|
|
47
|
+
});
|
|
35
48
|
function gatewayCliCommand() {
|
|
36
49
|
const extension = import.meta.url.endsWith(".ts") ? "ts" : "js";
|
|
37
50
|
const entry = nodePath.resolve(nodePath.dirname(fileURLToPath(import.meta.url)), `../../bin/cli.${extension}`);
|
|
@@ -44,7 +57,9 @@ function gatewayCliCommand() {
|
|
|
44
57
|
}
|
|
45
58
|
/** Release the gateway engine's local watcher and database resources. */
|
|
46
59
|
export async function closeKnodinToolEngine() {
|
|
47
|
-
|
|
60
|
+
if (initializedEngine)
|
|
61
|
+
await initializedEngine.close();
|
|
62
|
+
initializedEngine = null;
|
|
48
63
|
compactReadyRepos.clear();
|
|
49
64
|
}
|
|
50
65
|
const localTelemetry = [];
|
|
@@ -154,7 +169,9 @@ function buildDocumentedKnodinTools() {
|
|
|
154
169
|
"docs",
|
|
155
170
|
"doctor",
|
|
156
171
|
"pack",
|
|
172
|
+
"evidence",
|
|
157
173
|
"compress",
|
|
174
|
+
"execute",
|
|
158
175
|
"status",
|
|
159
176
|
"wait",
|
|
160
177
|
"worktrees",
|
|
@@ -163,7 +180,12 @@ function buildDocumentedKnodinTools() {
|
|
|
163
180
|
"system",
|
|
164
181
|
"telemetry",
|
|
165
182
|
],
|
|
166
|
-
description: "Which knodin capability to run. context: call this FIRST when starting any investigation and unsure which operation to reach for — one ultra-compact orientation (repo stats + top subsystems/hubs/flows + a risk score if there's a diff + a heuristic next-operation suggestion); the suggestion is only a hint and never blocks calling any operation directly. explain: use when orienting on a symbol/file or before editing it — returns edit-ready source + call paths + blast radius. review: use before writing a PR description or approving a diff — risk-scored context (changed symbols, affected flows, test gaps). map: use before a cross-cutting refactor or to understand subsystem boundaries — communities + hub/bridge nodes + confidence-tagged edges. search: use when you don't know the exact symbol name — hybrid semantic + keyword lookup over code symbols. query: use for a structured question about a known symbol — callers_of, tests_for, shortest_path, dead_code, rename_preview, flows, and more (see `pattern`). pack: create deterministic Markdown/JSON/XML source context under hard budgets, or bounded-read/exact-regex-grep a saved artifact. compress: reduce already-produced diagnostic text under exact line/content-byte budgets, preserve exit metadata and detected signals, retain private local drill-down data by default, and never label insufficient-fidelity output complete. prs: use to triage open GitHub PRs (via your local authenticated `gh`) — per-PR status, CI, and blast radius sorted ready-small-impact first; pass `prNumber` for one PR's impacted files + community names. wiki: write a static markdown documentation site for the repository's logical subsystems to `.knodin/wiki/`. Generates an index plus one page per mapped community. Reuses the `map` output. Idempotent: unchanged pages are untouched on disk unless `force` is true. docs: call this to retrieve curated, focused markdown usage guidance directly over MCP.",
|
|
183
|
+
description: "Which knodin capability to run. context: call this FIRST when starting any investigation and unsure which operation to reach for — one ultra-compact orientation (repo stats + top subsystems/hubs/flows + a risk score if there's a diff + a heuristic next-operation suggestion); the suggestion is only a hint and never blocks calling any operation directly. explain: use when orienting on a symbol/file or before editing it — returns edit-ready source + call paths + blast radius. review: use before writing a PR description or approving a diff — risk-scored context (changed symbols, affected flows, test gaps). map: use before a cross-cutting refactor or to understand subsystem boundaries — communities + hub/bridge nodes + confidence-tagged edges. search: use when you don't know the exact symbol name — hybrid semantic + keyword lookup over code symbols. query: use for a structured question about a known symbol — callers_of, tests_for, shortest_path, dead_code, rename_preview, flows, and more (see `pattern`). pack: create deterministic Markdown/JSON/XML source context under hard budgets, or bounded-read/exact-regex-grep a saved artifact. compress: reduce already-produced diagnostic text under exact line/content-byte budgets, preserve exit metadata and detected signals, retain private local drill-down data by default, and never label insufficient-fidelity output complete. execute: run one immutable repository-defined profile only when independently enabled globally and locally; no executable or argv is accepted from the caller, unsupported containment fails closed, and output is compressed then diagnosed. prs: use to triage open GitHub PRs (via your local authenticated `gh`) — per-PR status, CI, and blast radius sorted ready-small-impact first; pass `prNumber` for one PR's impacted files + community names. wiki: write a static markdown documentation site for the repository's logical subsystems to `.knodin/wiki/`. Generates an index plus one page per mapped community. Reuses the `map` output. Idempotent: unchanged pages are untouched on disk unless `force` is true. docs: call this to retrieve curated, focused markdown usage guidance directly over MCP.",
|
|
184
|
+
},
|
|
185
|
+
profile: {
|
|
186
|
+
type: "string",
|
|
187
|
+
pattern: "^[a-z][a-z0-9_-]{0,63}$",
|
|
188
|
+
description: "execute only: immutable repository-defined profile identity. The caller cannot supply executable, argv, cwd, environment, or limits.",
|
|
167
189
|
},
|
|
168
190
|
symbol: {
|
|
169
191
|
type: "string",
|
|
@@ -177,6 +199,24 @@ function buildDocumentedKnodinTools() {
|
|
|
177
199
|
type: "string",
|
|
178
200
|
description: "Repo-relative definition file selector.",
|
|
179
201
|
},
|
|
202
|
+
evidenceLevel: {
|
|
203
|
+
type: "string",
|
|
204
|
+
enum: ["locate", "outline", "evidence", "expand"],
|
|
205
|
+
description: "evidence only: deterministic progressive-delivery level.",
|
|
206
|
+
},
|
|
207
|
+
continuation: {
|
|
208
|
+
type: "string",
|
|
209
|
+
description: "evidence only: integrity-checked continuation returned by the prior level/page.",
|
|
210
|
+
},
|
|
211
|
+
baselineHash: {
|
|
212
|
+
type: "string",
|
|
213
|
+
description: "evidence only: SHA-256 of the complete baseline; omission requires an exact current match.",
|
|
214
|
+
},
|
|
215
|
+
baselineBytes: {
|
|
216
|
+
type: "number",
|
|
217
|
+
minimum: 0,
|
|
218
|
+
description: "evidence only: complete baseline UTF-8 byte length; required with baselineHash.",
|
|
219
|
+
},
|
|
180
220
|
kind: {
|
|
181
221
|
type: "string",
|
|
182
222
|
description: "Definition kind selector (function, class, method, etc.).",
|
|
@@ -622,16 +662,16 @@ function buildDocumentedKnodinTools() {
|
|
|
622
662
|
export function getDocumentedKnodinToolsForSchemaProof() {
|
|
623
663
|
return buildDocumentedKnodinTools();
|
|
624
664
|
}
|
|
625
|
-
const COMPACT_TOOL_DESCRIPTION = "Local code intelligence
|
|
665
|
+
const COMPACT_TOOL_DESCRIPTION = "Local code intelligence.";
|
|
626
666
|
const COMPACT_PARAMETER_DESCRIPTIONS = {
|
|
627
|
-
operation: "Capability
|
|
628
|
-
symbol: "Target
|
|
629
|
-
pattern: "Query pattern
|
|
630
|
-
impactMode: "
|
|
631
|
-
apply: "Apply
|
|
632
|
-
diffScope: "Review scope
|
|
633
|
-
section: "
|
|
634
|
-
persistTelemetry: "Persist metadata
|
|
667
|
+
operation: "Capability.",
|
|
668
|
+
symbol: "Target.",
|
|
669
|
+
pattern: "Query pattern.",
|
|
670
|
+
impactMode: "Symbol or file impact.",
|
|
671
|
+
apply: "Apply verified edits.",
|
|
672
|
+
diffScope: "Review diff scope.",
|
|
673
|
+
section: "Documentation section.",
|
|
674
|
+
persistTelemetry: "Persist metadata telemetry.",
|
|
635
675
|
};
|
|
636
676
|
function compactParameterDescription(name, description) {
|
|
637
677
|
void description;
|
|
@@ -881,6 +921,23 @@ function compactExplainSource(result) {
|
|
|
881
921
|
staleness: result.staleness,
|
|
882
922
|
};
|
|
883
923
|
}
|
|
924
|
+
function handleProgressiveEvidenceOperation(repo, args, observe) {
|
|
925
|
+
if (!args.file || !args.evidenceLevel)
|
|
926
|
+
throw new Error("knodin evidence requires `file` and `evidenceLevel`");
|
|
927
|
+
return observe(deliverProgressiveEvidence({
|
|
928
|
+
repo,
|
|
929
|
+
file: args.file,
|
|
930
|
+
level: args.evidenceLevel,
|
|
931
|
+
continuation: args.continuation,
|
|
932
|
+
baselineHash: args.baselineHash,
|
|
933
|
+
baselineBytes: args.baselineBytes,
|
|
934
|
+
startLine: args.startLine,
|
|
935
|
+
endLine: args.endLine,
|
|
936
|
+
byteLimit: args.byteBudget,
|
|
937
|
+
tokenLimit: args.tokenBudget,
|
|
938
|
+
itemLimit: args.itemBudget,
|
|
939
|
+
}), "evidence");
|
|
940
|
+
}
|
|
884
941
|
function compactCompression(result) {
|
|
885
942
|
return {
|
|
886
943
|
status: result.status,
|
|
@@ -958,6 +1015,9 @@ function compactFailureDiagnosis(result) {
|
|
|
958
1015
|
unresolved: result.unresolved,
|
|
959
1016
|
context: result.contextBundle,
|
|
960
1017
|
freshness: [result.freshness.state, result.freshness.indexedHead, result.freshness.currentHead],
|
|
1018
|
+
confidence: result.confidence,
|
|
1019
|
+
omissions: result.omissions,
|
|
1020
|
+
telemetry: result.telemetry,
|
|
961
1021
|
limitations: result.limitations,
|
|
962
1022
|
};
|
|
963
1023
|
}
|
|
@@ -973,6 +1033,7 @@ async function handleCompressionDiagnosis(repo, args, engine, graphRead) {
|
|
|
973
1033
|
artifactId: args.artifactId,
|
|
974
1034
|
text: args.text,
|
|
975
1035
|
maxDiagnostics: args.limit ?? args.itemBudget,
|
|
1036
|
+
diagnosticOffset: args.startLine,
|
|
976
1037
|
contextLines: args.contextLines,
|
|
977
1038
|
contextByteBudget: args.compressionByteBudget,
|
|
978
1039
|
});
|
|
@@ -1042,6 +1103,29 @@ async function handleCompressionOperation(repo, args, engine, observe, bounded,
|
|
|
1042
1103
|
throw new Error("knodin compress: invalid compressionAction");
|
|
1043
1104
|
}
|
|
1044
1105
|
}
|
|
1106
|
+
async function handleExecutionOperation(repo, profile, engine, bounded) {
|
|
1107
|
+
if (!profile)
|
|
1108
|
+
throw new Error("knodin execute requires `profile`");
|
|
1109
|
+
const execution = await runExecutionProfile({ repo, profile });
|
|
1110
|
+
let diagnosis = null;
|
|
1111
|
+
if (execution.output?.artifact.retained && execution.status !== "completed") {
|
|
1112
|
+
try {
|
|
1113
|
+
diagnosis = await diagnoseFailure(engine, repo, {
|
|
1114
|
+
artifactId: execution.output.artifact.id,
|
|
1115
|
+
maxDiagnostics: 10,
|
|
1116
|
+
contextLines: 2,
|
|
1117
|
+
contextByteBudget: 16_384,
|
|
1118
|
+
});
|
|
1119
|
+
}
|
|
1120
|
+
catch (error) {
|
|
1121
|
+
diagnosis = {
|
|
1122
|
+
status: "unavailable",
|
|
1123
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
1124
|
+
};
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
return bounded({ execution, diagnosis }, "execute", true);
|
|
1128
|
+
}
|
|
1045
1129
|
async function runCompactExplain(symbol, repo, selector, file, byteBudget) {
|
|
1046
1130
|
const result = await engine.explain(symbol, repo, "minimal", selector);
|
|
1047
1131
|
let sourceFile = file;
|
|
@@ -1159,6 +1243,9 @@ export async function handleKnodinTool(args) {
|
|
|
1159
1243
|
root.telemetry?.truncated === true,
|
|
1160
1244
|
schemaTokens: gatewaySchemaTokens(),
|
|
1161
1245
|
});
|
|
1246
|
+
if (op === "execute" && parsedArgs.profile) {
|
|
1247
|
+
telemetry.executionProfile = parsedArgs.profile;
|
|
1248
|
+
}
|
|
1162
1249
|
telemetry.temperature = localTelemetry.some(({ operation, repositoryId }) => operation === op && repositoryId === telemetry.repositoryId)
|
|
1163
1250
|
? "warm"
|
|
1164
1251
|
: "cold";
|
|
@@ -1237,7 +1324,7 @@ export async function handleKnodinTool(args) {
|
|
|
1237
1324
|
case "repair":
|
|
1238
1325
|
if (repairPlan)
|
|
1239
1326
|
return bounded(createRepairPlan(await engine.status(repo, { audit: "deep" })), "repair:plan");
|
|
1240
|
-
return engine.repair(repo)
|
|
1327
|
+
return bounded(attachRepairLifecycle(repo, await engine.repair(repo)), "repair");
|
|
1241
1328
|
case "repositories": {
|
|
1242
1329
|
if (!repositoryAction)
|
|
1243
1330
|
throw new Error("knodin repositories requires `repositoryAction`");
|
|
@@ -1273,6 +1360,10 @@ export async function handleKnodinTool(args) {
|
|
|
1273
1360
|
}
|
|
1274
1361
|
case "compress":
|
|
1275
1362
|
return handleCompressionOperation(repo, parsedArgs, engine, observe, bounded, graphRead);
|
|
1363
|
+
case "execute":
|
|
1364
|
+
return handleExecutionOperation(repo, parsedArgs.profile, engine, bounded);
|
|
1365
|
+
case "evidence":
|
|
1366
|
+
return handleProgressiveEvidenceOperation(repo, parsedArgs, observe);
|
|
1276
1367
|
case "pack": {
|
|
1277
1368
|
if (packAction !== undefined && !["export", "read", "grep"].includes(packAction))
|
|
1278
1369
|
throw new Error("knodin pack: invalid packAction");
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { createHash, createPublicKey } from "node:crypto";
|
|
2
|
+
import { canonicalizeUpdateMetadata, verifyUpdateRootChain, } from "./update-trust.js";
|
|
3
|
+
const SHA256 = /^[0-9a-f]{64}$/;
|
|
4
|
+
const ID = /^[A-Za-z0-9][A-Za-z0-9._-]{2,63}$/;
|
|
5
|
+
function exactKeys(value, expected, label) {
|
|
6
|
+
const actual = Object.keys(value).sort();
|
|
7
|
+
const wanted = [...expected].sort();
|
|
8
|
+
if (actual.join("\0") !== wanted.join("\0"))
|
|
9
|
+
throw new Error(`${label} contains missing or unknown fields`);
|
|
10
|
+
}
|
|
11
|
+
function rejectPrivateMaterial(value, path = "input") {
|
|
12
|
+
if (typeof value === "string" && /PRIVATE KEY|recovery secret|seed phrase/i.test(value))
|
|
13
|
+
throw new Error(`${path} contains private material`);
|
|
14
|
+
if (!value || typeof value !== "object")
|
|
15
|
+
return;
|
|
16
|
+
for (const [key, child] of Object.entries(value)) {
|
|
17
|
+
if (/private|secret|seed|mnemonic|password|token/i.test(key))
|
|
18
|
+
throw new Error(`${path}.${key} is forbidden`);
|
|
19
|
+
rejectPrivateMaterial(child, `${path}.${key}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function validateRootCeremonyManifest(value) {
|
|
23
|
+
rejectPrivateMaterial(value);
|
|
24
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
25
|
+
throw new Error("manifest must be an object");
|
|
26
|
+
const manifest = value;
|
|
27
|
+
exactKeys(manifest, [
|
|
28
|
+
"schemaVersion",
|
|
29
|
+
"ceremonyId",
|
|
30
|
+
"purpose",
|
|
31
|
+
"productionActivation",
|
|
32
|
+
"custodians",
|
|
33
|
+
"roleKeyIds",
|
|
34
|
+
"thresholds",
|
|
35
|
+
], "manifest");
|
|
36
|
+
if (manifest.schemaVersion !== 1 ||
|
|
37
|
+
!ID.test(manifest.ceremonyId) ||
|
|
38
|
+
!["rehearsal", "production"].includes(manifest.purpose) ||
|
|
39
|
+
manifest.productionActivation !== false)
|
|
40
|
+
throw new Error("invalid fail-closed ceremony header");
|
|
41
|
+
if (!Array.isArray(manifest.custodians) ||
|
|
42
|
+
manifest.custodians.length < 3 ||
|
|
43
|
+
new Set(manifest.custodians.map((c) => c.id)).size !== manifest.custodians.length ||
|
|
44
|
+
manifest.custodians.some((c) => {
|
|
45
|
+
exactKeys(c, c.role === "root-signer"
|
|
46
|
+
? ["id", "role", "publicKeyId"]
|
|
47
|
+
: ["id", "role", "recoveryKeyIds", "evidenceId"], "custodian");
|
|
48
|
+
return (!ID.test(c.id) ||
|
|
49
|
+
!["root-signer", "recovery"].includes(c.role) ||
|
|
50
|
+
(c.role === "root-signer" && !SHA256.test(c.publicKeyId ?? "")) ||
|
|
51
|
+
(c.role === "recovery" &&
|
|
52
|
+
(!ID.test(c.evidenceId ?? "") ||
|
|
53
|
+
!Array.isArray(c.recoveryKeyIds) ||
|
|
54
|
+
c.recoveryKeyIds.some((id) => !SHA256.test(id)))));
|
|
55
|
+
}))
|
|
56
|
+
throw new Error("at least three distinct named custodians are required");
|
|
57
|
+
const roles = ["root", "targets", "snapshot", "timestamp"];
|
|
58
|
+
exactKeys(manifest.roleKeyIds, roles, "roleKeyIds");
|
|
59
|
+
exactKeys(manifest.thresholds, roles, "thresholds");
|
|
60
|
+
const all = new Set();
|
|
61
|
+
for (const role of roles) {
|
|
62
|
+
const ids = manifest.roleKeyIds?.[role];
|
|
63
|
+
const threshold = manifest.thresholds?.[role];
|
|
64
|
+
if (!Array.isArray(ids) ||
|
|
65
|
+
ids.length === 0 ||
|
|
66
|
+
ids.some((id) => !SHA256.test(id) || all.has(id)))
|
|
67
|
+
throw new Error("role keys must be valid and disjoint");
|
|
68
|
+
for (const id of ids)
|
|
69
|
+
all.add(id);
|
|
70
|
+
if (!Number.isInteger(threshold) ||
|
|
71
|
+
threshold < (role === "root" || role === "targets" ? 2 : 1) ||
|
|
72
|
+
threshold > ids.length)
|
|
73
|
+
throw new Error(`invalid ${role} threshold`);
|
|
74
|
+
}
|
|
75
|
+
if (manifest.custodians.filter((c) => c.role === "root-signer").length < manifest.thresholds.root ||
|
|
76
|
+
!manifest.custodians.some((c) => c.role === "recovery"))
|
|
77
|
+
throw new Error("custodian roles do not satisfy threshold and recovery separation");
|
|
78
|
+
const assigned = manifest.custodians
|
|
79
|
+
.filter((c) => c.role === "root-signer")
|
|
80
|
+
.map((c) => c.publicKeyId)
|
|
81
|
+
.sort();
|
|
82
|
+
if (assigned.join("\0") !== [...manifest.roleKeyIds.root].sort().join("\0"))
|
|
83
|
+
throw new Error("root signer assignments must exactly match root role keys");
|
|
84
|
+
const recovery = manifest.custodians.filter((c) => c.role === "recovery");
|
|
85
|
+
if (recovery.length < manifest.thresholds.root ||
|
|
86
|
+
new Set(recovery.map((c) => c.evidenceId)).size !== recovery.length ||
|
|
87
|
+
recovery.some((c) => !c.recoveryKeyIds?.length ||
|
|
88
|
+
new Set(c.recoveryKeyIds).size !== c.recoveryKeyIds.length ||
|
|
89
|
+
c.recoveryKeyIds.length >= manifest.thresholds.root))
|
|
90
|
+
throw new Error("recovery sets must preserve root threshold independence");
|
|
91
|
+
const recovered = new Set(recovery.flatMap((c) => c.recoveryKeyIds ?? []));
|
|
92
|
+
if (manifest.roleKeyIds.root.some((id) => !recovered.has(id)))
|
|
93
|
+
throw new Error("recovery assignments must cover every root role key");
|
|
94
|
+
return manifest;
|
|
95
|
+
}
|
|
96
|
+
export function validateRootPinReviewReceipt(value, manifest) {
|
|
97
|
+
rejectPrivateMaterial(value);
|
|
98
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
99
|
+
throw new Error("receipt must be an object");
|
|
100
|
+
const receipt = value;
|
|
101
|
+
exactKeys(receipt, [
|
|
102
|
+
"schemaVersion",
|
|
103
|
+
"ceremonyId",
|
|
104
|
+
"rootEnvelopeSha256",
|
|
105
|
+
"reviewerId",
|
|
106
|
+
"reviewedAt",
|
|
107
|
+
"decision",
|
|
108
|
+
"authentication",
|
|
109
|
+
"productionActivation",
|
|
110
|
+
], "receipt");
|
|
111
|
+
if (receipt.schemaVersion !== 1 ||
|
|
112
|
+
receipt.ceremonyId !== manifest.ceremonyId ||
|
|
113
|
+
!SHA256.test(receipt.rootEnvelopeSha256) ||
|
|
114
|
+
!ID.test(receipt.reviewerId) ||
|
|
115
|
+
manifest.custodians.some((c) => c.id === receipt.reviewerId) ||
|
|
116
|
+
receipt.decision !== "approved" ||
|
|
117
|
+
receipt.authentication !== "rehearsal-unsigned" ||
|
|
118
|
+
receipt.productionActivation !== false ||
|
|
119
|
+
!Number.isFinite(Date.parse(receipt.reviewedAt)) ||
|
|
120
|
+
new Date(receipt.reviewedAt).toISOString() !== receipt.reviewedAt)
|
|
121
|
+
throw new Error("invalid or non-independent pin-review receipt");
|
|
122
|
+
return receipt;
|
|
123
|
+
}
|
|
124
|
+
export function preparePublicRootImport(envelope, pin, manifestValue, receiptValue, now = new Date()) {
|
|
125
|
+
rejectPrivateMaterial(envelope, "rootEnvelope");
|
|
126
|
+
const manifest = validateRootCeremonyManifest(manifestValue);
|
|
127
|
+
if (manifest.purpose === "production")
|
|
128
|
+
throw new Error("production import requires externally authenticated review and remains blocked");
|
|
129
|
+
const receipt = validateRootPinReviewReceipt(receiptValue, manifest);
|
|
130
|
+
if (Date.parse(receipt.reviewedAt) > now.getTime())
|
|
131
|
+
throw new Error("pin-review receipt cannot be future-dated");
|
|
132
|
+
const roles = ["root", "targets", "snapshot", "timestamp"];
|
|
133
|
+
for (const role of roles) {
|
|
134
|
+
const expected = manifest.roleKeyIds[role];
|
|
135
|
+
const actual = envelope.signed.roles[role];
|
|
136
|
+
if (actual.threshold !== manifest.thresholds[role] ||
|
|
137
|
+
[...actual.keyids].sort().join("\0") !== [...expected].sort().join("\0"))
|
|
138
|
+
throw new Error(`manifest ${role} role does not match root envelope`);
|
|
139
|
+
}
|
|
140
|
+
for (const key of Object.values(envelope.signed.keys)) {
|
|
141
|
+
let canonical = "";
|
|
142
|
+
try {
|
|
143
|
+
canonical = createPublicKey(key.keyval.public)
|
|
144
|
+
.export({ type: "spki", format: "pem" })
|
|
145
|
+
.toString();
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
/* fail below */
|
|
149
|
+
}
|
|
150
|
+
if (canonical !== key.keyval.public)
|
|
151
|
+
throw new Error("root envelope keys must use canonical public SPKI PEM only");
|
|
152
|
+
}
|
|
153
|
+
const digest = createHash("sha256").update(canonicalizeUpdateMetadata(envelope)).digest("hex");
|
|
154
|
+
if (pin !== digest || receipt.rootEnvelopeSha256 !== digest)
|
|
155
|
+
throw new Error("root envelope, pin, and independent review receipt disagree");
|
|
156
|
+
verifyUpdateRootChain(envelope, pin, [], now);
|
|
157
|
+
return { rootEnvelope: envelope, rootEnvelopeSha256: digest, productionActivation: false };
|
|
158
|
+
}
|
package/docs/CLI.md
CHANGED
|
@@ -32,6 +32,28 @@ example repository existence, mutually exclusive configuration modes, and
|
|
|
32
32
|
graph-query target rules), but syntax cannot reach a handler unless the shared
|
|
33
33
|
declarative model accepts it first.
|
|
34
34
|
|
|
35
|
+
For support evidence, `knodin diagnostics enable|status|collect|inspect|clear|disable`
|
|
36
|
+
manages an explicit local failure journal and redacted gzip JSON bundles.
|
|
37
|
+
Collection does not upload data. See `docs/DIAGNOSTICS.md` for retention,
|
|
38
|
+
redaction, inspection, and known bounds.
|
|
39
|
+
|
|
40
|
+
`knodin index --scip <file>` explicitly imports a bounded local SCIP protobuf
|
|
41
|
+
snapshot. The flag is never implied by `init` or ordinary indexing, and no
|
|
42
|
+
producer, daemon, account, credential, or network service is started. See
|
|
43
|
+
`docs/SCIP-IMPORT.md` for the fact, freshness, path, resource, conflict, and
|
|
44
|
+
known producer/language bounds.
|
|
45
|
+
|
|
46
|
+
`knodin evidence <locate|outline|evidence|expand> <file>` progressively returns
|
|
47
|
+
exact local source and stable continuations. A returned evidence handle plus
|
|
48
|
+
`--baseline-hash` and `--baseline-bytes` permits omission only on an exact
|
|
49
|
+
current-file match; all other baseline states return the required source. See
|
|
50
|
+
`docs/PROGRESSIVE-EVIDENCE.md` for the protocol and limitations.
|
|
51
|
+
|
|
52
|
+
`knodin review` returns separately itemized graph impact, test gaps, structural
|
|
53
|
+
centrality, churn, co-change, and coupling evidence. Git history has explicit
|
|
54
|
+
commit/file/time bounds and truthful unavailable or truncation states; see
|
|
55
|
+
`docs/GIT-HISTORY-REVIEW.md`.
|
|
56
|
+
|
|
35
57
|
`knodin pack grep` evaluates user patterns with exact-pinned RE2 WebAssembly,
|
|
36
58
|
not Node's backtracking regular-expression engine. Backreferences and lookaround
|
|
37
59
|
are rejected because they cannot retain RE2's linear-time guarantee. Route
|
|
@@ -4,8 +4,9 @@ knodin can deterministically compress already-produced build and test output
|
|
|
4
4
|
without running the command that created it. This capability is available from
|
|
5
5
|
the CLI and the single MCP gateway in post-0.3 development.
|
|
6
6
|
|
|
7
|
-
knodin deliberately does **not** expose `knodin run
|
|
8
|
-
|
|
7
|
+
knodin deliberately does **not** expose `knodin run` or accept command strings.
|
|
8
|
+
The separate `execute` MCP operation is limited to doubly enabled immutable
|
|
9
|
+
profiles and the certified platform boundary documented below.
|
|
9
10
|
|
|
10
11
|
## Use it
|
|
11
12
|
|
|
@@ -90,6 +91,7 @@ command:
|
|
|
90
91
|
```bash
|
|
91
92
|
knodin compress diagnose <artifact-id> --limit 10 --context 2
|
|
92
93
|
knodin compress diagnose <artifact-id> --max-output-bytes 16384 --json
|
|
94
|
+
knodin compress diagnose <artifact-id> --offset 10 --limit 10 --json
|
|
93
95
|
```
|
|
94
96
|
|
|
95
97
|
The same graph read is available through the single MCP gateway:
|
|
@@ -100,6 +102,7 @@ The same graph read is available through the single MCP gateway:
|
|
|
100
102
|
"compressionAction": "diagnose",
|
|
101
103
|
"artifactId": "<sha256>",
|
|
102
104
|
"limit": 10,
|
|
105
|
+
"startLine": 10,
|
|
103
106
|
"contextLines": 2,
|
|
104
107
|
"compressionByteBudget": 16384,
|
|
105
108
|
"detailLevel": "standard"
|
|
@@ -143,8 +146,18 @@ current graph evidence, and an untruncated context bundle. `partial` identifies
|
|
|
143
146
|
any omitted input, unresolved reference, stale graph, or exhausted context
|
|
144
147
|
budget. `unresolved` means no safe repository location was established.
|
|
145
148
|
|
|
146
|
-
The
|
|
147
|
-
|
|
149
|
+
The versioned JSON schema also reports `confidence`, exact `omissions`, and
|
|
150
|
+
`telemetry`. When the diagnostic item cap omits references from a retained
|
|
151
|
+
artifact, `omissions.continuation` identifies the artifact and next diagnostic
|
|
152
|
+
offset for recoverable local drill-down. `telemetry.commandRerun` is always
|
|
153
|
+
false. Outer MCP response budgets add hard byte/token/item accounting without
|
|
154
|
+
changing the diagnosis evidence contract.
|
|
155
|
+
|
|
156
|
+
For the compact single-tool schema, MCP diagnose continuation uses the existing
|
|
157
|
+
bounded `startLine` integer as its diagnostic offset; CLI uses `--offset`.
|
|
158
|
+
|
|
159
|
+
The relationships are bounded source-evidenced diagnostic candidates, not
|
|
160
|
+
proof of runtime causality, guaranteed root causes, or automatic fixes. Package ownership is evidenced by the nearest tracked
|
|
148
161
|
`package.json`, `pyproject.toml`, `pom.xml`, `Cargo.toml`, `go.mod`, or
|
|
149
162
|
`.csproj`; malformed or unnamed manifests remain explicit. Dynamic dispatch,
|
|
150
163
|
generated paths, source maps, and framework wiring can require additional
|
|
@@ -163,11 +176,15 @@ Oversized input is rejected rather than partially retained. Output artifacts
|
|
|
163
176
|
are content-addressed, reads are byte-bounded, deletion requires an exact
|
|
164
177
|
artifact identity, and symlinked storage paths are refused.
|
|
165
178
|
|
|
166
|
-
##
|
|
179
|
+
## Profile-based execution boundary
|
|
180
|
+
|
|
181
|
+
The compressor remains independently usable on already-produced output. The
|
|
182
|
+
optional `execute` operation now accepts only an immutable repository profile
|
|
183
|
+
identity and composes captured output through this compressor. The detailed
|
|
184
|
+
configuration and certified boundary are documented in
|
|
185
|
+
[Profile-based contained execution](CONTAINED-EXECUTION.md).
|
|
167
186
|
|
|
168
|
-
The
|
|
169
|
-
process. A future runner must address this threat model with executable,
|
|
170
|
-
platform-specific evidence:
|
|
187
|
+
The shipping runner is still held to this threat model:
|
|
171
188
|
|
|
172
189
|
| Threat | Required proof before shipping a runner |
|
|
173
190
|
|---|---|
|
|
@@ -185,10 +202,9 @@ platform-specific evidence:
|
|
|
185
202
|
| Telemetry leakage | No command text or raw output in default telemetry; redact before opt-in export |
|
|
186
203
|
| Platform differences | Separate verified guarantees and explicit unsupported cases |
|
|
187
204
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
artifact to knodin for bounded compression.
|
|
205
|
+
The original C58 prototype remains rejected: cwd/realpath checks and polling
|
|
206
|
+
were not containment. C88 reopens the capability under a narrower contract.
|
|
207
|
+
The certified macOS adapter adds enforceable network/user-data boundaries and
|
|
208
|
+
hard fork denial; Linux and Windows fail closed as unavailable. No portable
|
|
209
|
+
claim is inferred, no unrestricted command surface exists, and repository code
|
|
210
|
+
can still modify the authorized checkout.
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# Profile-based contained execution
|
|
2
|
+
|
|
3
|
+
`knodin` can execute one immutable repository-defined profile through the
|
|
4
|
+
single MCP gateway. It never accepts a command string, executable, argv, cwd,
|
|
5
|
+
environment, or limit from an MCP caller.
|
|
6
|
+
|
|
7
|
+
Execution is disabled by two independent defaults. Enable it globally in
|
|
8
|
+
`$XDG_CONFIG_HOME/knodin/execution.json` (or
|
|
9
|
+
`~/.config/knodin/execution.json`):
|
|
10
|
+
|
|
11
|
+
```json
|
|
12
|
+
{"execution":{"enabled":true}}
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Then define and enable exact profiles in `.knodin/execution.json`:
|
|
16
|
+
|
|
17
|
+
```json
|
|
18
|
+
{
|
|
19
|
+
"execution": {
|
|
20
|
+
"enabled": true,
|
|
21
|
+
"profiles": {
|
|
22
|
+
"test": {
|
|
23
|
+
"executable": "/opt/homebrew/bin/bun",
|
|
24
|
+
"args": ["run", "test"],
|
|
25
|
+
"network": "deny",
|
|
26
|
+
"timeoutMs": 120000,
|
|
27
|
+
"maxOutputBytes": 2097152,
|
|
28
|
+
"maxProcesses": 1,
|
|
29
|
+
"containment": "fully-contained"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
An agent may call `{ "operation": "execute", "profile": "test" }`. Profile
|
|
37
|
+
names are bounded identifiers. Shell strings and caller-supplied argument
|
|
38
|
+
suffixes are not part of the schema.
|
|
39
|
+
|
|
40
|
+
## Containment contract
|
|
41
|
+
|
|
42
|
+
The initial certified adapter is macOS `sandbox-exec`. It runs with a minimal
|
|
43
|
+
environment (`PATH`, `LANG`, `LC_ALL`, and repository-local `TMPDIR`), closed
|
|
44
|
+
stdin, no shell, repository cwd, a detached process group, hard timeout and
|
|
45
|
+
combined-output caps, and Seatbelt rules that:
|
|
46
|
+
|
|
47
|
+
- deny all network operations;
|
|
48
|
+
- deny process forks, making `maxProcesses: 1` a hard contract;
|
|
49
|
+
- deny all writes outside the repository;
|
|
50
|
+
- deny reads under user homes, mounted volumes, network mounts, and shared
|
|
51
|
+
temporary roots outside the repository; and
|
|
52
|
+
- allow repository reads/writes and read-only system runtime paths required to
|
|
53
|
+
load the approved executable.
|
|
54
|
+
|
|
55
|
+
Profiles needing child processes are rejected by this first contract. Define
|
|
56
|
+
the profile around a single-process test/lint/typecheck/build executable. The
|
|
57
|
+
macOS adapter reports `fully-contained` only when every configured boundary is
|
|
58
|
+
active. It also reports that `sandbox-exec` is deprecated; this certification
|
|
59
|
+
is tied to the checked-in adversarial tests, not a claim that Seatbelt is a
|
|
60
|
+
supported public Apple API.
|
|
61
|
+
|
|
62
|
+
Linux and Windows currently report `containment-unavailable` and do not spawn
|
|
63
|
+
the profile. A future Bubblewrap or Windows Job Object/restricted-token adapter
|
|
64
|
+
requires its own native evidence before it can change that result.
|
|
65
|
+
|
|
66
|
+
## Output, diagnosis, and audit
|
|
67
|
+
|
|
68
|
+
Captured stdout/stderr goes directly through recoverable compression. Failed,
|
|
69
|
+
timed-out, and output-limited MCP executions then request source-evidenced
|
|
70
|
+
failure diagnosis from the existing graph. Raw command text, argv, environment,
|
|
71
|
+
and output are never written to execution telemetry. The private repository
|
|
72
|
+
audit `.knodin/execution-audit.jsonl` records only timestamp, profile identity,
|
|
73
|
+
status, exit/signal, reported containment, and retained output-artifact identity.
|
|
74
|
+
|
|
75
|
+
Repository code can still be destructive inside the authorized repository.
|
|
76
|
+
This is a constrained trusted-repository workflow, not a guarantee that an
|
|
77
|
+
approved test/build profile cannot modify its own checkout.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Local troubleshooting diagnostics
|
|
2
|
+
|
|
3
|
+
knodin can retain a small, local diagnostic journal and build a redacted bundle
|
|
4
|
+
for a user to inspect and share with support. Diagnostics are disabled until the
|
|
5
|
+
user enables them, never upload automatically, and require no account or hosted
|
|
6
|
+
service.
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
knodin diagnostics enable --retention-days 14
|
|
10
|
+
knodin diagnostics status
|
|
11
|
+
knodin diagnostics collect --since 24h
|
|
12
|
+
knodin diagnostics inspect .knodin/diagnostics/knodin-diagnostics-….json.gz
|
|
13
|
+
knodin diagnostics clear
|
|
14
|
+
knodin diagnostics disable
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
`enable` records bounded failure envelopes from the CLI and MCP gateway. Each
|
|
18
|
+
record contains a timestamp, random correlation ID, surface, operation, phase,
|
|
19
|
+
safe error name/code, a message fingerprint, and sanitized stack shape. It does
|
|
20
|
+
not retain the command arguments, query, error message, source, raw output,
|
|
21
|
+
environment, username, repository path, or source paths. Records are stored in
|
|
22
|
+
`.knodin/diagnostics/events.jsonl`, mode `0600`, capped at 500 events, and
|
|
23
|
+
pruned to the configured 1–365 day retention window.
|
|
24
|
+
|
|
25
|
+
`collect` runs local installation and deep graph diagnostics and combines their
|
|
26
|
+
redacted results with recent failure envelopes, metadata-only telemetry when
|
|
27
|
+
present, and at most 64 KiB/200 lines from the lifecycle indexer log. The
|
|
28
|
+
default collection window is 24 hours; `--since` accepts hours or days. Bundle
|
|
29
|
+
output must remain inside the repository and cannot traverse a symlink.
|
|
30
|
+
|
|
31
|
+
The bundle is gzip-compressed JSON, written mode `0600`, and includes a privacy
|
|
32
|
+
manifest, explicit omissions, and a redaction count. `inspect` validates the
|
|
33
|
+
manifest and a 10 MiB compressed-size ceiling before returning its contents.
|
|
34
|
+
Users should inspect the bundle before attaching it to Jira, GitHub, email, or
|
|
35
|
+
another support channel. knodin does not transmit it.
|
|
36
|
+
|
|
37
|
+
`clear` removes only the event journal. It does not delete previously created
|
|
38
|
+
bundles or ROI telemetry. `disable` stops future failure recording but preserves
|
|
39
|
+
existing events so disabling never silently destroys troubleshooting evidence.
|
|
40
|
+
Run `clear` when deletion is intended.
|
|
41
|
+
|
|
42
|
+
Diagnostics are evidence, not runtime-causality proof. Message fingerprints can
|
|
43
|
+
group identical sanitized failures but cannot reconstruct the original message.
|
|
44
|
+
Redaction is defense in depth; users remain responsible for inspecting an
|
|
45
|
+
artifact before sharing it outside their organization.
|
|
@@ -69,8 +69,11 @@ the provenance/SBOM and five-channel attestation machinery; the production
|
|
|
69
69
|
release must populate it, and C65 must supply compromised-channel and recovery
|
|
70
70
|
evidence before this gate can open.
|
|
71
71
|
|
|
72
|
-
Run `knodin status --deep` for graph details and
|
|
73
|
-
|
|
72
|
+
Run `knodin status --deep` for graph details and follow its typed repair steps:
|
|
73
|
+
`knodin repair` repairs graph data, while `knodin init` restores lifecycle
|
|
74
|
+
routing. Re-run `knodin init` after changing the executable owner, MCP path, or
|
|
75
|
+
hook-manager installation. The complete refresh-path matrix is in
|
|
76
|
+
[`FRESHNESS-AND-LIFECYCLE.md`](FRESHNESS-AND-LIFECYCLE.md).
|
|
74
77
|
|
|
75
78
|
## Query availability
|
|
76
79
|
|