knodin 0.8.7 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/cli.js +307 -85
- package/dist/fixtures/update-verification/config/application.xml +4 -0
- package/dist/fixtures/update-verification/dbt/manifest.json +17 -0
- package/dist/fixtures/update-verification/dbt/models/accounts.sql +1 -0
- package/dist/fixtures/update-verification/dbt/models/contacts.sql +1 -0
- package/dist/fixtures/update-verification/fixture.json +147 -0
- package/dist/fixtures/update-verification/force-app/main/default/classes/UpdateFixture.cls +14 -0
- package/dist/fixtures/update-verification/force-app/main/default/flows/Update_Verification.flow-meta.xml +9 -0
- package/dist/fixtures/update-verification/package.json +5 -0
- package/dist/fixtures/update-verification/prisma/schema.prisma +10 -0
- package/dist/fixtures/update-verification/sql/functions.sql +16 -0
- package/dist/fixtures/update-verification/src/CsharpFixture.cs +6 -0
- package/dist/fixtures/update-verification/src/JavaFixture.java +9 -0
- package/dist/fixtures/update-verification/src/javascript.js +7 -0
- package/dist/fixtures/update-verification/src/python_fixture.py +6 -0
- package/dist/fixtures/update-verification/src/typescript.ts +7 -0
- package/dist/fixtures/update-verification/terraform/main.tf +7 -0
- package/dist/fixtures/update-verification/terraform/modules/verified-child/main.tf +3 -0
- package/dist/src/agent-integration.js +26 -3
- package/dist/src/backup-retention.js +811 -0
- package/dist/src/cli-model.js +35 -8
- package/dist/src/docs-sections.js +1 -0
- package/dist/src/doctor.js +142 -12
- package/dist/src/engine/embeddings.js +8 -1
- package/dist/src/engine/index.js +355 -292
- package/dist/src/init.js +222 -179
- package/dist/src/manager-update.js +455 -0
- package/dist/src/release-preflight.js +106 -95
- package/dist/src/repair-lease.js +77 -8
- package/dist/src/response-budget.js +5 -1
- package/dist/src/server.js +18 -5
- package/dist/src/shared-index/compatibility.js +4 -2
- package/dist/src/tools/knodin-tools.js +69 -10
- package/dist/src/update-ceremony.js +20 -24
- package/dist/src/update-coordination.js +158 -0
- package/dist/src/update-executor.js +336 -0
- package/dist/src/update-verifier.js +358 -0
- package/docs/BACKUP-RETENTION.md +54 -0
- package/docs/CLI.md +6 -0
- package/docs/DOCTOR-AND-UPDATES.md +44 -12
- package/docs/SIGNED-UPDATES.md +29 -22
- package/docs/releases/0.9.0.md +43 -0
- package/package.json +7 -1
- package/roadmap/competitive-roadmap.md +8 -0
|
@@ -5,15 +5,6 @@ import path from "node:path";
|
|
|
5
5
|
import { compareBytes } from "./compare.js";
|
|
6
6
|
import { validateRootCeremonyManifest } from "./update-ceremony.js";
|
|
7
7
|
import { canonicalizeUpdateMetadata, verifyUpdateRootChain, } from "./update-trust.js";
|
|
8
|
-
export const RELEASE_PREFLIGHT_DEPENDENCIES = [
|
|
9
|
-
"C60",
|
|
10
|
-
"C62",
|
|
11
|
-
"C63",
|
|
12
|
-
"C64",
|
|
13
|
-
"C65",
|
|
14
|
-
"C66",
|
|
15
|
-
"C75",
|
|
16
|
-
];
|
|
17
8
|
export const RELEASE_PREFLIGHT_REPOSITORY_COMMANDS = [
|
|
18
9
|
"npm ci --ignore-scripts",
|
|
19
10
|
"npm run lint",
|
|
@@ -47,6 +38,20 @@ const COMMAND_CATEGORIES = [
|
|
|
47
38
|
"attestation",
|
|
48
39
|
];
|
|
49
40
|
const TARGET_PLATFORMS = ["macOS", "Linux", "Windows"];
|
|
41
|
+
const VERIFIER_COVERAGE = [
|
|
42
|
+
"typescript",
|
|
43
|
+
"javascript",
|
|
44
|
+
"python",
|
|
45
|
+
"java",
|
|
46
|
+
"csharp",
|
|
47
|
+
"apex",
|
|
48
|
+
"sql",
|
|
49
|
+
"prisma",
|
|
50
|
+
"xml",
|
|
51
|
+
"salesforce",
|
|
52
|
+
"terraform",
|
|
53
|
+
"dbt",
|
|
54
|
+
];
|
|
50
55
|
const MAX_MANIFEST_BYTES = 1 * 1_024 * 1_024;
|
|
51
56
|
const MAX_EVIDENCE_BYTES = 16 * 1_024 * 1_024;
|
|
52
57
|
const MAX_TOTAL_EVIDENCE_BYTES = 128 * 1_024 * 1_024;
|
|
@@ -54,10 +59,8 @@ const COMMIT = /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/;
|
|
|
54
59
|
const SHA256 = /^[a-f0-9]{64}$/;
|
|
55
60
|
const SEMVER = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-(?:0|[1-9]\d*|[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|[A-Za-z-][0-9A-Za-z-]*))*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
|
56
61
|
const REPOSITORY_PATH = /^[A-Za-z0-9._+/-]+$/;
|
|
57
|
-
const IMMUTABLE_EVIDENCE_PATH = /^(?:docs\/evidence|benchmarks\/evaluations)\//;
|
|
58
62
|
const RUNTIME_NAME = /^[a-z][a-z0-9._-]{0,31}$/;
|
|
59
63
|
const RUNTIME_VERSION = /^[0-9A-Za-z][0-9A-Za-z._+-]{0,63}$/;
|
|
60
|
-
const TERMINAL_STATUS = /^(?:implemented(?: \([^()\n]+\)| — [^\n]+)?|evaluated — [^\n]+|certified(?: with [^\n]+| — [^\n]+)?|closed(?:; [^\n]+| — [^\n]+)?|done(?: — [^\n]+)?)$/;
|
|
61
64
|
function fail(message) {
|
|
62
65
|
throw new Error(`release preflight failed: ${message}`);
|
|
63
66
|
}
|
|
@@ -110,7 +113,7 @@ function parsePlan(value) {
|
|
|
110
113
|
"trustCeremonyManifestPath",
|
|
111
114
|
"trustReviewReceiptPath",
|
|
112
115
|
"verificationCommands",
|
|
113
|
-
"
|
|
116
|
+
"technicalEvidence",
|
|
114
117
|
"evidenceDirectory",
|
|
115
118
|
], "release plan");
|
|
116
119
|
if (plan.schemaVersion !== 1)
|
|
@@ -151,9 +154,8 @@ function parsePlan(value) {
|
|
|
151
154
|
!runtimeNames.includes("npm"))
|
|
152
155
|
fail("runtimes must uniquely include node and npm");
|
|
153
156
|
const certifiedTargetPlatforms = boundedStringArray(plan.certifiedTargetPlatforms, "certified target platforms", { maximumItems: 3, maximumLength: 16 });
|
|
154
|
-
if (certifiedTargetPlatforms.
|
|
155
|
-
|
|
156
|
-
fail("certified target platforms must contain exactly macOS, Linux, and Windows");
|
|
157
|
+
if (certifiedTargetPlatforms.some((platform) => !TARGET_PLATFORMS.includes(platform)))
|
|
158
|
+
fail("certified target platforms may contain only macOS, Linux, or Windows");
|
|
157
159
|
const trustRootSha256 = text(plan.trustRootSha256, "trust-root digest", 64);
|
|
158
160
|
if (!SHA256.test(trustRootSha256))
|
|
159
161
|
fail("trust-root digest must be a lowercase SHA-256 digest");
|
|
@@ -179,12 +181,23 @@ function parsePlan(value) {
|
|
|
179
181
|
expected.some((command, index) => verificationCommands[category][index] !== command))
|
|
180
182
|
fail(`${category} verification commands must equal the canonical ordered replay set`);
|
|
181
183
|
}
|
|
182
|
-
const
|
|
183
|
-
exactKeys(
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
184
|
+
const technicalEvidenceRecord = record(plan.technicalEvidence, "technical evidence");
|
|
185
|
+
exactKeys(technicalEvidenceRecord, ["repository", "platforms"], "technical evidence");
|
|
186
|
+
const repositoryEvidencePath = safeRelativePath(technicalEvidenceRecord.repository, "repository technical evidence path");
|
|
187
|
+
if (!Array.isArray(technicalEvidenceRecord.platforms))
|
|
188
|
+
fail("platform technical evidence must be an array");
|
|
189
|
+
const platformEvidence = technicalEvidenceRecord.platforms.map((candidate, index) => {
|
|
190
|
+
const item = record(candidate, `platform technical evidence[${index}]`);
|
|
191
|
+
exactKeys(item, ["platform", "path"], `platform technical evidence[${index}]`);
|
|
192
|
+
return {
|
|
193
|
+
platform: text(item.platform, `platform technical evidence[${index}].platform`, 16),
|
|
194
|
+
path: safeRelativePath(item.path, `platform technical evidence[${index}].path`),
|
|
195
|
+
};
|
|
196
|
+
});
|
|
197
|
+
if (platformEvidence.length !== certifiedTargetPlatforms.length ||
|
|
198
|
+
new Set(platformEvidence.map(({ platform }) => platform)).size !== platformEvidence.length ||
|
|
199
|
+
certifiedTargetPlatforms.some((platform) => !platformEvidence.some((evidence) => evidence.platform === platform)))
|
|
200
|
+
fail("platform technical evidence must exactly cover certified target platforms");
|
|
188
201
|
return {
|
|
189
202
|
schemaVersion: 1,
|
|
190
203
|
version,
|
|
@@ -198,7 +211,10 @@ function parsePlan(value) {
|
|
|
198
211
|
trustCeremonyManifestPath,
|
|
199
212
|
trustReviewReceiptPath,
|
|
200
213
|
verificationCommands,
|
|
201
|
-
|
|
214
|
+
technicalEvidence: {
|
|
215
|
+
repository: repositoryEvidencePath,
|
|
216
|
+
platforms: platformEvidence,
|
|
217
|
+
},
|
|
202
218
|
evidenceDirectory: safeRelativePath(plan.evidenceDirectory, "evidence directory"),
|
|
203
219
|
};
|
|
204
220
|
}
|
|
@@ -318,42 +334,6 @@ function requireTaggedBytes(git, commit, relativePath, bytes, label) {
|
|
|
318
334
|
if (!tagged.equals(bytes))
|
|
319
335
|
fail(`${label} bytes do not equal the exact source-commit blob`);
|
|
320
336
|
}
|
|
321
|
-
function verifyRoadmap(roadmap) {
|
|
322
|
-
const activeLedger = /### Track A — Active local-work ledger\n[\s\S]*?\n\| ID \| State \| Previous state \| Depends on \| Exit evidence \|\n\|---\|---\|---\|---\|---\|\n((?:\| C[^\n]+\n)*)\n### Track B — Parked external-evidence ledger/.exec(roadmap);
|
|
323
|
-
const parkedLedger = /### Track B — Parked external-evidence ledger\n[\s\S]*?\n\| ID \| State \| Previous state \| Depends on \| Exit evidence \|\n\|---\|---\|---\|---\|---\|\n((?:\| C[^\n]+\n)*)\n### Goal-loop ordering/.exec(roadmap);
|
|
324
|
-
if (!activeLedger || !parkedLedger)
|
|
325
|
-
fail("two-track roadmap ledger is missing or malformed");
|
|
326
|
-
const activeIds = new Set([...activeLedger[1].matchAll(/^\| (C\d+) \|/gm)].map((match) => match[1]));
|
|
327
|
-
const parkedIds = new Set([...parkedLedger[1].matchAll(/^\| (C\d+) \|/gm)].map((match) => match[1]));
|
|
328
|
-
if (parkedIds.size > 0)
|
|
329
|
-
fail(`Track B remains parked (${parkedIds.size} external item${parkedIds.size === 1 ? "" : "s"}: ${[...parkedIds].join(", ")})`);
|
|
330
|
-
const evidencePaths = new Map();
|
|
331
|
-
for (const dependency of RELEASE_PREFLIGHT_DEPENDENCIES) {
|
|
332
|
-
if (activeIds.has(dependency))
|
|
333
|
-
fail(`${dependency} remains in Track A`);
|
|
334
|
-
const header = dependency === "C75" ? "C69–C75" : dependency;
|
|
335
|
-
const section = new RegExp(String.raw `(^#{2,3} ${header} —[^\n]*\n[\s\S]*?)(?=^#{2,3} C(?:\d+|\d+–C\d+) —|(?![\s\S]))`, "m").exec(roadmap);
|
|
336
|
-
if (!section)
|
|
337
|
-
fail(`${dependency} has no authoritative item status`);
|
|
338
|
-
const statusMatch = /^- Status: ([^\n]+)$/m.exec(section[1]);
|
|
339
|
-
if (!statusMatch)
|
|
340
|
-
fail(`${dependency} has no authoritative item status`);
|
|
341
|
-
const status = statusMatch[1].trim();
|
|
342
|
-
if (!TERMINAL_STATUS.test(status))
|
|
343
|
-
fail(`${dependency} has nonterminal status ${JSON.stringify(status)}`);
|
|
344
|
-
const evidenceLabel = dependency === "C75" ? "C75 Evidence" : "Evidence";
|
|
345
|
-
const evidenceField = new RegExp(String.raw `^- ${evidenceLabel}: ([\s\S]*?)(?=\n\n|\n- [A-Z][^\n]*:|(?![\s\S]))`, "m").exec(section[1]);
|
|
346
|
-
if (!evidenceField)
|
|
347
|
-
fail(`${dependency} has no explicit authoritative evidence field`);
|
|
348
|
-
const paths = [...evidenceField[1].matchAll(/`([^`]+)`/g)].map((match) => match[1]);
|
|
349
|
-
if (paths.length === 0 || new Set(paths).size !== paths.length)
|
|
350
|
-
fail(`${dependency} authoritative evidence field must name distinct paths`);
|
|
351
|
-
if (!paths.some((candidate) => IMMUTABLE_EVIDENCE_PATH.test(candidate)))
|
|
352
|
-
fail(`${dependency} must declare at least one immutable evidence or evaluation path`);
|
|
353
|
-
evidencePaths.set(dependency, paths);
|
|
354
|
-
}
|
|
355
|
-
return evidencePaths;
|
|
356
|
-
}
|
|
357
337
|
function verifyFreshEvidenceDirectory(repositoryRoot, plan) {
|
|
358
338
|
const expected = `docs/evidence/releases/${plan.version}`;
|
|
359
339
|
if (plan.evidenceDirectory !== expected)
|
|
@@ -381,9 +361,8 @@ function verifyFreshEvidenceDirectory(repositoryRoot, plan) {
|
|
|
381
361
|
}
|
|
382
362
|
function verifyProductionTrust(plan, evidence) {
|
|
383
363
|
const paths = [plan.trustRootPath, plan.trustCeremonyManifestPath, plan.trustReviewReceiptPath];
|
|
384
|
-
if (new Set(paths).size !== paths.length
|
|
385
|
-
|
|
386
|
-
fail("production trust artifacts must be three distinct C62 dependency-evidence paths");
|
|
364
|
+
if (new Set(paths).size !== paths.length)
|
|
365
|
+
fail("production trust artifacts must be three distinct paths");
|
|
387
366
|
const rootBytes = evidence.get(plan.trustRootPath);
|
|
388
367
|
const manifestBytes = evidence.get(plan.trustCeremonyManifestPath);
|
|
389
368
|
const receiptBytes = evidence.get(plan.trustReviewReceiptPath);
|
|
@@ -416,7 +395,7 @@ function verifyProductionTrust(plan, evidence) {
|
|
|
416
395
|
fail(`production trust root must be a valid self-threshold-signed root envelope: ${error instanceof Error ? error.message : "invalid root"}`);
|
|
417
396
|
}
|
|
418
397
|
if (digest !== plan.trustRootSha256)
|
|
419
|
-
fail("trust-root digest does not match the canonical
|
|
398
|
+
fail("trust-root digest does not match the canonical root envelope");
|
|
420
399
|
const receipt = record(parseJson(receiptBytes, "production root review receipt"), "production root review receipt");
|
|
421
400
|
exactKeys(receipt, [
|
|
422
401
|
"schemaVersion",
|
|
@@ -434,14 +413,58 @@ function verifyProductionTrust(plan, evidence) {
|
|
|
434
413
|
receipt.ceremonyId !== manifest.ceremonyId ||
|
|
435
414
|
receipt.rootEnvelopeSha256 !== digest ||
|
|
436
415
|
typeof receipt.reviewerId !== "string" ||
|
|
437
|
-
manifest.custodians.some(({ id }) => id === receipt.reviewerId) ||
|
|
416
|
+
!manifest.custodians.some(({ id }) => id === receipt.reviewerId) ||
|
|
438
417
|
receipt.decision !== "approved" ||
|
|
439
|
-
receipt.authentication !== "
|
|
418
|
+
receipt.authentication !== "maintainer-attested" ||
|
|
440
419
|
receipt.productionActivation !== false ||
|
|
441
420
|
!Number.isFinite(reviewedTime.getTime()) ||
|
|
442
421
|
reviewedTime.toISOString() !== reviewedAt ||
|
|
443
422
|
reviewedTime.getTime() > Date.now())
|
|
444
|
-
fail("production root
|
|
423
|
+
fail("production root attestation is not maintainer-owned, timestamped, and digest-bound");
|
|
424
|
+
}
|
|
425
|
+
function verifyTechnicalEvidence(plan, evidence) {
|
|
426
|
+
const repository = record(parseJson(evidence.get(plan.technicalEvidence.repository) ?? Buffer.alloc(0), "repository technical evidence"), "repository technical evidence");
|
|
427
|
+
if (repository.schemaVersion !== 1 ||
|
|
428
|
+
repository.sourceCommit !== plan.sourceCommit ||
|
|
429
|
+
repository.passed !== true ||
|
|
430
|
+
repository.sonarQualityGate !== "passed")
|
|
431
|
+
fail("repository technical evidence must be source-bound with passing gates and Sonar");
|
|
432
|
+
const commandResults = repository.commands;
|
|
433
|
+
const expectedCommands = COMMAND_CATEGORIES.flatMap((category) => plan.verificationCommands[category].map((command) => ({ category, command })));
|
|
434
|
+
if (!Array.isArray(commandResults) ||
|
|
435
|
+
commandResults.length !== expectedCommands.length ||
|
|
436
|
+
expectedCommands.some((expected, index) => {
|
|
437
|
+
const actual = commandResults[index];
|
|
438
|
+
return (!actual ||
|
|
439
|
+
typeof actual !== "object" ||
|
|
440
|
+
actual.category !== expected.category ||
|
|
441
|
+
actual.command !== expected.command ||
|
|
442
|
+
actual.exitCode !== 0);
|
|
443
|
+
}))
|
|
444
|
+
fail("repository technical evidence must record every canonical command with exit code zero");
|
|
445
|
+
for (const declared of plan.technicalEvidence.platforms) {
|
|
446
|
+
const platform = record(parseJson(evidence.get(declared.path) ?? Buffer.alloc(0), `${declared.platform} technical evidence`), `${declared.platform} technical evidence`);
|
|
447
|
+
const runs = platform.runsMs;
|
|
448
|
+
const coverage = platform.coverage;
|
|
449
|
+
if (platform.schemaVersion !== 1 ||
|
|
450
|
+
platform.sourceCommit !== plan.sourceCommit ||
|
|
451
|
+
platform.platform !== declared.platform ||
|
|
452
|
+
platform.passed !== true ||
|
|
453
|
+
platform.fixtureVersion !== "2026-08-14.2" ||
|
|
454
|
+
platform.silentEmptyControl !== "failed-as-expected" ||
|
|
455
|
+
!Array.isArray(runs) ||
|
|
456
|
+
runs.length !== 5 ||
|
|
457
|
+
runs.some((run) => typeof run !== "number" || run < 0 || run > 60_000) ||
|
|
458
|
+
typeof platform.verifierP95Ms !== "number" ||
|
|
459
|
+
platform.verifierP95Ms < 0 ||
|
|
460
|
+
platform.verifierP95Ms > 20_000 ||
|
|
461
|
+
platform.verifierP95Ms !== Math.max(...runs) ||
|
|
462
|
+
platform.verifierHardLimitMs !== 60_000 ||
|
|
463
|
+
!coverage ||
|
|
464
|
+
typeof coverage !== "object" ||
|
|
465
|
+
VERIFIER_COVERAGE.some((language) => coverage[language] !== "passed"))
|
|
466
|
+
fail(`${declared.platform} technical evidence does not satisfy the verifier budget`);
|
|
467
|
+
}
|
|
445
468
|
}
|
|
446
469
|
export function evaluateReleasePreflight(options) {
|
|
447
470
|
const repositoryRoot = fs.realpathSync(options.repositoryRoot);
|
|
@@ -467,39 +490,28 @@ export function evaluateReleasePreflight(options) {
|
|
|
467
490
|
if (!git.isTracked(workflowFile.relativePath))
|
|
468
491
|
fail("C64 candidate workflow must be checked in");
|
|
469
492
|
requireTaggedBytes(git, plan.sourceCommit, workflowFile.relativePath, workflowFile.bytes, "C64 candidate workflow");
|
|
470
|
-
const roadmapFile = readBoundedRegularFile(repositoryRoot, path.join(repositoryRoot, "roadmap/competitive-roadmap.md"), "authoritative roadmap", MAX_EVIDENCE_BYTES);
|
|
471
|
-
if (!git.isTracked(roadmapFile.relativePath))
|
|
472
|
-
fail("authoritative roadmap must be checked in");
|
|
473
|
-
requireTaggedBytes(git, plan.sourceCommit, roadmapFile.relativePath, roadmapFile.bytes, "authoritative roadmap");
|
|
474
|
-
const roadmapEvidence = verifyRoadmap(roadmapFile.bytes.toString("utf8"));
|
|
475
493
|
let totalEvidenceBytes = 0;
|
|
476
|
-
const uniquePaths = new Set();
|
|
477
494
|
const evidenceBytes = new Map();
|
|
478
|
-
for (const
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
requireTaggedBytes(git, plan.sourceCommit, relativePath, evidence.bytes, `${dependency} evidence ${relativePath}`);
|
|
496
|
-
evidenceBytes.set(relativePath, evidence.bytes);
|
|
497
|
-
totalEvidenceBytes += evidence.size;
|
|
498
|
-
if (totalEvidenceBytes > MAX_TOTAL_EVIDENCE_BYTES)
|
|
499
|
-
fail("dependency evidence exceeds the aggregate byte budget");
|
|
500
|
-
}
|
|
495
|
+
for (const relativePath of [
|
|
496
|
+
plan.trustRootPath,
|
|
497
|
+
plan.trustCeremonyManifestPath,
|
|
498
|
+
plan.trustReviewReceiptPath,
|
|
499
|
+
plan.technicalEvidence.repository,
|
|
500
|
+
...plan.technicalEvidence.platforms.map(({ path: evidencePath }) => evidencePath),
|
|
501
|
+
]) {
|
|
502
|
+
const evidence = readBoundedRegularFile(repositoryRoot, path.join(repositoryRoot, relativePath), `production trust artifact ${relativePath}`, MAX_EVIDENCE_BYTES);
|
|
503
|
+
if (evidence.relativePath !== relativePath)
|
|
504
|
+
fail("production trust artifact path must be normalized");
|
|
505
|
+
if (!git.isTracked(relativePath))
|
|
506
|
+
fail(`production trust artifact must be checked in: ${relativePath}`);
|
|
507
|
+
requireTaggedBytes(git, plan.sourceCommit, relativePath, evidence.bytes, `production trust artifact ${relativePath}`);
|
|
508
|
+
evidenceBytes.set(relativePath, evidence.bytes);
|
|
509
|
+
totalEvidenceBytes += evidence.size;
|
|
510
|
+
if (totalEvidenceBytes > MAX_TOTAL_EVIDENCE_BYTES)
|
|
511
|
+
fail("production trust artifacts exceed the aggregate byte budget");
|
|
501
512
|
}
|
|
502
513
|
verifyProductionTrust(plan, evidenceBytes);
|
|
514
|
+
verifyTechnicalEvidence(plan, evidenceBytes);
|
|
503
515
|
verifyFreshEvidenceDirectory(repositoryRoot, plan);
|
|
504
516
|
return {
|
|
505
517
|
ok: true,
|
|
@@ -508,6 +520,5 @@ export function evaluateReleasePreflight(options) {
|
|
|
508
520
|
sourceCommit: plan.sourceCommit,
|
|
509
521
|
workflowCommit: plan.workflow.commit,
|
|
510
522
|
evidenceDirectory: plan.evidenceDirectory,
|
|
511
|
-
dependencies: [...RELEASE_PREFLIGHT_DEPENDENCIES],
|
|
512
523
|
};
|
|
513
524
|
}
|
package/dist/src/repair-lease.js
CHANGED
|
@@ -1,10 +1,24 @@
|
|
|
1
|
+
import child_process from "node:child_process";
|
|
1
2
|
import crypto from "node:crypto";
|
|
2
3
|
import fs from "node:fs";
|
|
3
4
|
import path from "node:path";
|
|
5
|
+
import { acquireLifecycleCoordination } from "./update-coordination.js";
|
|
4
6
|
const MAX_LEASE_BYTES = 4_096;
|
|
5
7
|
function leasePath(repo) {
|
|
6
8
|
return path.join(path.resolve(repo), ".knodin", "repair.lock");
|
|
7
9
|
}
|
|
10
|
+
function activityPath(repo) {
|
|
11
|
+
return path.join(path.resolve(repo), ".knodin", "lifecycle-activity.json");
|
|
12
|
+
}
|
|
13
|
+
function writeActivity(repo, record, finishedAt) {
|
|
14
|
+
const target = activityPath(repo);
|
|
15
|
+
const temporary = `${target}.${record.nonce}.tmp`;
|
|
16
|
+
fs.writeFileSync(temporary, `${JSON.stringify({ ...record, finishedAt: finishedAt ?? null })}\n`, {
|
|
17
|
+
encoding: "utf8",
|
|
18
|
+
mode: 0o600,
|
|
19
|
+
});
|
|
20
|
+
fs.renameSync(temporary, target);
|
|
21
|
+
}
|
|
8
22
|
function processIsAlive(pid) {
|
|
9
23
|
try {
|
|
10
24
|
process.kill(pid, 0);
|
|
@@ -14,17 +28,36 @@ function processIsAlive(pid) {
|
|
|
14
28
|
return false;
|
|
15
29
|
}
|
|
16
30
|
}
|
|
31
|
+
function processStartToken(pid) {
|
|
32
|
+
try {
|
|
33
|
+
return (child_process
|
|
34
|
+
.execFileSync("ps", ["-o", "lstart=", "-p", String(pid)], {
|
|
35
|
+
encoding: "utf8",
|
|
36
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
37
|
+
})
|
|
38
|
+
.trim() || null);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
17
44
|
function readLease(target) {
|
|
18
45
|
try {
|
|
19
46
|
const stat = fs.statSync(target);
|
|
20
47
|
if (!stat.isFile() || stat.size <= 0 || stat.size > MAX_LEASE_BYTES)
|
|
21
48
|
return null;
|
|
22
49
|
const value = JSON.parse(fs.readFileSync(target, "utf8"));
|
|
23
|
-
if (value.schemaVersion !==
|
|
50
|
+
if (value.schemaVersion !== 2 ||
|
|
24
51
|
!Number.isInteger(value.pid) ||
|
|
25
52
|
(value.pid ?? 0) <= 0 ||
|
|
26
53
|
typeof value.startedAt !== "string" ||
|
|
27
54
|
!Number.isFinite(Date.parse(value.startedAt)) ||
|
|
55
|
+
typeof value.processStartToken !== "string" ||
|
|
56
|
+
!value.processStartToken ||
|
|
57
|
+
typeof value.command !== "string" ||
|
|
58
|
+
!value.command ||
|
|
59
|
+
typeof value.repository !== "string" ||
|
|
60
|
+
!path.isAbsolute(value.repository) ||
|
|
28
61
|
typeof value.nonce !== "string" ||
|
|
29
62
|
!value.nonce)
|
|
30
63
|
return null;
|
|
@@ -43,7 +76,9 @@ function writeLease(target, record) {
|
|
|
43
76
|
}
|
|
44
77
|
function removeStaleLeaseOrThrow(target, operation) {
|
|
45
78
|
const current = readLease(target);
|
|
46
|
-
if (current &&
|
|
79
|
+
if (current &&
|
|
80
|
+
processIsAlive(current.pid) &&
|
|
81
|
+
processStartToken(current.pid) === current.processStartToken) {
|
|
47
82
|
const contention = operation === "repair"
|
|
48
83
|
? "another repair is already in progress"
|
|
49
84
|
: "another lifecycle mutation is already in progress";
|
|
@@ -61,28 +96,62 @@ function removeStaleLeaseOrThrow(target, operation) {
|
|
|
61
96
|
export function acquireRepairLease(repo, operation = "repair") {
|
|
62
97
|
const target = leasePath(repo);
|
|
63
98
|
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
99
|
+
const lifecycleCommand = [
|
|
100
|
+
"repair",
|
|
101
|
+
"init",
|
|
102
|
+
"configure",
|
|
103
|
+
"index",
|
|
104
|
+
"lifecycle-hook",
|
|
105
|
+
].includes(operation)
|
|
106
|
+
? operation
|
|
107
|
+
: "repair";
|
|
108
|
+
const coordination = acquireLifecycleCoordination({
|
|
109
|
+
command: lifecycleCommand,
|
|
110
|
+
repository: repo,
|
|
111
|
+
verificationToken: process.env.KNODIN_UPDATE_VERIFICATION_TOKEN,
|
|
112
|
+
});
|
|
64
113
|
const record = {
|
|
65
|
-
schemaVersion:
|
|
114
|
+
schemaVersion: 2,
|
|
66
115
|
pid: process.pid,
|
|
67
116
|
startedAt: new Date().toISOString(),
|
|
117
|
+
processStartToken: processStartToken(process.pid) ?? `node-start-${Date.now() - process.uptime() * 1000}`,
|
|
118
|
+
command: operation,
|
|
119
|
+
repository: path.resolve(repo),
|
|
68
120
|
nonce: crypto.randomUUID(),
|
|
69
121
|
};
|
|
70
122
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
71
123
|
try {
|
|
72
124
|
writeLease(target, record);
|
|
125
|
+
writeActivity(repo, record);
|
|
73
126
|
return {
|
|
74
127
|
release() {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
128
|
+
try {
|
|
129
|
+
const current = readLease(target);
|
|
130
|
+
if (current?.nonce === record.nonce) {
|
|
131
|
+
writeActivity(repo, record, new Date().toISOString());
|
|
132
|
+
fs.rmSync(target, { force: true });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
finally {
|
|
136
|
+
coordination.release();
|
|
137
|
+
}
|
|
78
138
|
},
|
|
79
139
|
};
|
|
80
140
|
}
|
|
81
141
|
catch (error) {
|
|
82
|
-
if (error.code !== "EEXIST")
|
|
142
|
+
if (error.code !== "EEXIST") {
|
|
143
|
+
coordination.release();
|
|
83
144
|
throw error;
|
|
84
|
-
|
|
145
|
+
}
|
|
146
|
+
try {
|
|
147
|
+
removeStaleLeaseOrThrow(target, operation);
|
|
148
|
+
}
|
|
149
|
+
catch (error_) {
|
|
150
|
+
coordination.release();
|
|
151
|
+
throw error_;
|
|
152
|
+
}
|
|
85
153
|
}
|
|
86
154
|
}
|
|
155
|
+
coordination.release();
|
|
87
156
|
throw new Error(`knodin ${operation}: could not acquire the repository lifecycle lease; retry shortly`);
|
|
88
157
|
}
|
|
@@ -42,9 +42,11 @@ const PROTECTED_CONTRACT_KEYS = new Set([
|
|
|
42
42
|
"operation",
|
|
43
43
|
"kind",
|
|
44
44
|
"name",
|
|
45
|
+
"client",
|
|
45
46
|
"target",
|
|
46
47
|
"to",
|
|
47
48
|
]);
|
|
49
|
+
const PROTECTED_CONTRACT_ARRAY_PATHS = new Set(["$/agents/clients"]);
|
|
48
50
|
function isTruncatableDetail(entry) {
|
|
49
51
|
return (!entry.path.startsWith("$/sharedIndex/") &&
|
|
50
52
|
(typeof entry.key !== "string" || !PROTECTED_CONTRACT_KEYS.has(entry.key)));
|
|
@@ -105,7 +107,9 @@ function truncatePayloadToBytes(root, byteLimit) {
|
|
|
105
107
|
}
|
|
106
108
|
while (bytes(root) > byteLimit) {
|
|
107
109
|
const candidate = collectArrays(root)
|
|
108
|
-
.filter((entry) => !entry.path.startsWith("$/responseBudget") &&
|
|
110
|
+
.filter((entry) => !entry.path.startsWith("$/responseBudget") &&
|
|
111
|
+
!PROTECTED_CONTRACT_ARRAY_PATHS.has(entry.path) &&
|
|
112
|
+
entry.value.length > 0)
|
|
109
113
|
.sort((a, b) => bytes(b.value) - bytes(a.value))[0];
|
|
110
114
|
if (!candidate)
|
|
111
115
|
break;
|
package/dist/src/server.js
CHANGED
|
@@ -23,6 +23,7 @@ const RETRY_SAFE_OPERATIONS = new Set([
|
|
|
23
23
|
"explain",
|
|
24
24
|
"map",
|
|
25
25
|
"query",
|
|
26
|
+
"query_batch",
|
|
26
27
|
"review",
|
|
27
28
|
"search",
|
|
28
29
|
"status",
|
|
@@ -69,7 +70,9 @@ export function recordMcpDiagnosticFailure(args, error, defaultRepo = process.cw
|
|
|
69
70
|
});
|
|
70
71
|
const message = error instanceof Error ? error.message : String(error);
|
|
71
72
|
return diagnostic.recorded
|
|
72
|
-
? new Error(`${message} [diagnostic ${diagnostic.correlationId}]`, {
|
|
73
|
+
? new Error(`${message} [diagnostic ${diagnostic.correlationId}]`, {
|
|
74
|
+
cause: error,
|
|
75
|
+
})
|
|
73
76
|
: error;
|
|
74
77
|
}
|
|
75
78
|
export function createServer(dispatch = handleKnodinTool) {
|
|
@@ -111,7 +114,11 @@ export function createServer(dispatch = handleKnodinTool) {
|
|
|
111
114
|
void extra
|
|
112
115
|
.sendNotification({
|
|
113
116
|
method: "notifications/progress",
|
|
114
|
-
params: {
|
|
117
|
+
params: {
|
|
118
|
+
progressToken,
|
|
119
|
+
progress: sequence,
|
|
120
|
+
message: phase,
|
|
121
|
+
},
|
|
115
122
|
})
|
|
116
123
|
.catch(ignoreNotificationFailure);
|
|
117
124
|
})
|
|
@@ -121,7 +128,9 @@ export function createServer(dispatch = handleKnodinTool) {
|
|
|
121
128
|
if (progressToken === undefined)
|
|
122
129
|
return;
|
|
123
130
|
progressSequence++;
|
|
124
|
-
recordMcpLifecycle(context, "progress", {
|
|
131
|
+
recordMcpLifecycle(context, "progress", {
|
|
132
|
+
sequence: progressSequence,
|
|
133
|
+
});
|
|
125
134
|
await extra.sendNotification({
|
|
126
135
|
method: "notifications/progress",
|
|
127
136
|
params: {
|
|
@@ -143,7 +152,9 @@ export function createServer(dispatch = handleKnodinTool) {
|
|
|
143
152
|
}, { cooperativeCancellation: operation === "repair" })),
|
|
144
153
|
};
|
|
145
154
|
const result = execution.result;
|
|
146
|
-
recordMcpLifecycle(context, "success", {
|
|
155
|
+
recordMcpLifecycle(context, "success", {
|
|
156
|
+
elapsedMs: Date.now() - startedAt,
|
|
157
|
+
});
|
|
147
158
|
return {
|
|
148
159
|
_meta: {
|
|
149
160
|
requestId: context.requestId,
|
|
@@ -192,7 +203,9 @@ export function createServer(dispatch = handleKnodinTool) {
|
|
|
192
203
|
finally {
|
|
193
204
|
if (heartbeat)
|
|
194
205
|
clearInterval(heartbeat);
|
|
195
|
-
recordMcpLifecycle(context, "cleanup", {
|
|
206
|
+
recordMcpLifecycle(context, "cleanup", {
|
|
207
|
+
elapsedMs: Date.now() - startedAt,
|
|
208
|
+
});
|
|
196
209
|
}
|
|
197
210
|
});
|
|
198
211
|
return server;
|
|
@@ -2,7 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { EMBEDDING_DIMENSIONS, EMBEDDING_MODEL_ID, EMBEDDING_MODEL_REVISION, } from "../engine/embeddings.js";
|
|
5
|
+
import { EMBEDDING_DIMENSIONS, EMBEDDING_MODEL_DTYPE, EMBEDDING_MODEL_ID, EMBEDDING_MODEL_REVISION, } from "../engine/embeddings.js";
|
|
6
6
|
import { KNODIN_SCHEMA_VERSION } from "../engine/index.js";
|
|
7
7
|
import { prunePolicyDescriptor } from "../engine/prune.js";
|
|
8
8
|
import { KNODIN_VERSION } from "../version.js";
|
|
@@ -44,6 +44,7 @@ export function currentSharedIndexCompatibility() {
|
|
|
44
44
|
id: EMBEDDING_MODEL_ID,
|
|
45
45
|
revision: EMBEDDING_MODEL_REVISION,
|
|
46
46
|
dimensions: EMBEDDING_DIMENSIONS,
|
|
47
|
+
dtype: EMBEDDING_MODEL_DTYPE,
|
|
47
48
|
runtime: `@huggingface/transformers@${packageVersion("@huggingface/transformers")}`,
|
|
48
49
|
};
|
|
49
50
|
const parsers = {
|
|
@@ -61,9 +62,10 @@ export function currentSharedIndexCompatibility() {
|
|
|
61
62
|
modelDigest: digest(model),
|
|
62
63
|
parserBundleDigest: digest(parsers),
|
|
63
64
|
indexConfigDigest: digest({
|
|
64
|
-
policyVersion:
|
|
65
|
+
policyVersion: 2,
|
|
65
66
|
databaseSchemaVersion: KNODIN_SCHEMA_VERSION,
|
|
66
67
|
embeddingDimensions: EMBEDDING_DIMENSIONS,
|
|
68
|
+
embeddingDtype: EMBEDDING_MODEL_DTYPE,
|
|
67
69
|
}),
|
|
68
70
|
prunePolicyDigest: digest(prunePolicyDescriptor()),
|
|
69
71
|
};
|