knodin 0.7.5 → 0.8.2
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 +18 -3
- package/benchmarks/competitors/SYNTHESIS.md +66 -0
- package/dist/bin/cli.js +371 -66
- package/dist/bin/launcher.js +16 -1
- package/dist/src/agent-integration.js +82 -16
- package/dist/src/artifact-refresh.js +2 -1
- package/dist/src/cli-args.js +19 -1
- package/dist/src/cli-model.js +28 -2
- package/dist/src/codeflow-replay.js +2 -1
- package/dist/src/compare.js +39 -0
- package/dist/src/competitive-constraints.js +2 -1
- package/dist/src/competitive-runner.js +4 -4
- package/dist/src/context-export.js +3 -2
- package/dist/src/context.js +1 -1
- package/dist/src/deterministic-random.js +34 -0
- package/dist/src/diagnostics-write-helper.js +473 -0
- package/dist/src/diagnostics.js +1160 -133
- package/dist/src/doctor.js +3 -1
- package/dist/src/engine/ann-hnsw.js +2 -12
- package/dist/src/engine/file-walker.js +8 -2
- package/dist/src/engine/git-history.js +12 -12
- package/dist/src/engine/index.js +1174 -313
- package/dist/src/engine/sarif-import.js +341 -0
- package/dist/src/engine/scip-import.js +28 -13
- package/dist/src/engine/source-policy.js +16 -0
- package/dist/src/engine/state-paths.js +175 -0
- package/dist/src/execution-profile.js +15 -10
- package/dist/src/failure-diagnosis.js +7 -1
- package/dist/src/graph-layout.js +173 -0
- package/dist/src/index-activity.js +2 -1
- package/dist/src/init.js +86 -45
- package/dist/src/lifecycle-health.js +41 -9
- package/dist/src/mcp-graph-worker.js +69 -0
- package/dist/src/mcp-reliability.js +154 -0
- package/dist/src/mcp-worker-supervisor.js +350 -0
- package/dist/src/mirror.js +290 -0
- package/dist/src/node-runtime.js +157 -0
- package/dist/src/output-compression.js +2 -1
- package/dist/src/output-telemetry.js +16 -11
- package/dist/src/progressive-evidence.js +30 -26
- package/dist/src/pure-compression-cli.js +4 -3
- package/dist/src/relationship-adapters.js +15 -8
- package/dist/src/release-preflight.js +13 -10
- package/dist/src/repair-lease.js +85 -0
- package/dist/src/repository-init-process.js +13 -9
- package/dist/src/repository-management.js +34 -4
- package/dist/src/response-budget.js +8 -6
- package/dist/src/server.js +80 -35
- package/dist/src/structural-fast-path.js +16 -10
- package/dist/src/structural-snapshot.js +6 -2
- package/dist/src/system-config.js +25 -2
- package/dist/src/tools/knodin-tools.js +142 -31
- package/dist/src/update-ceremony.js +9 -5
- package/dist/src/update-trust.js +5 -4
- package/dist/src/visualization.js +372 -19
- package/dist/src/worktree-lifecycle.js +5 -2
- package/docs/BEHAVIORAL-CONTRACT.md +72 -0
- package/docs/CLI.md +20 -1
- package/docs/COMPARISON.md +403 -0
- package/docs/COMPETITIVE-LANDSCAPE-2026-08.md +267 -0
- package/docs/DIAGNOSTICS.md +46 -11
- package/docs/HANDOFF.md +180 -0
- package/docs/INSTALLATION.md +21 -2
- package/docs/MCP.md +59 -8
- package/docs/PT-ACCESS-RECOMMENDATION.md +5 -7
- package/docs/REPOSITORIES-AND-WORKTREES.md +18 -6
- package/docs/SCIP-IMPORT.md +5 -0
- package/docs/TOKEN-OPTIMIZER-SCORECARD.md +79 -0
- package/docs/releases/0.5.1.md +4 -4
- package/docs/releases/0.8.0.md +74 -0
- package/docs/releases/0.8.2.md +34 -0
- package/package.json +17 -4
- package/roadmap/competitive-roadmap.md +3801 -0
- package/schemas/release-attestation-v1.schema.json +1 -1
- package/schemas/support-bundle-v2.schema.json +212 -0
|
@@ -12,6 +12,7 @@ import { resolveCliRuntimeCommand } from "../cli-args.js";
|
|
|
12
12
|
import { compactExplainResult, compactQueryResult, compactSearchResult, expandCompactIdentity, } from "../compact-structural.js";
|
|
13
13
|
import { buildKnodinContext } from "../context.js";
|
|
14
14
|
import { exportContext, grepPackedArtifact, readPackedArtifact } from "../context-export.js";
|
|
15
|
+
import { collectDiagnostics, persistDiagnosticsPreview } from "../diagnostics.js";
|
|
15
16
|
import { getDocSection, listDocTopics } from "../docs-sections.js";
|
|
16
17
|
import { diagnoseInstallation } from "../doctor.js";
|
|
17
18
|
import { createEngine, REPO_WIDE_QUERY_PATTERNS, } from "../engine/index.js";
|
|
@@ -21,6 +22,7 @@ import { diagnoseFailure, } from "../failure-diagnosis.js";
|
|
|
21
22
|
import { decorateGraphQueryResult, inspectGraphQueryHealth } from "../graph-query-health.js";
|
|
22
23
|
import { inspectRepositoryIntegrationStatus } from "../init.js";
|
|
23
24
|
import { attachLifecycleHealth, attachRepairLifecycle } from "../lifecycle-health.js";
|
|
25
|
+
import { listMirrors } from "../mirror.js";
|
|
24
26
|
import { compressOutput, compressOutputFile, deleteOutputArtifact, readOutputArtifact, } from "../output-compression.js";
|
|
25
27
|
import { appendTelemetryRecord, clearTelemetry, countOutputTokens, exportTelemetry, measureOutput, readTelemetryRecords, telemetryStatus, writeTelemetryReport, } from "../output-telemetry.js";
|
|
26
28
|
import { auditPullRequests, ghUnavailableReason, triagePrDetail } from "../pr-triage.js";
|
|
@@ -29,7 +31,7 @@ import { createRepairPlan } from "../repair-progress.js";
|
|
|
29
31
|
import { runRepositoryInitializationProcess } from "../repository-init-process.js";
|
|
30
32
|
import { discoverRepositories, initializeRepositories, inventoryRepository, searchRepositories, } from "../repository-management.js";
|
|
31
33
|
import { applyResponseBudget } from "../response-budget.js";
|
|
32
|
-
import { enrichSystemRelationships, incorporateSystemQueryEvidence, indexModeForPath, loadSystemConfiguration, queryConfiguredSystem, systemMembershipsForPath, validateSystemHealth, } from "../system-config.js";
|
|
34
|
+
import { configuredRepositoryInitMemoryLimitBytes, enrichSystemRelationships, incorporateSystemQueryEvidence, indexModeForPath, loadSystemConfiguration, queryConfiguredSystem, systemMembershipsForPath, validateSystemHealth, } from "../system-config.js";
|
|
33
35
|
import { trustedUpdateStatus } from "../update-policy.js";
|
|
34
36
|
import { waitForFresh } from "../wait-for-fresh.js";
|
|
35
37
|
import { inspectWorktrees, reconcileWorktrees, removeManagedWorktree, } from "../worktree-lifecycle.js";
|
|
@@ -177,10 +179,12 @@ function buildDocumentedKnodinTools() {
|
|
|
177
179
|
"worktrees",
|
|
178
180
|
"repair",
|
|
179
181
|
"repositories",
|
|
182
|
+
"remote",
|
|
180
183
|
"system",
|
|
181
184
|
"telemetry",
|
|
185
|
+
"diagnostics",
|
|
182
186
|
],
|
|
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.",
|
|
187
|
+
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. remote: list read-only mirrors of repositories that are reachable but not checked out locally, so you can run explain/query/map/search against one by passing its `path` as `repoPath`. Each mirror is a snapshot pinned at a commit; the remote is not watched. Acquiring, refreshing, and removing mirrors is CLI-only (`knodin remote add|refresh|remove`) because it clones another repository's source onto this machine and consumes disk nothing reclaims automatically — the user's decision to make, not an agent's.",
|
|
184
188
|
},
|
|
185
189
|
profile: {
|
|
186
190
|
type: "string",
|
|
@@ -442,7 +446,7 @@ function buildDocumentedKnodinTools() {
|
|
|
442
446
|
telemetryAction: {
|
|
443
447
|
type: "string",
|
|
444
448
|
enum: ["records", "status", "report", "export", "clear"],
|
|
445
|
-
description: "telemetry action.",
|
|
449
|
+
description: "telemetry action; diagnostics uses report (preview) or export (archive).",
|
|
446
450
|
},
|
|
447
451
|
telemetryRetentionDays: {
|
|
448
452
|
type: "integer",
|
|
@@ -631,17 +635,23 @@ function buildDocumentedKnodinTools() {
|
|
|
631
635
|
statusAudit: {
|
|
632
636
|
type: "string",
|
|
633
637
|
enum: ["cached", "deep"],
|
|
634
|
-
description: "status
|
|
638
|
+
description: "status: cached (default) or complete deep audit.",
|
|
635
639
|
},
|
|
636
640
|
timeoutMs: {
|
|
637
|
-
type: "
|
|
641
|
+
type: "integer",
|
|
638
642
|
minimum: 0,
|
|
639
|
-
maximum:
|
|
643
|
+
maximum: 300_000,
|
|
640
644
|
description: "wait: maximum milliseconds to wait for freshness.",
|
|
641
645
|
},
|
|
646
|
+
sinceHours: {
|
|
647
|
+
type: "integer",
|
|
648
|
+
minimum: 1,
|
|
649
|
+
maximum: 8_760,
|
|
650
|
+
description: "diagnostics: history window in hours (default 24).",
|
|
651
|
+
},
|
|
642
652
|
client: {
|
|
643
653
|
type: "string",
|
|
644
|
-
enum: ["claude", "codex", "gemini", "antigravity"],
|
|
654
|
+
enum: ["claude", "codex", "gemini", "copilot", "antigravity"],
|
|
645
655
|
description: "doctor: optionally return one client-specific integration diagnosis.",
|
|
646
656
|
},
|
|
647
657
|
repairPlan: {
|
|
@@ -662,26 +672,32 @@ function buildDocumentedKnodinTools() {
|
|
|
662
672
|
export function getDocumentedKnodinToolsForSchemaProof() {
|
|
663
673
|
return buildDocumentedKnodinTools();
|
|
664
674
|
}
|
|
665
|
-
const COMPACT_TOOL_DESCRIPTION = "Local code intelligence.";
|
|
666
675
|
const COMPACT_PARAMETER_DESCRIPTIONS = {
|
|
667
676
|
operation: "Capability.",
|
|
668
677
|
symbol: "Target.",
|
|
669
678
|
pattern: "Query pattern.",
|
|
670
|
-
impactMode: "Symbol or file
|
|
671
|
-
apply: "Apply
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
679
|
+
impactMode: "Symbol or file.",
|
|
680
|
+
apply: "Apply edits.",
|
|
681
|
+
// Shortened from "Review diff scope." / "Documentation section." on main: the
|
|
682
|
+
// `remote` operation added to the enum costs tokens, and this surface has a
|
|
683
|
+
// hard ceiling enforced by tool-schema-budget.spec.ts.
|
|
684
|
+
diffScope: "Diff scope.",
|
|
685
|
+
section: "Docs section.",
|
|
686
|
+
persistTelemetry: "Persist telemetry.",
|
|
675
687
|
};
|
|
676
|
-
|
|
677
|
-
|
|
688
|
+
/**
|
|
689
|
+
* The documented description is deliberately discarded: the compact surface
|
|
690
|
+
* carries only the curated one-liners, and falling back to full prose would
|
|
691
|
+
* reintroduce the per-tool context cost this surface exists to avoid.
|
|
692
|
+
*/
|
|
693
|
+
function compactParameterDescription(name, _description) {
|
|
678
694
|
return COMPACT_PARAMETER_DESCRIPTIONS[name];
|
|
679
695
|
}
|
|
680
696
|
/** The compact, always-on MCP surface. Full prose remains available via docs. */
|
|
681
697
|
export function getKnodinTools() {
|
|
682
698
|
const tools = buildDocumentedKnodinTools();
|
|
683
699
|
for (const tool of tools) {
|
|
684
|
-
tool.description =
|
|
700
|
+
tool.description = undefined;
|
|
685
701
|
const properties = tool.inputSchema.properties;
|
|
686
702
|
for (const [name, property] of Object.entries(properties)) {
|
|
687
703
|
property.description = compactParameterDescription(name, property.description);
|
|
@@ -703,6 +719,8 @@ const RESPONSE_DEFAULTS = {
|
|
|
703
719
|
"explain:minimal": { bytes: 16_384, tokens: 4_096, items: 25 },
|
|
704
720
|
map: { bytes: 131_072, tokens: 32_768, items: 200 },
|
|
705
721
|
"map:minimal": { bytes: 32_768, tokens: 8_192, items: 15 },
|
|
722
|
+
"diagnostics:preview": { bytes: 524_288, tokens: 131_072, items: 1_000 },
|
|
723
|
+
"diagnostics:archive": { bytes: 65_536, tokens: 16_384, items: 100 },
|
|
706
724
|
default: { bytes: 65_536, tokens: 16_384, items: 100 },
|
|
707
725
|
};
|
|
708
726
|
async function populateRepositoryDiagnoses(records, repositories, configRoot) {
|
|
@@ -757,6 +775,7 @@ async function handleRepositoriesOperation(options) {
|
|
|
757
775
|
if (repositoryAction === "init") {
|
|
758
776
|
const command = gatewayCliCommand();
|
|
759
777
|
const systemConfig = loadSystemConfiguration(discoveryRoots[0] ?? repo);
|
|
778
|
+
const memoryLimitBytes = configuredRepositoryInitMemoryLimitBytes(systemConfig);
|
|
760
779
|
const summary = await initializeRepositories(discoveryRoots, {
|
|
761
780
|
command,
|
|
762
781
|
index: (target) => engine.index(target),
|
|
@@ -766,7 +785,7 @@ async function handleRepositoriesOperation(options) {
|
|
|
766
785
|
dryRun,
|
|
767
786
|
manifestPath,
|
|
768
787
|
indexMode: (target) => indexModeForPath(systemConfig, target),
|
|
769
|
-
isolatedInitialize: (target) => runRepositoryInitializationProcess({ repository: target, command }),
|
|
788
|
+
isolatedInitialize: (target) => runRepositoryInitializationProcess({ repository: target, command, memoryLimitBytes }),
|
|
770
789
|
});
|
|
771
790
|
const { repositories } = await inventory();
|
|
772
791
|
return bounded({ schemaVersion: 1, ...summary, repositories }, "repositories:init");
|
|
@@ -898,8 +917,19 @@ function validateKnodinArgs(args) {
|
|
|
898
917
|
if (args.timeoutMs !== undefined &&
|
|
899
918
|
(!Number.isInteger(args.timeoutMs) || args.timeoutMs < 0 || args.timeoutMs > 300_000))
|
|
900
919
|
throw new Error("knodin: timeoutMs must be an integer between 0 and 300000");
|
|
920
|
+
if (args.operation === "diagnostics" &&
|
|
921
|
+
args.telemetryAction !== undefined &&
|
|
922
|
+
!["report", "export"].includes(args.telemetryAction))
|
|
923
|
+
throw new Error("knodin: diagnostics telemetryAction must be report or export");
|
|
924
|
+
if (args.sinceHours !== undefined &&
|
|
925
|
+
(!Number.isInteger(args.sinceHours) || args.sinceHours < 1 || args.sinceHours > 8760))
|
|
926
|
+
throw new Error("knodin: sinceHours must be an integer from 1 through 8760");
|
|
927
|
+
if (args.operation === "diagnostics" &&
|
|
928
|
+
args.artifactId !== undefined &&
|
|
929
|
+
!/^[a-f0-9]{32}$/.test(args.artifactId))
|
|
930
|
+
throw new Error("knodin: diagnostics artifactId must be a preview ID");
|
|
901
931
|
if (args.client !== undefined &&
|
|
902
|
-
!["claude", "codex", "gemini", "antigravity"].includes(args.client))
|
|
932
|
+
!["claude", "codex", "gemini", "copilot", "antigravity"].includes(args.client))
|
|
903
933
|
throw new Error("knodin: invalid client");
|
|
904
934
|
}
|
|
905
935
|
function compactExplainSource(result) {
|
|
@@ -1209,7 +1239,53 @@ async function tryCompactStructural(args, repo, selector, bounded) {
|
|
|
1209
1239
|
(await runCompactQuery(compactPattern, symbol ?? "", repo, Math.min(args.limit ?? args.itemBudget ?? 100, args.itemBudget ?? 1000), args.depth, selector, byteBudget)),
|
|
1210
1240
|
};
|
|
1211
1241
|
}
|
|
1212
|
-
|
|
1242
|
+
const mcpRequestSignals = new WeakMap();
|
|
1243
|
+
export function attachMcpRequestSignal(args, signal) {
|
|
1244
|
+
if (args && typeof args === "object")
|
|
1245
|
+
mcpRequestSignals.set(args, signal);
|
|
1246
|
+
return args;
|
|
1247
|
+
}
|
|
1248
|
+
async function repairLifecycleForRequest(repo, args) {
|
|
1249
|
+
const signal = args && typeof args === "object" ? mcpRequestSignals.get(args) : undefined;
|
|
1250
|
+
return attachRepairLifecycle(repo, await engine.repair(repo, { signal }));
|
|
1251
|
+
}
|
|
1252
|
+
async function handleDiagnosticsOperation(repo, args, bounded) {
|
|
1253
|
+
let graph;
|
|
1254
|
+
let doctor;
|
|
1255
|
+
try {
|
|
1256
|
+
graph = attachLifecycleHealth(repo, await engine.status(repo, { audit: "deep" }));
|
|
1257
|
+
}
|
|
1258
|
+
catch {
|
|
1259
|
+
// A support bundle must remain available when graph diagnostics fail.
|
|
1260
|
+
}
|
|
1261
|
+
if (graph) {
|
|
1262
|
+
try {
|
|
1263
|
+
doctor = await diagnoseInstallation(repo, {
|
|
1264
|
+
currentVersion: graph.version,
|
|
1265
|
+
runtimeCommand: [process.execPath, process.argv[1] ?? "", "serve"],
|
|
1266
|
+
graph,
|
|
1267
|
+
client: args.client,
|
|
1268
|
+
});
|
|
1269
|
+
}
|
|
1270
|
+
catch {
|
|
1271
|
+
// The allowlisted health section reports installation as unavailable.
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
const options = {
|
|
1275
|
+
sinceHours: args.sinceHours ?? 24,
|
|
1276
|
+
doctor,
|
|
1277
|
+
graph,
|
|
1278
|
+
knodinVersion: graph?.version,
|
|
1279
|
+
};
|
|
1280
|
+
return bounded(args.telemetryAction === "export"
|
|
1281
|
+
? collectDiagnostics(repo, {
|
|
1282
|
+
...options,
|
|
1283
|
+
outputPath: args.outputPath,
|
|
1284
|
+
previewId: args.artifactId,
|
|
1285
|
+
})
|
|
1286
|
+
: persistDiagnosticsPreview(repo, options), `diagnostics:${args.telemetryAction === "export" ? "archive" : "preview"}`);
|
|
1287
|
+
}
|
|
1288
|
+
async function dispatchKnodinTool(args) {
|
|
1213
1289
|
const startedAt = performance.now();
|
|
1214
1290
|
const parsedArgs = (args ?? {});
|
|
1215
1291
|
const { operation, symbol, base, diffScope, from, toRevision, reviewFiles, query, pattern, to, limit, depth, impactMode, direction, relationKinds, minConfidence, includeTests, includeDataFlow, flowVariable, apply, force, detailLevel, prNumber, prState, branches, auditRange, auditBase, auditHead, expectedLogin, worktreeAction, worktreePath, telemetryAction, telemetryRetentionDays, task, changedFiles, repoPath, section, systemAction, repositoryAction, roots, linkedWorktrees, cursor, allowPartial, dryRun, manifestPath, identity, file, kind, toIdentity, toFile, toKind, byteBudget, tokenBudget, itemBudget, includeSource, languages, extensions, kinds, path, architectureFacets, testScope, offset, minLines, minComplexity, topN, sort, packAction, format, include, exclude, filePolicies, alreadyPresent, chatFiles, lineNumbers, includeTree, outputPath, artifactPath, startLine, endLine, regex, regexFlags, gitDiffScope, gitLog, statusAudit, timeoutMs, client, repairPlan, persistTelemetry, } = parsedArgs;
|
|
@@ -1230,9 +1306,8 @@ export async function handleKnodinTool(args) {
|
|
|
1230
1306
|
const observe = (output, op, minimal = false) => {
|
|
1231
1307
|
const root = output;
|
|
1232
1308
|
const budgetMeta = root.responseBudget;
|
|
1233
|
-
if (op === "review")
|
|
1309
|
+
if (op === "review")
|
|
1234
1310
|
measurePerfPhaseSync("review_serialization", () => JSON.stringify(output));
|
|
1235
|
-
}
|
|
1236
1311
|
const telemetry = measureOutput({
|
|
1237
1312
|
repo,
|
|
1238
1313
|
operation: op,
|
|
@@ -1243,18 +1318,16 @@ export async function handleKnodinTool(args) {
|
|
|
1243
1318
|
root.telemetry?.truncated === true,
|
|
1244
1319
|
schemaTokens: gatewaySchemaTokens(),
|
|
1245
1320
|
});
|
|
1246
|
-
if (op === "execute" && parsedArgs.profile)
|
|
1321
|
+
if (op === "execute" && parsedArgs.profile)
|
|
1247
1322
|
telemetry.executionProfile = parsedArgs.profile;
|
|
1248
|
-
}
|
|
1249
1323
|
telemetry.temperature = localTelemetry.some(({ operation, repositoryId }) => operation === op && repositoryId === telemetry.repositoryId)
|
|
1250
1324
|
? "warm"
|
|
1251
1325
|
: "cold";
|
|
1252
1326
|
localTelemetry.push(telemetry);
|
|
1253
1327
|
if (localTelemetry.length > 1000)
|
|
1254
1328
|
localTelemetry.shift();
|
|
1255
|
-
if (persistTelemetry === true)
|
|
1329
|
+
if (persistTelemetry === true)
|
|
1256
1330
|
appendTelemetryRecord(repo, telemetry, telemetryRetentionDays);
|
|
1257
|
-
}
|
|
1258
1331
|
return root;
|
|
1259
1332
|
};
|
|
1260
1333
|
const bounded = (result, op = String(operation), minimal = false) => {
|
|
@@ -1279,6 +1352,8 @@ export async function handleKnodinTool(args) {
|
|
|
1279
1352
|
if (detailLevel === "compact")
|
|
1280
1353
|
throw new Error("knodin: compact detail is unsupported here");
|
|
1281
1354
|
switch (operation) {
|
|
1355
|
+
case "diagnostics":
|
|
1356
|
+
return handleDiagnosticsOperation(repo, parsedArgs, bounded);
|
|
1282
1357
|
case "telemetry": {
|
|
1283
1358
|
const action = telemetryAction ?? "records";
|
|
1284
1359
|
if (action === "records")
|
|
@@ -1324,7 +1399,29 @@ export async function handleKnodinTool(args) {
|
|
|
1324
1399
|
case "repair":
|
|
1325
1400
|
if (repairPlan)
|
|
1326
1401
|
return bounded(createRepairPlan(await engine.status(repo, { audit: "deep" })), "repair:plan");
|
|
1327
|
-
return bounded(
|
|
1402
|
+
return bounded(await repairLifecycleForRequest(repo, args), "repair");
|
|
1403
|
+
case "remote": {
|
|
1404
|
+
// Read-only over MCP by design. Acquiring a mirror clones another
|
|
1405
|
+
// repository's source onto this machine and consumes disk that nothing
|
|
1406
|
+
// reclaims automatically; refresh discards local state and remove
|
|
1407
|
+
// deletes a graph that cost minutes to build. Those are the user's
|
|
1408
|
+
// calls, made explicitly at the CLI, not side effects of an agent's
|
|
1409
|
+
// investigation. Listing is safe and is what makes a mirror usable:
|
|
1410
|
+
// pass a listed `path` as `repoPath` to explain/query/map/search it.
|
|
1411
|
+
const mirrors = listMirrors().map((mirror) => ({
|
|
1412
|
+
identity: mirror.identity,
|
|
1413
|
+
url: mirror.url,
|
|
1414
|
+
path: mirror.path,
|
|
1415
|
+
snapshot: mirror.sha,
|
|
1416
|
+
fetchedAt: mirror.fetchedAt,
|
|
1417
|
+
readOnly: true,
|
|
1418
|
+
}));
|
|
1419
|
+
return bounded({
|
|
1420
|
+
mirrors,
|
|
1421
|
+
acquisition: "CLI only: `knodin remote add <url>` / `refresh <identity>` / `remove <identity>`.",
|
|
1422
|
+
note: "Each mirror is a snapshot pinned at `snapshot`; the remote is not watched, so results describe that commit and no later one.",
|
|
1423
|
+
}, "remote:list");
|
|
1424
|
+
}
|
|
1328
1425
|
case "repositories": {
|
|
1329
1426
|
if (!repositoryAction)
|
|
1330
1427
|
throw new Error("knodin repositories requires `repositoryAction`");
|
|
@@ -1517,13 +1614,27 @@ export async function handleKnodinTool(args) {
|
|
|
1517
1614
|
const result = pattern === "impact" || pattern === "dead_code"
|
|
1518
1615
|
? incorporateSystemQueryEvidence(await enrichSystemRelationships(loadSystemConfiguration(repo)), repo, pattern, symbol ?? "", queryResult)
|
|
1519
1616
|
: queryResult;
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
:
|
|
1617
|
+
let decorated = result;
|
|
1618
|
+
if (queryHealth?.available) {
|
|
1619
|
+
// A verified probe supersedes the unverified one for both fields, so
|
|
1620
|
+
// state and freshness always come from the same observation.
|
|
1621
|
+
const authoritative = verifiedQueryHealth?.available ? verifiedQueryHealth : queryHealth;
|
|
1622
|
+
decorated = decorateGraphQueryResult(result, authoritative.state, authoritative.graph.freshness);
|
|
1623
|
+
}
|
|
1624
|
+
return bounded(decorated, `query:${pattern}`);
|
|
1525
1625
|
}
|
|
1526
1626
|
default:
|
|
1527
1627
|
throw new Error(`unknown knodin operation: ${String(operation)}`);
|
|
1528
1628
|
}
|
|
1529
1629
|
}
|
|
1630
|
+
let responseTestMutator;
|
|
1631
|
+
/** @internal Test-only response seam. Production callers must never register a mutator. */
|
|
1632
|
+
export function setKnodinResponseTestMutator(mutator) {
|
|
1633
|
+
if (process.env.KNODIN_C95_TEST_MODE !== "offline-contract-replay")
|
|
1634
|
+
throw new Error("knodin: C95 response mutation seam is unavailable outside its test replay");
|
|
1635
|
+
responseTestMutator = mutator;
|
|
1636
|
+
}
|
|
1637
|
+
export async function handleKnodinTool(args) {
|
|
1638
|
+
const response = await dispatchKnodinTool(args);
|
|
1639
|
+
return responseTestMutator ? responseTestMutator(args, response) : response;
|
|
1640
|
+
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { createHash, createPublicKey } from "node:crypto";
|
|
2
|
+
import { compareBytes } from "./compare.js";
|
|
2
3
|
import { canonicalizeUpdateMetadata, verifyUpdateRootChain, } from "./update-trust.js";
|
|
3
4
|
const SHA256 = /^[0-9a-f]{64}$/;
|
|
4
5
|
const ID = /^[A-Za-z0-9][A-Za-z0-9._-]{2,63}$/;
|
|
5
6
|
function exactKeys(value, expected, label) {
|
|
6
|
-
const actual = Object.keys(value).sort();
|
|
7
|
-
const wanted = [...expected].sort();
|
|
7
|
+
const actual = Object.keys(value).sort(compareBytes);
|
|
8
|
+
const wanted = [...expected].sort(compareBytes);
|
|
8
9
|
if (actual.join("\0") !== wanted.join("\0"))
|
|
9
10
|
throw new Error(`${label} contains missing or unknown fields`);
|
|
10
11
|
}
|
|
@@ -78,8 +79,10 @@ export function validateRootCeremonyManifest(value) {
|
|
|
78
79
|
const assigned = manifest.custodians
|
|
79
80
|
.filter((c) => c.role === "root-signer")
|
|
80
81
|
.map((c) => c.publicKeyId)
|
|
81
|
-
|
|
82
|
-
|
|
82
|
+
// `publicKeyId` is optional on the type but validated present for every
|
|
83
|
+
// root-signer above; `sort` also never passes undefined to a comparator.
|
|
84
|
+
.sort((left, right) => compareBytes(left ?? "", right ?? ""));
|
|
85
|
+
if (assigned.join("\0") !== [...manifest.roleKeyIds.root].sort(compareBytes).join("\0"))
|
|
83
86
|
throw new Error("root signer assignments must exactly match root role keys");
|
|
84
87
|
const recovery = manifest.custodians.filter((c) => c.role === "recovery");
|
|
85
88
|
if (recovery.length < manifest.thresholds.root ||
|
|
@@ -134,7 +137,8 @@ export function preparePublicRootImport(envelope, pin, manifestValue, receiptVal
|
|
|
134
137
|
const expected = manifest.roleKeyIds[role];
|
|
135
138
|
const actual = envelope.signed.roles[role];
|
|
136
139
|
if (actual.threshold !== manifest.thresholds[role] ||
|
|
137
|
-
[...actual.keyids].sort().join("\0") !==
|
|
140
|
+
[...actual.keyids].sort(compareBytes).join("\0") !==
|
|
141
|
+
[...expected].sort(compareBytes).join("\0"))
|
|
138
142
|
throw new Error(`manifest ${role} role does not match root envelope`);
|
|
139
143
|
}
|
|
140
144
|
for (const key of Object.values(envelope.signed.keys)) {
|
package/dist/src/update-trust.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash, createPublicKey, verify as cryptoVerify, timingSafeEqual, } from "node:crypto";
|
|
2
|
+
import { compareBytes } from "./compare.js";
|
|
2
3
|
export class UpdateTrustError extends Error {
|
|
3
4
|
code;
|
|
4
5
|
role;
|
|
@@ -53,10 +54,10 @@ function canonicalize(value, seen) {
|
|
|
53
54
|
seen.add(value);
|
|
54
55
|
const object = value;
|
|
55
56
|
const serialized = `{${Object.keys(object)
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
.sort()
|
|
57
|
+
// Code-unit ordering is locale-independent, which is required because these
|
|
58
|
+
// bytes are signed on one host and verified on another. A localeCompare
|
|
59
|
+
// comparator would make trust bytes depend on host ICU data.
|
|
60
|
+
.sort(compareBytes)
|
|
60
61
|
.map((key) => {
|
|
61
62
|
if (object[key] === undefined)
|
|
62
63
|
invalid(`metadata field ${key} must not be undefined`);
|