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,150 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {mkdtemp, readFile, rm, writeFile} from 'node:fs/promises';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import {acquireKernelExclusiveLock, verifyKernelLockBinary} from '../src/kernel-lock-r74.mjs';
|
|
7
|
+
|
|
8
|
+
async function safeRm(dir) {
|
|
9
|
+
for (let i = 0; i < 15; i++) {
|
|
10
|
+
try {
|
|
11
|
+
await rm(dir, {recursive: true, force: true});
|
|
12
|
+
return;
|
|
13
|
+
} catch (err) {
|
|
14
|
+
if (err.code !== 'EBUSY' && err.code !== 'EPERM') throw err;
|
|
15
|
+
await new Promise(r => setTimeout(r, 40));
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
await rm(dir, {recursive: true, force: true});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
test('kernel lock binary is verified and hash-pinned', async () => {
|
|
22
|
+
const verified = await verifyKernelLockBinary();
|
|
23
|
+
assert.equal(verified.ok, true, verified.reason);
|
|
24
|
+
assert.ok(/^[0-9a-f]{64}$/.test(verified.sha256));
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('kernel exclusive lock can be acquired and released normally', async () => {
|
|
28
|
+
const root = await mkdtemp(path.join(os.tmpdir(), 'deadbyte-r74-lock-basic-'));
|
|
29
|
+
const lock = path.join(root, 'target.lock');
|
|
30
|
+
try {
|
|
31
|
+
const release = await acquireKernelExclusiveLock(lock);
|
|
32
|
+
assert.equal(typeof release, 'function');
|
|
33
|
+
assert.ok(Number.isSafeInteger(release.pid));
|
|
34
|
+
await release();
|
|
35
|
+
const content = await readFile(lock, 'utf8');
|
|
36
|
+
assert.match(content, /^deadbyte\.kernel-lock\.r74:pid=\d+/);
|
|
37
|
+
} finally {
|
|
38
|
+
await safeRm(root);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('concurrent writer is blocked while lock is held by another process', async () => {
|
|
43
|
+
const root = await mkdtemp(path.join(os.tmpdir(), 'deadbyte-r74-lock-contention-'));
|
|
44
|
+
const lock = path.join(root, 'target.lock');
|
|
45
|
+
try {
|
|
46
|
+
const release = await acquireKernelExclusiveLock(lock);
|
|
47
|
+
let error = null;
|
|
48
|
+
try {
|
|
49
|
+
await acquireKernelExclusiveLock(lock, {timeoutMs: 100, pollMs: 15});
|
|
50
|
+
} catch (err) {
|
|
51
|
+
error = err;
|
|
52
|
+
}
|
|
53
|
+
assert.ok(error, 'second writer should have been blocked');
|
|
54
|
+
assert.match(error.message, /atomic mutation lock timed out/);
|
|
55
|
+
await release();
|
|
56
|
+
} finally {
|
|
57
|
+
await safeRm(root);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('crash-safe recovery: killed owner releases handle via OS, successor acquires without deleting lock pathname', async () => {
|
|
62
|
+
const root = await mkdtemp(path.join(os.tmpdir(), 'deadbyte-r74-lock-crash-'));
|
|
63
|
+
const lock = path.join(root, 'target.lock');
|
|
64
|
+
try {
|
|
65
|
+
const releaseFirst = await acquireKernelExclusiveLock(lock);
|
|
66
|
+
const lockPid = releaseFirst.pid;
|
|
67
|
+
assert.ok(Number.isSafeInteger(lockPid));
|
|
68
|
+
|
|
69
|
+
// Abrupt termination (simulating crash)
|
|
70
|
+
process.kill(lockPid, 'SIGKILL');
|
|
71
|
+
|
|
72
|
+
// Wait a brief moment for Windows kernel to close the handle
|
|
73
|
+
await new Promise(r => setTimeout(r, 60));
|
|
74
|
+
|
|
75
|
+
// Successor MUST be able to acquire the lock immediately WITHOUT deleting the lock file!
|
|
76
|
+
const releaseSecond = await acquireKernelExclusiveLock(lock, {timeoutMs: 1000});
|
|
77
|
+
assert.equal(typeof releaseSecond, 'function');
|
|
78
|
+
|
|
79
|
+
// Clean release
|
|
80
|
+
await releaseSecond();
|
|
81
|
+
} finally {
|
|
82
|
+
await safeRm(root);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('dead PID or stale on-disk marker does not block new acquisition when no handle is held', async () => {
|
|
87
|
+
const root = await mkdtemp(path.join(os.tmpdir(), 'deadbyte-r74-lock-pidreuse-'));
|
|
88
|
+
const lock = path.join(root, 'target.lock');
|
|
89
|
+
try {
|
|
90
|
+
// Write a stale/foreign PID into the lock file
|
|
91
|
+
await writeFile(lock, 'deadbyte.kernel-lock.r74:pid=999999999\n');
|
|
92
|
+
|
|
93
|
+
// Because no live kernel handle holds the file, acquisition must succeed immediately
|
|
94
|
+
const release = await acquireKernelExclusiveLock(lock, {timeoutMs: 500});
|
|
95
|
+
assert.equal(typeof release, 'function');
|
|
96
|
+
await release();
|
|
97
|
+
} finally {
|
|
98
|
+
await safeRm(root);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('concurrent recovery: exactly one worker acquires lock, others are serialized', async () => {
|
|
103
|
+
const root = await mkdtemp(path.join(os.tmpdir(), 'deadbyte-r74-lock-concurrent-'));
|
|
104
|
+
const lock = path.join(root, 'target.lock');
|
|
105
|
+
const sequence = [];
|
|
106
|
+
try {
|
|
107
|
+
const runWorker = async (id) => {
|
|
108
|
+
const release = await acquireKernelExclusiveLock(lock, {timeoutMs: 5000, pollMs: 10});
|
|
109
|
+
sequence.push(`enter_${id}`);
|
|
110
|
+
await new Promise(r => setTimeout(r, 20));
|
|
111
|
+
sequence.push(`exit_${id}`);
|
|
112
|
+
await release();
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// Run 3 workers concurrently
|
|
116
|
+
await Promise.all([runWorker(1), runWorker(2), runWorker(3)]);
|
|
117
|
+
|
|
118
|
+
assert.equal(sequence.length, 6);
|
|
119
|
+
// Every enter must be followed by its matching exit before next enter
|
|
120
|
+
for (let i = 0; i < sequence.length; i += 2) {
|
|
121
|
+
const enter = sequence[i];
|
|
122
|
+
const exit = sequence[i + 1];
|
|
123
|
+
const id = enter.split('_')[1];
|
|
124
|
+
assert.equal(exit, `exit_${id}`, `non-exclusive execution detected: ${enter} followed by ${exit}`);
|
|
125
|
+
}
|
|
126
|
+
} finally {
|
|
127
|
+
await safeRm(root);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('replaced binary negative: tampered executable fails closed with recovery blocked', async () => {
|
|
132
|
+
const root = await mkdtemp(path.join(os.tmpdir(), 'deadbyte-r74-lock-tampered-'));
|
|
133
|
+
const lock = path.join(root, 'target.lock');
|
|
134
|
+
// Temporarily override docs/R74-KERNEL-LOCK-BINARY.json hash
|
|
135
|
+
const metaPath = path.resolve('docs/R74-KERNEL-LOCK-BINARY.json');
|
|
136
|
+
const originalMeta = await readFile(metaPath, 'utf8');
|
|
137
|
+
try {
|
|
138
|
+
const tampered = JSON.parse(originalMeta);
|
|
139
|
+
tampered.sha256 = '0'.repeat(64);
|
|
140
|
+
await writeFile(metaPath, JSON.stringify(tampered, null, 2) + '\n');
|
|
141
|
+
|
|
142
|
+
await assert.rejects(
|
|
143
|
+
acquireKernelExclusiveLock(lock, {timeoutMs: 200}),
|
|
144
|
+
/atomic mutation lock recovery blocked: native exclusive locking unavailable/
|
|
145
|
+
);
|
|
146
|
+
} finally {
|
|
147
|
+
await writeFile(metaPath, originalMeta);
|
|
148
|
+
await safeRm(root);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {mkdtemp, readFile, rm, cp, writeFile} from 'node:fs/promises';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import {fileURLToPath} from 'node:url';
|
|
7
|
+
import {verifyR74PrepublishContract} from '../scripts/npm-prepublish-contract-r74.mjs';
|
|
8
|
+
|
|
9
|
+
const repoRoot = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
|
10
|
+
|
|
11
|
+
test('R74 prepublish contract: positive path succeeds and verifies identities', async () => {
|
|
12
|
+
const result = await verifyR74PrepublishContract({rootPath: repoRoot});
|
|
13
|
+
assert.equal(result.schema, 'deadbyte.r74-prepublish-contract.v1');
|
|
14
|
+
assert.equal(result.valid, true);
|
|
15
|
+
assert.equal(result.checks.predecessors_distinct, true);
|
|
16
|
+
assert.equal(result.checks.manifest_exists, true);
|
|
17
|
+
assert.equal(result.checks.manifest_verified, true);
|
|
18
|
+
assert.equal(result.checks.package_leak_check.ok, true);
|
|
19
|
+
assert.equal(result.checks.no_self_referential_tarball_hash, true);
|
|
20
|
+
assert.equal(result.identities.package_name, 'deadbyte-mcp');
|
|
21
|
+
assert.equal(result.identities.package_version, '0.15.47');
|
|
22
|
+
assert.equal(result.identities.target_train, 'R74');
|
|
23
|
+
assert.equal(result.identities.published_source_predecessor, '0.15.46');
|
|
24
|
+
assert.notEqual(result.identities.published_source_predecessor, result.identities.runtime_predecessor);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('R74 prepublish contract: negative - tampered native binary hash fails closed', async () => {
|
|
28
|
+
const sandbox = await mkdtemp(path.join(os.tmpdir(), 'deadbyte-r74-prepub-tamper-'));
|
|
29
|
+
try {
|
|
30
|
+
// Copy minimal required files to sandbox
|
|
31
|
+
await cp(path.join(repoRoot, 'package.json'), path.join(sandbox, 'package.json'));
|
|
32
|
+
await cp(path.join(repoRoot, 'RELEASE.json'), path.join(sandbox, 'RELEASE.json'));
|
|
33
|
+
await cp(path.join(repoRoot, 'MANIFEST.SHA256'), path.join(sandbox, 'MANIFEST.SHA256'));
|
|
34
|
+
await cp(path.join(repoRoot, 'docs'), path.join(sandbox, 'docs'), {recursive: true});
|
|
35
|
+
await cp(path.join(repoRoot, 'bin'), path.join(sandbox, 'bin'), {recursive: true});
|
|
36
|
+
|
|
37
|
+
// Tamper with binary
|
|
38
|
+
const exePath = path.join(sandbox, 'bin', 'deadbyte-kernel-lock-r74.exe');
|
|
39
|
+
await writeFile(exePath, 'TAMPERED_BYTES_R74');
|
|
40
|
+
|
|
41
|
+
await assert.rejects(
|
|
42
|
+
async () => {
|
|
43
|
+
await verifyR74PrepublishContract({rootPath: sandbox});
|
|
44
|
+
},
|
|
45
|
+
/Prepublish contract failed: native binary .* hash mismatch/
|
|
46
|
+
);
|
|
47
|
+
} finally {
|
|
48
|
+
await rm(sandbox, {recursive: true, force: true}).catch(() => {});
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('R74 prepublish contract: negative - predecessor collision fails closed', async () => {
|
|
53
|
+
const sandbox = await mkdtemp(path.join(os.tmpdir(), 'deadbyte-r74-prepub-collision-'));
|
|
54
|
+
try {
|
|
55
|
+
const pkg = JSON.parse(await readFile(path.join(repoRoot, 'package.json'), 'utf8'));
|
|
56
|
+
const release = JSON.parse(await readFile(path.join(repoRoot, 'RELEASE.json'), 'utf8'));
|
|
57
|
+
|
|
58
|
+
// Set runtime predecessor to match published source predecessor (0.15.46)
|
|
59
|
+
release.parent = release.parent || {};
|
|
60
|
+
release.parent.version = '0.15.46';
|
|
61
|
+
|
|
62
|
+
await writeFile(path.join(sandbox, 'package.json'), JSON.stringify(pkg, null, 2));
|
|
63
|
+
await writeFile(path.join(sandbox, 'RELEASE.json'), JSON.stringify(release, null, 2));
|
|
64
|
+
|
|
65
|
+
await assert.rejects(
|
|
66
|
+
async () => {
|
|
67
|
+
await verifyR74PrepublishContract({rootPath: sandbox});
|
|
68
|
+
},
|
|
69
|
+
/Prepublish contract failed: published source and runtime predecessor must be distinct/
|
|
70
|
+
);
|
|
71
|
+
} finally {
|
|
72
|
+
await rm(sandbox, {recursive: true, force: true}).catch(() => {});
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('R74 prepublish contract: negative - package leakage fails closed', async () => {
|
|
77
|
+
// Verify that filter catches testing/, references/, .git, .env
|
|
78
|
+
const leakedNames = ['testing/secret.json', 'references/doc.md', '.git/config', '.env'];
|
|
79
|
+
for (const name of leakedNames) {
|
|
80
|
+
const isLeaked = /^(?:testing|references|\.git)(?:\/|$)|(?:^|\/)\.env(?:\.|$)/i.test(name);
|
|
81
|
+
assert.equal(isLeaked, true, `Expected ${name} to be detected as leaked`);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('R74 prepublish contract: non-self-referential tarball check', async () => {
|
|
86
|
+
// Verify that RELEASE.json does NOT contain a prospective hash of the tarball that contains it
|
|
87
|
+
const release = JSON.parse(await readFile(path.join(repoRoot, 'RELEASE.json'), 'utf8'));
|
|
88
|
+
assert.equal(release.tarball_sha256, undefined, 'RELEASE.json must not claim self-referential tarball hash');
|
|
89
|
+
});
|