knodin 0.5.1 → 0.7.3
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 +190 -535
- package/dist/bin/cli.js +173 -17
- package/dist/bin/launcher.js +11 -0
- package/dist/src/agent-integration.js +4 -16
- package/dist/src/artifact-refresh.js +1 -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 +4 -4
- package/dist/src/context-export.js +1 -1
- package/dist/src/diagnostics.js +449 -0
- package/dist/src/docs-sections.js +5 -5
- package/dist/src/engine/ann-hnsw.js +5 -5
- package/dist/src/engine/embeddings.js +4 -4
- package/dist/src/engine/git-history.js +289 -0
- package/dist/src/engine/index.js +450 -103
- package/dist/src/engine/prune.js +2 -2
- package/dist/src/engine/scip-import.js +408 -0
- package/dist/src/engine/symbol-delete.js +1 -1
- 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/index-activity.js +1 -1
- package/dist/src/init-progress-worker.js +1 -1
- package/dist/src/init.js +361 -92
- package/dist/src/lifecycle-health.js +51 -14
- package/dist/src/lsp-readonly.js +1 -1
- package/dist/src/output-compression.js +1 -1
- package/dist/src/output-telemetry.js +8 -4
- package/dist/src/pr-triage.js +2 -2
- 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/repair-progress-worker.js +1 -1
- package/dist/src/repository-init-process.js +1 -1
- package/dist/src/repository-management.js +145 -3
- package/dist/src/response-budget.js +11 -1
- package/dist/src/server.js +23 -3
- package/dist/src/structural-fast-path.js +303 -0
- package/dist/src/structural-snapshot.js +33 -0
- package/dist/src/system-config.js +7 -7
- package/dist/src/tools/knodin-tools.js +106 -15
- package/dist/src/update-ceremony.js +158 -0
- package/dist/src/update-policy.js +4 -4
- package/dist/src/wait-for-fresh.js +2 -2
- package/dist/src/worktree-lifecycle.js +2 -2
- package/docs/CLI.md +22 -0
- package/docs/COMMAND-OUTPUT-COMPRESSION.md +32 -16
- package/docs/CONTAINED-EXECUTION.md +77 -0
- package/docs/DIAGNOSTICS.md +45 -0
- package/docs/DOCTOR-AND-UPDATES.md +7 -4
- package/docs/GIT-HISTORY-REVIEW.md +39 -0
- package/docs/INSTALLATION.md +1 -1
- package/docs/MCP.md +15 -0
- package/docs/PROGRESSIVE-EVIDENCE.md +37 -0
- package/docs/REPOSITORIES-AND-WORKTREES.md +31 -1
- package/docs/SCIP-IMPORT.md +57 -0
- package/docs/SIGNED-UPDATES.md +5 -0
- package/docs/SYSTEMS-AND-RELATIONSHIPS.md +2 -2
- package/docs/TELEMETRY.md +6 -2
- package/docs/releases/0.6.0.md +18 -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/package.json +34 -3
- package/dist/src/tools/reckon-tools.js +0 -5
|
@@ -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 `.
|
|
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.).",
|
|
@@ -610,7 +650,7 @@ function buildDocumentedKnodinTools() {
|
|
|
610
650
|
},
|
|
611
651
|
persistTelemetry: {
|
|
612
652
|
type: "boolean",
|
|
613
|
-
description: "Opt in to local metadata-only JSONL persistence at .
|
|
653
|
+
description: "Opt in to local metadata-only JSONL persistence at .knodin-telemetry.jsonl; never records source.",
|
|
614
654
|
},
|
|
615
655
|
},
|
|
616
656
|
required: ["operation"],
|
|
@@ -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
|
+
}
|
|
@@ -100,8 +100,8 @@ export function claimScheduledUpdateCheck(options) {
|
|
|
100
100
|
if (!config ||
|
|
101
101
|
!config.enabled ||
|
|
102
102
|
config.offline ||
|
|
103
|
-
env.
|
|
104
|
-
env.
|
|
103
|
+
env.KNODIN_UPDATE_CHECK === "0" ||
|
|
104
|
+
env.KNODIN_OFFLINE === "1")
|
|
105
105
|
return false;
|
|
106
106
|
const now = options.now?.() ?? new Date();
|
|
107
107
|
const checkedAt = readTrustedUpdateState(options.stateHome)?.checkedAt;
|
|
@@ -474,9 +474,9 @@ async function checkedUpdate(options) {
|
|
|
474
474
|
if (!config)
|
|
475
475
|
return baseResult(options, "trust-unconfigured");
|
|
476
476
|
const env = options.env ?? process.env;
|
|
477
|
-
if (!config.enabled || env.
|
|
477
|
+
if (!config.enabled || env.KNODIN_UPDATE_CHECK === "0")
|
|
478
478
|
return baseResult(options, "disabled");
|
|
479
|
-
if (config.offline || env.
|
|
479
|
+
if (config.offline || env.KNODIN_OFFLINE === "1")
|
|
480
480
|
return baseResult(options, "offline");
|
|
481
481
|
const now = options.now?.() ?? new Date();
|
|
482
482
|
const previous = readTrustedUpdateState(options.stateHome);
|
|
@@ -6,7 +6,7 @@ const delay = (milliseconds) => new Promise((resolve) => setTimeout(resolve, mil
|
|
|
6
6
|
const PROCESSOR_RETRY_MS = 500;
|
|
7
7
|
function installedProcessorIsRunning(repo) {
|
|
8
8
|
try {
|
|
9
|
-
const pid = Number(fs.readFileSync(path.join(repo, ".
|
|
9
|
+
const pid = Number(fs.readFileSync(path.join(repo, ".knodin", "hooks", "refresh.lock", "pid"), "utf-8"));
|
|
10
10
|
if (!Number.isInteger(pid) || pid <= 0)
|
|
11
11
|
return false;
|
|
12
12
|
process.kill(pid, 0);
|
|
@@ -17,7 +17,7 @@ function installedProcessorIsRunning(repo) {
|
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
export function launchInstalledLifecycleProcessor(repo) {
|
|
20
|
-
const processor = path.join(repo, ".
|
|
20
|
+
const processor = path.join(repo, ".knodin", "hooks", "background-index.sh");
|
|
21
21
|
if (fs.statSync(processor).mode & 0o111) {
|
|
22
22
|
const child = childProcess.spawn(processor, [], {
|
|
23
23
|
cwd: repo,
|
|
@@ -124,7 +124,7 @@ export async function inspectWorktrees(repoPath, status) {
|
|
|
124
124
|
for (const worktree of current) {
|
|
125
125
|
const identity = identityFor(repo, worktree.path, worktree.classification);
|
|
126
126
|
const registered = registry.entries.some((entry) => entry.identity === identity);
|
|
127
|
-
const graphInitialized = fs.existsSync(path.join(worktree.path, ".
|
|
127
|
+
const graphInitialized = fs.existsSync(path.join(worktree.path, ".knodin", "db.sqlite"));
|
|
128
128
|
let indexedHead = null;
|
|
129
129
|
if (graphInitialized) {
|
|
130
130
|
try {
|
|
@@ -160,7 +160,7 @@ export async function inspectWorktrees(repoPath, status) {
|
|
|
160
160
|
locked: false,
|
|
161
161
|
prunable: "registered worktree is absent from Git's worktree inventory",
|
|
162
162
|
registered: true,
|
|
163
|
-
graphInitialized: fs.existsSync(path.join(entry.path, ".
|
|
163
|
+
graphInitialized: fs.existsSync(path.join(entry.path, ".knodin", "db.sqlite")),
|
|
164
164
|
indexedHead: null,
|
|
165
165
|
freshness: "removed",
|
|
166
166
|
removedAt: entry.removedAt ?? null,
|
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
|
|
@@ -152,7 +165,7 @@ runtime evidence.
|
|
|
152
165
|
|
|
153
166
|
## Local retention and redaction
|
|
154
167
|
|
|
155
|
-
Artifacts are stored beneath `.
|
|
168
|
+
Artifacts are stored beneath `.knodin/output/` with a private directory mode
|
|
156
169
|
and `0600` files. The default returned content and bounded reads redact common
|
|
157
170
|
credential forms and remove ANSI/control bytes. `--raw` is an explicit local
|
|
158
171
|
request and can reveal the original secrets. Redaction is defense in depth,
|
|
@@ -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.
|