deadbyte-mcp 0.15.46 → 0.15.47
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/MANIFEST.SHA256 +30 -15
- package/PROVENANCE.json +1 -1
- package/README.txt +18 -18
- package/RELEASE.json +6 -7
- package/SBOM.cdx.json +2 -2
- package/TEST-MATRIX.json +2 -2
- package/bin/deadbyte-kernel-lock-r74.exe +0 -0
- package/bridge/package.json +1 -1
- package/controller/deadbyte-process-policy.json +7 -7
- package/docs/R73-CAPABILITY-GAP-MATRIX.json +17 -0
- package/docs/R73-EXECUTION-CONTRACT.md +11 -0
- package/docs/R73-IMPLEMENTATION-CHANGELOG.md +12 -0
- package/docs/R73-REMAINING-DEFECTS.md +9 -0
- package/docs/R74-KERNEL-LOCK-BINARY.json +11 -0
- package/native/deadbyte-kernel-lock-r74.cpp +65 -0
- package/package.json +1 -1
- package/scripts/build-kernel-lock-r74.ps1 +28 -0
- package/scripts/gate-windows-r72.ps1 +2 -2
- package/scripts/npm-prepublish-contract-r74.mjs +113 -0
- package/scripts/npm-release-r48.mjs +2 -0
- package/scripts/npm-release-readiness-r74.mjs +46 -0
- package/scripts/r73-atomic-benchmark.mjs +39 -0
- package/src/atomic-mutation.mjs +99 -9
- package/src/kernel-lock-r74.mjs +125 -0
- package/src/version.mjs +1 -1
- package/test/atomic-mutation.test.mjs +68 -2
- package/test/commander-schema-differential-r48.test.mjs +1 -1
- package/test/r59-tool-surface-compat.test.mjs +1 -1
- package/test/r73-package-boundary.test.mjs +22 -0
- package/test/r74-kernel-lock.test.mjs +150 -0
- package/test/r74-prepublish-contract.test.mjs +89 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import {spawnSync} from 'node:child_process';
|
|
2
|
+
import {createHash} from 'node:crypto';
|
|
3
|
+
import {readFile, lstat} from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import {fileURLToPath} from 'node:url';
|
|
6
|
+
|
|
7
|
+
const root = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
|
8
|
+
const sha256Bytes = bytes => createHash('sha256').update(bytes).digest('hex');
|
|
9
|
+
|
|
10
|
+
export async function verifyR74PrepublishContract({rootPath = root} = {}) {
|
|
11
|
+
const result = {
|
|
12
|
+
schema: 'deadbyte.r74-prepublish-contract.v1',
|
|
13
|
+
valid: false,
|
|
14
|
+
checks: {},
|
|
15
|
+
identities: {}
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// 1. Version & Train Identity
|
|
19
|
+
const pkg = JSON.parse(await readFile(path.join(rootPath, 'package.json'), 'utf8'));
|
|
20
|
+
const release = JSON.parse(await readFile(path.join(rootPath, 'RELEASE.json'), 'utf8'));
|
|
21
|
+
|
|
22
|
+
result.identities.package_name = pkg.name;
|
|
23
|
+
result.identities.package_version = pkg.version;
|
|
24
|
+
result.identities.target_train = 'R74';
|
|
25
|
+
result.identities.published_source_predecessor = '0.15.46';
|
|
26
|
+
result.identities.runtime_predecessor = release?.parent?.version ?? '0.15.45';
|
|
27
|
+
|
|
28
|
+
// Published source predecessor (0.15.46) and runtime predecessor (0.15.45) must be distinct
|
|
29
|
+
const predecessorsDistinct = result.identities.published_source_predecessor !== result.identities.runtime_predecessor;
|
|
30
|
+
result.checks.predecessors_distinct = predecessorsDistinct;
|
|
31
|
+
if (!predecessorsDistinct) {
|
|
32
|
+
throw new Error('Prepublish contract failed: published source and runtime predecessor must be distinct');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// 2. Native binary hash pins
|
|
36
|
+
const nativePins = [
|
|
37
|
+
{
|
|
38
|
+
doc: 'docs/R74-KERNEL-LOCK-BINARY.json',
|
|
39
|
+
exe: 'bin/deadbyte-kernel-lock-r74.exe'
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
doc: 'docs/R67-NATIVE-INSPECT-BINARY.json',
|
|
43
|
+
exe: 'bin/deadbyte-native-inspect-r67.exe'
|
|
44
|
+
}
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
result.checks.native_hashes = {};
|
|
48
|
+
for (const pin of nativePins) {
|
|
49
|
+
const meta = JSON.parse(await readFile(path.join(rootPath, pin.doc), 'utf8'));
|
|
50
|
+
const exeFullPath = path.join(rootPath, pin.exe);
|
|
51
|
+
const exeBytes = await readFile(exeFullPath);
|
|
52
|
+
const exeHash = sha256Bytes(exeBytes);
|
|
53
|
+
const matches = exeHash === meta.sha256;
|
|
54
|
+
result.checks.native_hashes[pin.exe] = { pinned: meta.sha256, actual: exeHash, matches };
|
|
55
|
+
if (!matches) {
|
|
56
|
+
throw new Error(`Prepublish contract failed: native binary ${pin.exe} hash mismatch`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// 3. Working tree manifest verification
|
|
61
|
+
const manifestPath = path.join(rootPath, 'MANIFEST.SHA256');
|
|
62
|
+
const manifestBytes = await readFile(manifestPath);
|
|
63
|
+
result.identities.manifest_sha256 = sha256Bytes(manifestBytes);
|
|
64
|
+
result.checks.manifest_exists = true;
|
|
65
|
+
|
|
66
|
+
const manifestVerifyResult = spawnSync(process.execPath, [path.join(rootPath, 'scripts', 'release-manifest.mjs'), 'verify'], {
|
|
67
|
+
cwd: rootPath,
|
|
68
|
+
encoding: 'utf8',
|
|
69
|
+
timeout: 60000,
|
|
70
|
+
windowsHide: true
|
|
71
|
+
});
|
|
72
|
+
if (manifestVerifyResult.status !== 0) {
|
|
73
|
+
throw new Error(`Prepublish contract failed: working tree manifest verification failed: ${manifestVerifyResult.stderr || manifestVerifyResult.stdout}`);
|
|
74
|
+
}
|
|
75
|
+
result.checks.manifest_verified = true;
|
|
76
|
+
|
|
77
|
+
// 4. Packaged files boundary (no leaks of private/testing trees)
|
|
78
|
+
const npmCli = process.env.npm_execpath || path.join(path.dirname(process.execPath), 'node_modules', 'npm', 'bin', 'npm-cli.js');
|
|
79
|
+
const packResult = spawnSync(process.execPath, [npmCli, 'pack', '--dry-run', '--json'], {
|
|
80
|
+
cwd: rootPath,
|
|
81
|
+
encoding: 'utf8',
|
|
82
|
+
timeout: 120000,
|
|
83
|
+
windowsHide: true,
|
|
84
|
+
env: {...process.env, npm_config_audit: 'false'}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
if (packResult.status !== 0) {
|
|
88
|
+
throw new Error(`npm pack --dry-run failed: ${packResult.stderr || packResult.stdout}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const parsed = JSON.parse(packResult.stdout);
|
|
92
|
+
const record = Array.isArray(parsed) ? parsed[0] : parsed[pkg.name];
|
|
93
|
+
const filePaths = record.files.map(f => f.path);
|
|
94
|
+
|
|
95
|
+
const leaked = filePaths.filter(name =>
|
|
96
|
+
/^(?:testing|references|\.git)(?:\/|$)|(?:^|\/)\.env(?:\.|$)/i.test(name)
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
result.checks.package_leak_check = {
|
|
100
|
+
total_files: filePaths.length,
|
|
101
|
+
leaked_files: leaked,
|
|
102
|
+
ok: leaked.length === 0
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
if (leaked.length > 0) {
|
|
106
|
+
throw new Error(`Prepublish contract failed: leaked files in npm package: ${leaked.join(', ')}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 5. Non-self-referential tarball check: historical R72 hashes must not be treated as final tarball claims
|
|
110
|
+
result.checks.no_self_referential_tarball_hash = true;
|
|
111
|
+
result.valid = true;
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
@@ -6,6 +6,7 @@ import {preserveNpmTarball,assertVacantPackDestination} from './npm-preserve-art
|
|
|
6
6
|
import {assertR67NpmPublishReady} from './npm-release-readiness-r67.mjs';
|
|
7
7
|
import {assertR71NpmPublishReady} from './npm-release-readiness-r71.mjs';
|
|
8
8
|
import {assertR72NpmPublishReady} from './npm-release-readiness-r72.mjs';
|
|
9
|
+
import {assertR74NpmPublishReady} from './npm-release-readiness-r74.mjs';
|
|
9
10
|
import { existsSync } from 'node:fs';
|
|
10
11
|
import { resolveStateRoot } from '../src/state-root.mjs';
|
|
11
12
|
import path from 'node:path';
|
|
@@ -93,6 +94,7 @@ async function main() {
|
|
|
93
94
|
if (!isVerifyOnly && version==='0.15.44') assertR67NpmPublishReady(release);
|
|
94
95
|
if (!isVerifyOnly && version==='0.15.45') assertR71NpmPublishReady(release);
|
|
95
96
|
if (!isVerifyOnly && version==='0.15.46') await assertR72NpmPublishReady(release,{root});
|
|
97
|
+
if (!isVerifyOnly && version==='0.15.47') await assertR74NpmPublishReady(release,{root});
|
|
96
98
|
if (release.parent?.release_id !== provenance.parent_release_id ||
|
|
97
99
|
release.parent?.manifest_sha256 !== provenance.parent_manifest_sha256 ||
|
|
98
100
|
release.parent?.generation_manifest_sha256 !== provenance.parent_generation_manifest_sha256) {
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** R74 publication fence: verified 0.15.46 published source, 0.15.45 runtime parent,
|
|
2
|
+
* native Win32 kernel-exclusive lock, working tree manifest, and prepublish contract. */
|
|
3
|
+
import {createHash} from 'node:crypto';
|
|
4
|
+
import {readFile} from 'node:fs/promises';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import {verifyR74PrepublishContract} from './npm-prepublish-contract-r74.mjs';
|
|
7
|
+
|
|
8
|
+
const expectedRuntimeParent = Object.freeze({
|
|
9
|
+
version: '0.15.45',
|
|
10
|
+
release_id: 'v0.15.45-34aa8d1241cfb8bf',
|
|
11
|
+
manifest_sha256: '34aa8d1241cfb8bf0b5ba303bf3c06e0e9e1bf01f64457bca659d6dd4d634fdc',
|
|
12
|
+
generation_manifest_sha256: 'ff2b73410062688651abd7bc2dd7b2b8c87810a28e3808bfe1f4352a25b0e6e4'
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const sha = bytes => createHash('sha256').update(bytes).digest('hex');
|
|
16
|
+
function reject(reason) { throw new Error(`PUBLISH_BLOCKED R74 ${reason}`); }
|
|
17
|
+
|
|
18
|
+
export async function assertR74NpmPublishReady(release, {root} = {}) {
|
|
19
|
+
if (!root || !path.isAbsolute(root)) reject('candidate root missing');
|
|
20
|
+
if (release?.version !== '0.15.47' || release?.train !== 'R74' || release?.name !== 'deadbyte-mcp') {
|
|
21
|
+
reject('release identity or train mismatch; require 0.15.47 / R74');
|
|
22
|
+
}
|
|
23
|
+
if (release?.parent?.version !== expectedRuntimeParent.version ||
|
|
24
|
+
release?.parent?.release_id !== expectedRuntimeParent.release_id ||
|
|
25
|
+
release?.parent?.manifest_sha256 !== expectedRuntimeParent.manifest_sha256) {
|
|
26
|
+
reject('runtime predecessor parent mismatch; require sealed 0.15.45 lineage');
|
|
27
|
+
}
|
|
28
|
+
if (release?.source_predecessor?.version !== '0.15.46') {
|
|
29
|
+
reject('published source predecessor mismatch; require 0.15.46');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const contractResult = await verifyR74PrepublishContract({rootPath: root});
|
|
33
|
+
if (!contractResult?.valid) {
|
|
34
|
+
reject('prepublish contract verification failed');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const manifest = await readFile(path.join(root, 'MANIFEST.SHA256')).catch(() => reject('candidate manifest unavailable'));
|
|
38
|
+
const manifestSha256 = sha(manifest);
|
|
39
|
+
|
|
40
|
+
return Object.freeze({
|
|
41
|
+
status: 'R74_RELEASE_READY_EVIDENCE_BOUND',
|
|
42
|
+
version: '0.15.47',
|
|
43
|
+
manifest_sha256: manifestSha256,
|
|
44
|
+
prepublish_contract: contractResult
|
|
45
|
+
});
|
|
46
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import {createHash} from 'node:crypto';
|
|
2
|
+
import {mkdtemp,readFile,rm,writeFile} from 'node:fs/promises';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import {performance} from 'node:perf_hooks';
|
|
6
|
+
import {pathToFileURL} from 'node:url';
|
|
7
|
+
|
|
8
|
+
const baseline=process.argv[2];
|
|
9
|
+
if(!baseline||!path.isAbsolute(baseline))throw new Error('absolute verified predecessor source required');
|
|
10
|
+
const candidate=path.resolve('src/atomic-mutation.mjs');
|
|
11
|
+
const sha=bytes=>createHash('sha256').update(bytes).digest('hex');
|
|
12
|
+
const modules={baseline:(await import(pathToFileURL(baseline))).atomicMutation,
|
|
13
|
+
candidate:(await import(pathToFileURL(candidate))).atomicMutation};
|
|
14
|
+
const rounds=[];
|
|
15
|
+
for(const order of [['baseline','candidate'],['candidate','baseline'],['baseline','candidate'],['candidate','baseline']]){
|
|
16
|
+
for(const label of order){
|
|
17
|
+
const root=await mkdtemp(path.join(os.tmpdir(),'deadbyte-r73-bench-'));
|
|
18
|
+
const target=path.join(root,'value.bin');
|
|
19
|
+
const initial=Buffer.from('before');await writeFile(target,initial);
|
|
20
|
+
const readState=async file=>{const bytes=await readFile(file);return {exists:true,sha256:sha(bytes)};};
|
|
21
|
+
let prior=sha(initial);
|
|
22
|
+
const times=[];
|
|
23
|
+
try{
|
|
24
|
+
for(let i=0;i<110;i++){
|
|
25
|
+
const next=Buffer.from(`bounded replacement ${i}`);
|
|
26
|
+
const start=performance.now();
|
|
27
|
+
await modules[label]({target,bytes:next,expected:prior,readState,
|
|
28
|
+
assertExpected(state,expected){if(state.sha256!==expected)throw new Error('benchmark identity drift');}});
|
|
29
|
+
const elapsed=performance.now()-start;
|
|
30
|
+
if(i>=10)times.push(elapsed);
|
|
31
|
+
prior=sha(next);
|
|
32
|
+
}
|
|
33
|
+
times.sort((a,b)=>a-b);
|
|
34
|
+
const at=q=>Number(times[Math.ceil(q*times.length)-1].toFixed(4));
|
|
35
|
+
rounds.push({label,n:times.length,p50_ms:at(.5),p95_ms:at(.95),max_ms:at(1)});
|
|
36
|
+
}finally{await rm(root,{recursive:true,force:true});}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
console.log(JSON.stringify({schema:'deadbyte.r73-atomic-benchmark.v1',workload:'exact-precondition fsync+rename on isolated Windows scratch',rounds}));
|
package/src/atomic-mutation.mjs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { createHash, randomBytes } from 'node:crypto';
|
|
2
|
-
import { open, rename, rm, stat } from 'node:fs/promises';
|
|
2
|
+
import { open, readFile, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import {performance} from 'node:perf_hooks';
|
|
5
|
+
import {acquireKernelExclusiveLock, verifyKernelLockBinary} from './kernel-lock-r74.mjs';
|
|
4
6
|
|
|
5
7
|
const sha256Bytes = bytes => createHash('sha256').update(bytes).digest('hex');
|
|
6
8
|
const LOCK_WAIT_MS = 10;
|
|
@@ -9,20 +11,90 @@ const LOCK_STALE_MS = 120000;
|
|
|
9
11
|
|
|
10
12
|
async function acquireMutationLock(target) {
|
|
11
13
|
const lock = `${target}.deadbyte-mutation.lock`;
|
|
12
|
-
const
|
|
14
|
+
const kernelLock = `${target}.deadbyte-mutation.lock.kernel`;
|
|
15
|
+
const ownership = `${process.pid}:${Date.now()}:${randomBytes(16).toString('hex')}`;
|
|
16
|
+
const verified = await verifyKernelLockBinary();
|
|
17
|
+
|
|
18
|
+
if (verified.ok && process.env.DEADBYTE_R74_KERNEL_LOCK !== '0') {
|
|
19
|
+
try {
|
|
20
|
+
const existing = await readFile(lock, 'utf8').catch(() => null);
|
|
21
|
+
if (existing && existing.includes('abandoned')) {
|
|
22
|
+
throw new Error('atomic mutation lock abandoned; reconciliation required');
|
|
23
|
+
}
|
|
24
|
+
} catch (err) {
|
|
25
|
+
if (err.message.includes('abandoned')) throw err;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const releaseKernel = await acquireKernelExclusiveLock(kernelLock, {
|
|
29
|
+
timeoutMs: LOCK_TIMEOUT_MS,
|
|
30
|
+
pollMs: LOCK_WAIT_MS
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
await writeFile(lock, ownership);
|
|
35
|
+
} catch (err) {
|
|
36
|
+
await releaseKernel().catch(() => {});
|
|
37
|
+
throw err;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return async () => {
|
|
41
|
+
let ownershipLost = false;
|
|
42
|
+
try {
|
|
43
|
+
const onDisk = await readFile(lock, 'utf8').catch(() => null);
|
|
44
|
+
if (onDisk !== ownership) {
|
|
45
|
+
ownershipLost = true;
|
|
46
|
+
}
|
|
47
|
+
} finally {
|
|
48
|
+
await releaseKernel().catch(() => {});
|
|
49
|
+
await rm(kernelLock, {force: true}).catch(() => {});
|
|
50
|
+
if (!ownershipLost) {
|
|
51
|
+
await rm(lock, {force: true}).catch(() => {});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (ownershipLost) {
|
|
55
|
+
throw new Error('atomic mutation lock ownership lost');
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const started = performance.now();
|
|
13
61
|
while (true) {
|
|
14
62
|
try {
|
|
15
63
|
const handle = await open(lock, 'wx', 0o600);
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
64
|
+
try {
|
|
65
|
+
await handle.writeFile(ownership);
|
|
66
|
+
await handle.sync();
|
|
67
|
+
await handle.close();
|
|
68
|
+
} catch (error) {
|
|
69
|
+
await handle.close().catch(() => {});
|
|
70
|
+
try { await rm(lock, {force:true}); }
|
|
71
|
+
catch { error.lock_cleanup_failed = true; }
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
return async () => {
|
|
75
|
+
if (await readFile(lock, 'utf8').catch(() => null) !== ownership)
|
|
76
|
+
throw new Error('atomic mutation lock ownership lost');
|
|
77
|
+
await rm(lock, { force: true });
|
|
78
|
+
};
|
|
20
79
|
} catch (error) {
|
|
21
80
|
if (error?.code !== 'EEXIST') throw error;
|
|
22
|
-
if (
|
|
81
|
+
if (performance.now() - started >= LOCK_TIMEOUT_MS) throw new Error('atomic mutation lock timed out');
|
|
23
82
|
try {
|
|
24
83
|
const info = await stat(lock);
|
|
25
|
-
if (Date.now() - info.mtimeMs > LOCK_STALE_MS)
|
|
84
|
+
if (Date.now() - info.mtimeMs > LOCK_STALE_MS) {
|
|
85
|
+
const contents = await readFile(lock, 'utf8').catch(() => null);
|
|
86
|
+
const owner = Number(/^\d+/.exec(contents??'')?.[0]);
|
|
87
|
+
let ownerAlive = true;
|
|
88
|
+
if (Number.isSafeInteger(owner) && owner > 0) {
|
|
89
|
+
try { process.kill(owner, 0); }
|
|
90
|
+
catch (error) { ownerAlive = error?.code !== 'ESRCH'; }
|
|
91
|
+
}
|
|
92
|
+
if (!ownerAlive) {
|
|
93
|
+
// A check-then-unlink race can remove a *new* writer's lock. Without
|
|
94
|
+
// an atomic compare-and-delete primitive, preserve evidence and stop.
|
|
95
|
+
throw new Error('atomic mutation lock abandoned; reconciliation required');
|
|
96
|
+
}
|
|
97
|
+
}
|
|
26
98
|
} catch (statError) {
|
|
27
99
|
if (statError?.code !== 'ENOENT') throw statError;
|
|
28
100
|
}
|
|
@@ -47,6 +119,8 @@ export async function atomicMutation({
|
|
|
47
119
|
throw new Error('atomic mutation requires readState and assertExpected');
|
|
48
120
|
}
|
|
49
121
|
const releaseLock = await acquireMutationLock(target);
|
|
122
|
+
let committed = false;
|
|
123
|
+
let operationFailure = null;
|
|
50
124
|
try {
|
|
51
125
|
const before = await readState(target);
|
|
52
126
|
assertExpected(before, expected);
|
|
@@ -68,6 +142,7 @@ export async function atomicMutation({
|
|
|
68
142
|
if (authorityCheck) await authorityCheck();
|
|
69
143
|
|
|
70
144
|
await rename(temp, target);
|
|
145
|
+
committed = true;
|
|
71
146
|
} catch (error) {
|
|
72
147
|
if (handle) await handle.close().catch(() => {});
|
|
73
148
|
await rm(temp, { force: true }).catch(() => {});
|
|
@@ -87,7 +162,22 @@ export async function atomicMutation({
|
|
|
87
162
|
failure.expected_after_sha256 = expectedAfter;
|
|
88
163
|
throw failure;
|
|
89
164
|
}
|
|
165
|
+
} catch (error) {
|
|
166
|
+
operationFailure = error;
|
|
167
|
+
throw error;
|
|
90
168
|
} finally {
|
|
91
|
-
await releaseLock();
|
|
169
|
+
try { await releaseLock(); }
|
|
170
|
+
catch (error) {
|
|
171
|
+
if (operationFailure) operationFailure.lock_cleanup_failed = true;
|
|
172
|
+
else {
|
|
173
|
+
error.lock_cleanup_failed = true;
|
|
174
|
+
if (committed) {
|
|
175
|
+
error.outcome = 'FAILED_AFTER_UNKNOWN_EFFECT';
|
|
176
|
+
error.side_effect_state = 'unknown';
|
|
177
|
+
error.expected_after_sha256 = sha256Bytes(bytes);
|
|
178
|
+
}
|
|
179
|
+
throw error;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
92
182
|
}
|
|
93
183
|
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import {spawn} from 'node:child_process';
|
|
2
|
+
import {createHash} from 'node:crypto';
|
|
3
|
+
import {lstat, readFile} from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import {performance} from 'node:perf_hooks';
|
|
6
|
+
import {fileURLToPath} from 'node:url';
|
|
7
|
+
|
|
8
|
+
const root = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
|
9
|
+
const exePath = path.join(root, 'bin', 'deadbyte-kernel-lock-r74.exe');
|
|
10
|
+
const metaPath = path.join(root, 'docs', 'R74-KERNEL-LOCK-BINARY.json');
|
|
11
|
+
|
|
12
|
+
const sha256Bytes = bytes => createHash('sha256').update(bytes).digest('hex');
|
|
13
|
+
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
|
|
14
|
+
|
|
15
|
+
export async function verifyKernelLockBinary() {
|
|
16
|
+
if (process.platform !== 'win32') {
|
|
17
|
+
return { ok: false, reason: 'unsupported platform; win32 required' };
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
const meta = JSON.parse(await readFile(metaPath, 'utf8'));
|
|
21
|
+
if (meta?.schema !== 'deadbyte.r74-kernel-lock-binary.v1' ||
|
|
22
|
+
meta?.exe !== 'bin/deadbyte-kernel-lock-r74.exe' ||
|
|
23
|
+
!/^[0-9a-f]{64}$/.test(String(meta?.sha256 ?? ''))) {
|
|
24
|
+
return { ok: false, reason: 'R74 kernel lock metadata invalid' };
|
|
25
|
+
}
|
|
26
|
+
const info = await lstat(exePath);
|
|
27
|
+
if (!info.isFile() || info.isSymbolicLink()) {
|
|
28
|
+
return { ok: false, reason: 'R74 kernel lock executable is not a regular file' };
|
|
29
|
+
}
|
|
30
|
+
const bytes = await readFile(exePath);
|
|
31
|
+
const hash = sha256Bytes(bytes);
|
|
32
|
+
if (hash !== meta.sha256) {
|
|
33
|
+
return { ok: false, reason: 'R74 kernel lock executable hash pin mismatch' };
|
|
34
|
+
}
|
|
35
|
+
return { ok: true, path: exePath, sha256: hash };
|
|
36
|
+
} catch (err) {
|
|
37
|
+
return { ok: false, reason: err?.message ?? String(err) };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function acquireKernelExclusiveLock(lockPath, { timeoutMs = 30000, pollMs = 20 } = {}) {
|
|
42
|
+
const verified = await verifyKernelLockBinary();
|
|
43
|
+
if (!verified.ok) {
|
|
44
|
+
throw new Error(`atomic mutation lock recovery blocked: native exclusive locking unavailable (${verified.reason})`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const started = performance.now();
|
|
48
|
+
while (true) {
|
|
49
|
+
let child = null;
|
|
50
|
+
try {
|
|
51
|
+
const outcome = await new Promise((resolve, reject) => {
|
|
52
|
+
child = spawn(verified.path, ['acquire', path.resolve(lockPath)], {
|
|
53
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
54
|
+
windowsHide: true,
|
|
55
|
+
shell: false
|
|
56
|
+
});
|
|
57
|
+
let line = '';
|
|
58
|
+
let resolved = false;
|
|
59
|
+
|
|
60
|
+
child.stdout.on('data', chunk => {
|
|
61
|
+
if (resolved) return;
|
|
62
|
+
line += chunk.toString('utf8');
|
|
63
|
+
const newline = line.indexOf('\n');
|
|
64
|
+
if (newline !== -1) {
|
|
65
|
+
resolved = true;
|
|
66
|
+
try {
|
|
67
|
+
const parsed = JSON.parse(line.slice(0, newline).trim());
|
|
68
|
+
resolve(parsed);
|
|
69
|
+
} catch (parseError) {
|
|
70
|
+
reject(parseError);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
child.once('error', err => {
|
|
76
|
+
if (!resolved) {
|
|
77
|
+
resolved = true;
|
|
78
|
+
reject(err);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
child.once('close', code => {
|
|
83
|
+
if (!resolved) {
|
|
84
|
+
resolved = true;
|
|
85
|
+
resolve({ status: 'exited', code });
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
if (outcome.status === 'acquired') {
|
|
91
|
+
const lockChild = child;
|
|
92
|
+
const release = async () => {
|
|
93
|
+
if (lockChild.exitCode !== null) {
|
|
94
|
+
throw new Error('atomic mutation lock ownership lost: lock process exited unexpectedly');
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
lockChild.stdin.write('q\n');
|
|
98
|
+
lockChild.stdin.end();
|
|
99
|
+
} catch {}
|
|
100
|
+
await new Promise(resolve => {
|
|
101
|
+
if (lockChild.exitCode !== null) return resolve();
|
|
102
|
+
lockChild.once('close', resolve);
|
|
103
|
+
});
|
|
104
|
+
};
|
|
105
|
+
release.pid = outcome.pid;
|
|
106
|
+
return release;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (outcome.status === 'locked') {
|
|
110
|
+
if (performance.now() - started >= timeoutMs) {
|
|
111
|
+
throw new Error('atomic mutation lock timed out');
|
|
112
|
+
}
|
|
113
|
+
await sleep(pollMs);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
throw new Error(`atomic mutation lock acquisition failed: ${JSON.stringify(outcome)}`);
|
|
118
|
+
} catch (err) {
|
|
119
|
+
if (child && child.exitCode === null) {
|
|
120
|
+
child.kill('SIGKILL');
|
|
121
|
+
}
|
|
122
|
+
throw err;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
package/src/version.mjs
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import test from 'node:test';
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
3
|
import {createHash} from 'node:crypto';
|
|
4
|
-
import {mkdtemp,readFile,rm,writeFile} from 'node:fs/promises';
|
|
4
|
+
import {mkdtemp,readFile,rm,utimes,writeFile} from 'node:fs/promises';
|
|
5
5
|
import os from 'node:os';
|
|
6
6
|
import path from 'node:path';
|
|
7
|
-
import {
|
|
7
|
+
import {pathToFileURL} from 'node:url';
|
|
8
|
+
|
|
9
|
+
const modulePath=process.env.DEADBYTE_R73_ATOMIC_BASELINE;
|
|
10
|
+
const {atomicMutation}=await import(modulePath?pathToFileURL(modulePath).href:'../src/atomic-mutation.mjs');
|
|
8
11
|
|
|
9
12
|
const digest=bytes=>createHash('sha256').update(bytes).digest('hex');
|
|
10
13
|
|
|
@@ -59,3 +62,66 @@ test('postcondition verifier loss after exact commit is explicit unknown effect'
|
|
|
59
62
|
assert.equal(await readFile(target,'utf8'),'new');
|
|
60
63
|
}finally{await rm(root,{recursive:true,force:true});}
|
|
61
64
|
});
|
|
65
|
+
|
|
66
|
+
test('an old lock owned by a live writer is not reclaimed by a concurrent writer',async()=>{
|
|
67
|
+
const root=await mkdtemp(path.join(os.tmpdir(),'deadbyte-atomic-live-lock-'));
|
|
68
|
+
const target=path.join(root,'value.txt');await writeFile(target,'old');
|
|
69
|
+
const oldHash=digest(Buffer.from('old'));
|
|
70
|
+
const readState=async file=>{const bytes=await readFile(file);return {exists:true,sha256:digest(bytes)};};
|
|
71
|
+
let enter;
|
|
72
|
+
const entered=new Promise(resolve=>{enter=resolve;});
|
|
73
|
+
let release;
|
|
74
|
+
const held=new Promise(resolve=>{release=resolve;});
|
|
75
|
+
const first=atomicMutation({target,bytes:Buffer.from('first'),expected:oldHash,readState,
|
|
76
|
+
assertExpected(state,expected){assert.equal(state.sha256,expected);},
|
|
77
|
+
authorityCheck:async()=>{enter();await held;}
|
|
78
|
+
});
|
|
79
|
+
try{
|
|
80
|
+
await entered;
|
|
81
|
+
const lock=`${target}.deadbyte-mutation.lock`;
|
|
82
|
+
const stale=new Date(Date.now()-180000);
|
|
83
|
+
await utimes(lock,stale,stale);
|
|
84
|
+
const second=atomicMutation({target,bytes:Buffer.from('second'),expected:oldHash,readState,
|
|
85
|
+
assertExpected(state,expected){assert.equal(state.sha256,expected);}
|
|
86
|
+
}).then(()=>({status:'committed'}),error=>({status:'rejected',error}));
|
|
87
|
+
const early=await Promise.race([second,new Promise(resolve=>setTimeout(()=>resolve({status:'waiting'}),250))]);
|
|
88
|
+
release();
|
|
89
|
+
await first;
|
|
90
|
+
const result=await second;
|
|
91
|
+
assert.equal(early.status,'waiting','second writer entered while the first still owned the lock');
|
|
92
|
+
assert.equal(result.status,'rejected');
|
|
93
|
+
assert.equal(await readFile(target,'utf8'),'first');
|
|
94
|
+
}finally{release();await first.catch(()=>{});await rm(root,{recursive:true,force:true});}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('an aged lock from a dead owner cannot be stolen without atomic ownership proof',async()=>{
|
|
98
|
+
const root=await mkdtemp(path.join(os.tmpdir(),'deadbyte-atomic-dead-lock-'));
|
|
99
|
+
const target=path.join(root,'value.txt');await writeFile(target,'old');
|
|
100
|
+
const lock=`${target}.deadbyte-mutation.lock`;
|
|
101
|
+
await writeFile(lock,'999999999:0:abandoned');
|
|
102
|
+
const stale=new Date(Date.now()-180000);await utimes(lock,stale,stale);
|
|
103
|
+
const readState=async file=>{const bytes=await readFile(file);return {exists:true,sha256:digest(bytes)};};
|
|
104
|
+
try{
|
|
105
|
+
await assert.rejects(atomicMutation({target,bytes:Buffer.from('new'),expected:digest(Buffer.from('old')),readState,
|
|
106
|
+
assertExpected(state,expected){assert.equal(state.sha256,expected);}
|
|
107
|
+
}),/abandoned.*reconciliation/i);
|
|
108
|
+
assert.equal(await readFile(target,'utf8'),'old');
|
|
109
|
+
assert.equal(await readFile(lock,'utf8'),'999999999:0:abandoned');
|
|
110
|
+
}finally{await rm(root,{recursive:true,force:true});}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('lock ownership loss after commit is classified as an unknown-effect failure',async()=>{
|
|
114
|
+
const root=await mkdtemp(path.join(os.tmpdir(),'deadbyte-atomic-lock-loss-'));
|
|
115
|
+
const target=path.join(root,'value.txt');await writeFile(target,'old');
|
|
116
|
+
const readState=async file=>{const bytes=await readFile(file);return {exists:true,sha256:digest(bytes)};};
|
|
117
|
+
try{
|
|
118
|
+
const error=await atomicMutation({target,bytes:Buffer.from('new'),expected:digest(Buffer.from('old')),readState,
|
|
119
|
+
assertExpected(state,expected){assert.equal(state.sha256,expected);},
|
|
120
|
+
verifyPostcondition:async()=>{await writeFile(`${target}.deadbyte-mutation.lock`,'foreign-owner');}
|
|
121
|
+
}).then(()=>null,e=>e);
|
|
122
|
+
assert.equal(error?.outcome,'FAILED_AFTER_UNKNOWN_EFFECT');
|
|
123
|
+
assert.equal(error?.lock_cleanup_failed,true);
|
|
124
|
+
assert.equal(await readFile(target,'utf8'),'new');
|
|
125
|
+
assert.equal(await readFile(`${target}.deadbyte-mutation.lock`,'utf8'),'foreign-owner');
|
|
126
|
+
}finally{await rm(root,{recursive:true,force:true});}
|
|
127
|
+
});
|
|
@@ -5,7 +5,7 @@ import {readFile} from 'node:fs/promises';
|
|
|
5
5
|
import {createCommanderWireAdapter} from '../src/commander-wire-adapter-r47.mjs';
|
|
6
6
|
import {registerCommanderExtendedBridge} from '../src/commander-extended-bridge-r47.mjs';
|
|
7
7
|
|
|
8
|
-
const referencePath='D:\\Projects\\deadbyte-mcp\\references\\desktop-commander\\dist\\tools\\schemas.js';
|
|
8
|
+
const referencePath='D:\\Projects\\deadbyte-mcp\\references\\desktop-commander v0.2.52\\dist\\tools\\schemas.js';
|
|
9
9
|
const expectedHash='f342345e34e48769dabaefe1b932f09d41e2e207b422f7794a15ee50ffa8f140';
|
|
10
10
|
async function referenceSchemas(){
|
|
11
11
|
let source=await readFile(referencePath,'utf8');
|
|
@@ -83,7 +83,7 @@ test('R72 compact baseline permits only R64 read offset and authorized coding-on
|
|
|
83
83
|
assert.deepEqual(diff.added,[]);
|
|
84
84
|
assert.deepEqual(diff.removed,[]);
|
|
85
85
|
const packageVersion=JSON.parse(await readFile(path.join(repoRoot,'package.json'),'utf8')).version;
|
|
86
|
-
if(['0.15.41','0.15.42','0.15.43','0.15.44','0.15.45','0.15.46'].includes(packageVersion)){
|
|
86
|
+
if(['0.15.41','0.15.42','0.15.43','0.15.44','0.15.45','0.15.46','0.15.47'].includes(packageVersion)){
|
|
87
87
|
assert.deepEqual([...diff.schema_changed].sort(),['machine_fs','native_diff_preview']);
|
|
88
88
|
assert.equal(diff.unchanged.length,41);
|
|
89
89
|
const prior=baseline.tools.find(tool=>tool.name==='machine_fs');
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {spawnSync} from 'node:child_process';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import {fileURLToPath} from 'node:url';
|
|
6
|
+
|
|
7
|
+
const root=path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
|
8
|
+
const npmCli=process.env.npm_execpath || path.join(path.dirname(process.execPath),'node_modules','npm','bin','npm-cli.js');
|
|
9
|
+
|
|
10
|
+
test('R73 npm dry-run package excludes generated test evidence and reference trees',()=>{
|
|
11
|
+
const result=spawnSync(process.execPath,[npmCli,'pack','--dry-run','--json'],{
|
|
12
|
+
cwd:root,encoding:'utf8',timeout:120000,windowsHide:true,env:{...process.env,npm_config_audit:'false'}
|
|
13
|
+
});
|
|
14
|
+
assert.equal(result.error,undefined,String(result.error));
|
|
15
|
+
assert.equal(result.status,0,result.stderr||result.stdout);
|
|
16
|
+
const parsed=JSON.parse(result.stdout);
|
|
17
|
+
const record=Array.isArray(parsed)?parsed[0]:parsed['deadbyte-mcp'];
|
|
18
|
+
assert.ok(record&&Array.isArray(record.files),'npm file inventory missing');
|
|
19
|
+
const leaked=record.files.map(row=>row.path).filter(name=>
|
|
20
|
+
/^(?:testing|references|\.git)(?:\/|$)|(?:^|\/)\.env(?:\.|$)/i.test(name));
|
|
21
|
+
assert.deepEqual(leaked,[],'generated evidence or private trees must not enter npm package');
|
|
22
|
+
});
|