deadbyte-mcp 0.15.47 → 0.15.48

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.
Files changed (79) hide show
  1. package/MANIFEST.SHA256 +78 -41
  2. package/PROVENANCE.json +22 -11
  3. package/README.txt +11 -11
  4. package/RELEASE.json +378 -368
  5. package/TEST-MATRIX.json +1 -1
  6. package/bin/deadbyte-epoch-lock-r85.node +0 -0
  7. package/controller/deadbyte-process-policy.json +4 -4
  8. package/docs/R75-RECOVERY-HANDOFF.md +16 -0
  9. package/docs/R85-EPOCH-LOCK-BINARY.json +13 -0
  10. package/native/deadbyte-epoch-lock-r85.cpp +241 -0
  11. package/package.json +1 -1
  12. package/scripts/build-epoch-lock-r85.ps1 +34 -0
  13. package/scripts/deploy-r48.mjs +3 -1
  14. package/scripts/npm-release-r48.mjs +22 -4
  15. package/scripts/npm-release-readiness-r108.mjs +134 -0
  16. package/scripts/r75-npm-bin-integrity-gate.mjs +33 -0
  17. package/scripts/release-manifest.mjs +1 -1
  18. package/scripts/verify-r70-full-regression.ps1 +12 -2
  19. package/src/activation-authority-proof-r75.mjs +67 -0
  20. package/src/activation-continuity-engine-r65.mjs +17 -4
  21. package/src/activation-preflight-r65.mjs +13 -0
  22. package/src/authority-continuity-r65.mjs +265 -21
  23. package/src/deadbyte-cli.mjs +55 -18
  24. package/src/epoch-lock-r85.mjs +48 -0
  25. package/src/foreground-marker-cas-win32.mjs +284 -0
  26. package/src/foreground-process-identity.mjs +3 -3
  27. package/src/privileged-install-trust-r89.mjs +196 -0
  28. package/src/remote-session-guardian.mjs +9 -7
  29. package/src/runtime-update.mjs +5 -22
  30. package/src/version.mjs +1 -1
  31. package/src/windows-powershell-environment.mjs +24 -0
  32. package/test/cli-entrypoint.test.mjs +1 -1
  33. package/test/helpers/r75-authority-proof.mjs +9 -0
  34. package/test/r100-posix-owned-lock-race.test.mjs +72 -0
  35. package/test/r101-acl-probe-environment.test.mjs +51 -0
  36. package/test/r106-trusted-powershell-spawn.test.mjs +53 -0
  37. package/test/r107-updater-powershell-path.test.mjs +77 -0
  38. package/test/r33-closeout-regression.test.mjs +2 -1
  39. package/test/r54-offline-predecessor-self-exclusion.test.mjs +2 -2
  40. package/test/r55-offline-predecessor-guardian-family.test.mjs +2 -2
  41. package/test/r56-rollback-launcher-restore.test.mjs +2 -2
  42. package/test/r57-core-startup-race.test.mjs +2 -2
  43. package/test/r58-concurrent-recovery.test.mjs +10 -7
  44. package/test/r58-stale-owner.test.mjs +5 -6
  45. package/test/r59-tool-surface-compat.test.mjs +1 -1
  46. package/test/r65-activation-orchestration.test.mjs +2 -1
  47. package/test/r65-activation-preflight.test.mjs +7 -6
  48. package/test/r65-authority-epoch-recovery.test.mjs +3 -1
  49. package/test/r65-authority-restored-safe.test.mjs +2 -1
  50. package/test/r65-deploy-preflight.test.mjs +13 -14
  51. package/test/r65-epoch-native-activation-integration.test.mjs +5 -4
  52. package/test/r65-legacy-authority-migration.test.mjs +2 -1
  53. package/test/r67-npm-cross-volume.test.mjs +9 -2
  54. package/test/r71-full-verifier-artifact.test.mjs +21 -15
  55. package/test/r71-isolated-slot-rollback-restore.test.mjs +3 -1
  56. package/test/r71-release-identity.test.mjs +3 -1
  57. package/test/r74-kernel-lock.test.mjs +12 -6
  58. package/test/r74-prepublish-contract.test.mjs +1 -1
  59. package/test/r75-activation-authority-proof.test.mjs +46 -0
  60. package/test/r75-authority-evidence.test.mjs +77 -0
  61. package/test/r76-pinned-marker-cas.test.mjs +78 -0
  62. package/test/r77-epoch-grant-cas.test.mjs +51 -0
  63. package/test/r78-parent-junction-cas.test.mjs +62 -0
  64. package/test/r79-orphan-revocation-reconcile.test.mjs +88 -0
  65. package/test/r79-windows-fsync-regression.test.mjs +45 -0
  66. package/test/r80-durable-epoch-commit.test.mjs +76 -0
  67. package/test/r82-epoch-lock-owned-cas.test.mjs +93 -0
  68. package/test/r83-epoch-lock-fileid-cas.test.mjs +92 -0
  69. package/test/r84-epoch-lock-created-fileid.test.mjs +112 -0
  70. package/test/r85-epoch-lock-release-lifetime.test.mjs +25 -0
  71. package/test/r85-epoch-repeat-append.test.mjs +30 -0
  72. package/test/r86-epoch-cleanup-and-loader.test.mjs +126 -0
  73. package/test/r87-crashed-orphan-revoke-reconcile.test.mjs +110 -0
  74. package/test/r88-native-disposition-failure.test.mjs +35 -0
  75. package/test/r89-privileged-install-boundary.test.mjs +123 -0
  76. package/test/r92-acl-ace-applicability.test.mjs +45 -0
  77. package/test/r94-win32-propagation-enum.test.mjs +51 -0
  78. package/test/r99-privileged-import-closure.test.mjs +45 -0
  79. package/test/remote-session-cli.test.mjs +69 -9
package/TEST-MATRIX.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schema": "deadbyte.test-matrix.v1",
3
- "version": "0.15.47",
3
+ "version": "0.15.48",
4
4
  "train": "R74",
5
5
  "primary_platform": "win32-x64",
6
6
  "reference": {
Binary file
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "schema": "deadbyte.process-policy.v1",
3
3
  "enabled": true,
4
- "grant_file": "D:\\Projects\\deadbyte-mcp\\development\\deadbyte-mcp-0.15.47-r74\\controller\\runtime\\process-grant.json",
5
- "state_dir": "D:\\Projects\\deadbyte-mcp\\development\\deadbyte-mcp-0.15.47-r74\\controller\\runtime\\process-state",
4
+ "grant_file": "D:\\Projects\\deadbyte-mcp\\development\\deadbyte-mcp-0.15.48-r108-release-dev\\controller\\runtime\\process-grant.json",
5
+ "state_dir": "D:\\Projects\\deadbyte-mcp\\development\\deadbyte-mcp-0.15.48-r108-release-dev\\controller\\runtime\\process-state",
6
6
  "native_host": {
7
- "path": "D:\\Projects\\deadbyte-mcp\\development\\deadbyte-mcp-0.15.47-r74\\bin\\deadbyte-process-host.exe",
7
+ "path": "D:\\Projects\\deadbyte-mcp\\development\\deadbyte-mcp-0.15.48-r108-release-dev\\bin\\deadbyte-process-host.exe",
8
8
  "sha256": "daf9cfb371bee88cafbf5597eaa507fec504dbe0b0ddd40d0c3c2e02817f49e4"
9
9
  },
10
10
  "roots": {
11
11
  "core": {
12
- "path": "D:\\Projects\\deadbyte-mcp\\development\\deadbyte-mcp-0.15.47-r74",
12
+ "path": "D:\\Projects\\deadbyte-mcp\\development\\deadbyte-mcp-0.15.48-r108-release-dev",
13
13
  "deny_paths": [
14
14
  "node_modules",
15
15
  "controller/runtime",
@@ -0,0 +1,16 @@
1
+ # R75 isolated authority recovery hardening
2
+
3
+ - Source predecessor: `D:\Projects\deadbyte-mcp\development\deadbyte-mcp-0.15.48-r75`, non-Git, verified 2,304-file manifest SHA-256 `e70561e261610d451d33731a5f19b8439548d7062bed4bf109c4e90f1cd8cf38` before copy.
4
+ - Published source identity remains `0.15.47`; active runtime predecessor observed read-only as `v0.15.45-34aa8d1241cfb8bf`, OFFLINE, with no plane armed. The Autonomous status reason was a broad Coding root, not verified clean-disarmed evidence.
5
+ - This successor keeps `0.15.47/R74` package metadata during behavior verification. It is neither sealed nor release-ready. Production slots, grants, pointer, and historical evidence were not mutated.
6
+ - RED: an `armed=false` Autonomous observation with an unverified reason was activation-safe; an object or Boolean observation without direct grant proof was activation-safe; a startup primary failure was hidden by cleanup failure; malformed ownership marker exited successfully. Each now has a matching focused GREEN test.
7
+ - Authority proof reads each policy by digest, resolves its policy-bound grant location within the isolated runtime, checks grant presence, and requires stable regular-file identity across reads. Host policy v1/v2 remain accepted. The broad-root issuance validator is not used for revocation observation.
8
+ - The existing R75 Autonomous revocation split and startup AggregateError were retained. The new CLI change addresses failure masking in the post-synchronization lifecycle and makes unverified marker cleanup terminal rather than success.
9
+ - R72 isolated controller fixtures verify broad-root absent-grant revocation, signed epoch revocation attempt for a present grant, external-path denial and failed epoch handling. These tests do not operate the production predecessor.
10
+ - Recovery boundary added after independent review: absent owner PID and PID reuse are not ownership evidence. A marker with no verified guardian/owner now remains in place and startup fails closed without touching authority. Identical behavioral RED 0/2 (stale marker proceeded, PID reuse passed), then GREEN 2/2; full remote-session CLI suite 30/30. The verified guardian cleanup path remains intact. This leaves manual reconciliation necessary for an orphan marker without a verified guardian; it does not authorize marker deletion.
11
+ - Remaining proof before `R75_RECOVERY_VERIFIED`: rerun final-byte full regression, Windows gate, manifest, native/bin identity, package inspection, isolated install/doctor, and final independent review.
12
+ - First full verifier run on an intermediate manifest produced 1,574 tests: 1,525 pass, 5 fail, 44 historical skips. The failures were isolated preflight fixture lacking the newly required runtime/policy proof; copied Process policy still pointed to R74; a CLI source-grep harness assumed LF only; and two frozen R72 identity tests asserted version 0.15.46 against current development 0.15.47. The first three fixtures/paths were corrected, and R72-only tests now carry explicit historical-version skip reasons. This first run is not final-byte PASS evidence.
13
+ - A later full verifier on intermediate bytes produced 1,575 tests: 1,527 pass, 2 fail, 46 historical skips. Both failures were R58 tests expecting automatic stale-marker removal from an absent/reused PID. Their isolated fixtures now assert the safer R75 contract instead: no authority command, no marker removal, no foreground READY. Focused rerun 4/4 passed. This intermediate full run is not final-byte PASS evidence.
14
+ - Final production-source gates: durable full verifier `1,575 tests / 1,529 pass / 0 fail / 46 historical/version-scoped skip`, stdout SHA-256 `740f30b8078ecc88142645b4197fcce65abfdc952f086457734769b77dd12a35`; Windows gate focused `56 / 54 pass / 0 fail / 2 historical skip`, repeated full `1,575 / 1,529 pass / 0 fail / 46 skip`, stdout SHA-256 `3b1878039f6b3f123dc2fd4d0bf58b97c51a54309dfa380c50508a35e026e9ba`. Isolated doctor PASS.
15
+ - Exact development package before this handoff update: 2,310 tar members, 12,088,356 bytes, tarball SHA-256 `ee550022b468b998e9d34d2a01e61fb85af9fe89c7812fcffa70d08fca1dda28`, isolated npm install/doctor PASS and canonical npm release `--verify-only` PASS. Publication and activation were not run. Regenerate the development manifest/package after this documentation update before using an exact package claim.
16
+ - Independent review: the owner-PID stale-marker deletion path is closed. Armed epoch validity is verified by the controller and composed with independent pinned grant-file observation, not reverified from signed epoch bytes by this module. `releaseRemoteSession` still has a read-identity then pathname-remove TOCTOU boundary; no production-only runtime or crash proof was performed. These limitations must not be described as proven away.
@@ -0,0 +1,13 @@
1
+ {
2
+ "schema": "deadbyte.r85-epoch-lock-binary.v1",
3
+ "status": "development-candidate",
4
+ "platform": "win32-x64",
5
+ "node_api_version": 8,
6
+ "source": "native/deadbyte-epoch-lock-r85.cpp",
7
+ "source_sha256": "c451c257467b6a141918400b61855bac0e466015c1f3901e8be56ce6d9c32c7f",
8
+ "addon": "bin/deadbyte-epoch-lock-r85.node",
9
+ "addon_sha256": "fb73007ca2303d66120940f192461aac2fa4c45dc4603d920f0878d77e65cfed",
10
+ "build_node_header_version": "25.9.0",
11
+ "build_node_api_header_sha256": "2d4560831e525b47b060ec8a0864ab73993df9e20215ce9f0fe7b24cd31af32a",
12
+ "build_node_import_lib_sha256": "e3577a5a4a772b21646fe05a24d53ce3727395bbbc412f326889ddf7129bc7a9"
13
+ }
@@ -0,0 +1,241 @@
1
+ #define WIN32_LEAN_AND_MEAN
2
+ #define NOMINMAX
3
+ #define NAPI_VERSION 8
4
+ #include <windows.h>
5
+ #include <node_api.h>
6
+ #include <algorithm>
7
+ #include <cstddef>
8
+ #include <cstring>
9
+ #include <memory>
10
+ #include <string>
11
+ #include <vector>
12
+
13
+ namespace {
14
+ struct Lease {
15
+ HANDLE leaf = INVALID_HANDLE_VALUE;
16
+ std::vector<HANDLE> ancestors;
17
+ bool renamed = false;
18
+ ~Lease() {
19
+ if (leaf != INVALID_HANDLE_VALUE) CloseHandle(leaf);
20
+ for (auto it = ancestors.rbegin(); it != ancestors.rend(); ++it) CloseHandle(*it);
21
+ }
22
+ };
23
+
24
+ void Throw(napi_env env, const char* message) { napi_throw_error(env, nullptr, message); }
25
+ void ThrowWin32(napi_env env, const char* message, DWORD code) {
26
+ std::string detail = std::string(message) + " (win32=" + std::to_string(code) + ")";
27
+ Throw(env, detail.c_str());
28
+ }
29
+ bool Text(napi_env env, napi_value value, std::string& out) {
30
+ size_t size = 0;
31
+ if (napi_get_value_string_utf8(env, value, nullptr, 0, &size) != napi_ok || size > 32760) return false;
32
+ out.resize(size);
33
+ size_t actual = 0;
34
+ return napi_get_value_string_utf8(env, value, out.data(), size + 1, &actual) == napi_ok && actual == size &&
35
+ out.find('\0') == std::string::npos;
36
+ }
37
+ bool Wide(const std::string& input, std::wstring& output) {
38
+ if (input.empty()) return false;
39
+ int needed = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, input.data(), static_cast<int>(input.size()), nullptr, 0);
40
+ if (needed < 1) return false;
41
+ output.resize(needed);
42
+ return MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, input.data(), static_cast<int>(input.size()),
43
+ output.data(), needed) == needed;
44
+ }
45
+ bool LocalFullPath(const std::wstring& input, std::wstring& output) {
46
+ DWORD needed = GetFullPathNameW(input.c_str(), 0, nullptr, nullptr);
47
+ if (needed < 4 || needed > 32760) return false;
48
+ std::vector<wchar_t> chars(needed);
49
+ if (GetFullPathNameW(input.c_str(), needed, chars.data(), nullptr) != needed - 1) return false;
50
+ output.assign(chars.data());
51
+ if (output.size() < 4 || !((output[0] >= L'A' && output[0] <= L'Z') ||
52
+ (output[0] >= L'a' && output[0] <= L'z')) || output[1] != L':' || output[2] != L'\\' ||
53
+ output.find(L':', 2) != std::wstring::npos || output.back() == L'\\') return false;
54
+ return true;
55
+ }
56
+ bool PinDirectory(const std::wstring& directory, Lease& lease) {
57
+ HANDLE h = CreateFileW(directory.c_str(), FILE_READ_ATTRIBUTES, FILE_SHARE_READ, nullptr, OPEN_EXISTING,
58
+ FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT, nullptr);
59
+ if (h == INVALID_HANDLE_VALUE) return false;
60
+ FILE_ATTRIBUTE_TAG_INFO info{};
61
+ if (!GetFileInformationByHandleEx(h, FileAttributeTagInfo, &info, sizeof(info)) ||
62
+ !(info.FileAttributes & FILE_ATTRIBUTE_DIRECTORY) || (info.FileAttributes & FILE_ATTRIBUTE_REPARSE_POINT)) {
63
+ CloseHandle(h); SetLastError(ERROR_REPARSE_TAG_INVALID); return false;
64
+ }
65
+ lease.ancestors.push_back(h);
66
+ return true;
67
+ }
68
+ bool PinAncestors(const std::wstring& full, Lease& lease) {
69
+ const size_t end = full.find_last_of(L'\\');
70
+ if (end == std::wstring::npos || end < 2) return false;
71
+ std::wstring current = full.substr(0, 3);
72
+ if (!PinDirectory(current, lease)) return false;
73
+ size_t at = 3;
74
+ while (at < end) {
75
+ size_t next = full.find(L'\\', at);
76
+ if (next == std::wstring::npos || next > end) next = end;
77
+ if (next == at) return false;
78
+ current += full.substr(at, next - at);
79
+ if (!PinDirectory(current, lease)) return false;
80
+ current += L'\\';
81
+ at = next + 1;
82
+ }
83
+ return true;
84
+ }
85
+ DWORD ClosePinnedAncestors(Lease& lease) {
86
+ DWORD firstError = ERROR_SUCCESS;
87
+ for (auto it = lease.ancestors.rbegin(); it != lease.ancestors.rend(); ++it) {
88
+ if (!CloseHandle(*it) && firstError == ERROR_SUCCESS) firstError = GetLastError();
89
+ }
90
+ lease.ancestors.clear();
91
+ return firstError;
92
+ }
93
+ DWORD DisposeHeldLeaf(Lease& lease) {
94
+ if (lease.leaf == INVALID_HANDLE_VALUE) return ERROR_INVALID_HANDLE;
95
+ FILE_DISPOSITION_INFO disposition{};
96
+ disposition.DeleteFile = TRUE;
97
+ if (!SetFileInformationByHandle(lease.leaf, FileDispositionInfo, &disposition, sizeof(disposition)))
98
+ return GetLastError();
99
+ return ERROR_SUCCESS;
100
+ }
101
+ void ThrowWin32WithCleanup(napi_env env, const char* message, DWORD primary, DWORD cleanup) {
102
+ std::string detail = std::string(message) + " (win32=" + std::to_string(primary);
103
+ if (cleanup != ERROR_SUCCESS) detail += ", cleanup_win32=" + std::to_string(cleanup);
104
+ detail += ")";
105
+ Throw(env, detail.c_str());
106
+ }
107
+ void Finalize(napi_env, void* data, void*) { delete static_cast<Lease*>(data); }
108
+
109
+ napi_value Acquire(napi_env env, napi_callback_info info) {
110
+ size_t argc = 3;
111
+ napi_value argv[3];
112
+ if (napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr) != napi_ok || argc != 3) {
113
+ Throw(env, "epoch native acquire arguments invalid"); return nullptr;
114
+ }
115
+ std::string pathUtf8, nonce;
116
+ void* content = nullptr;
117
+ size_t contentLength = 0;
118
+ bool buffer = false;
119
+ if (!Text(env, argv[0], pathUtf8) || napi_is_buffer(env, argv[1], &buffer) != napi_ok || !buffer ||
120
+ napi_get_buffer_info(env, argv[1], &content, &contentLength) != napi_ok ||
121
+ contentLength < 2 || contentLength > 16384 || !Text(env, argv[2], nonce) || nonce.size() != 32 ||
122
+ !std::all_of(nonce.begin(), nonce.end(), [](unsigned char c) {
123
+ return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f');
124
+ })) {
125
+ Throw(env, "epoch native acquire arguments invalid"); return nullptr;
126
+ }
127
+ std::wstring widePath, full;
128
+ if (!Wide(pathUtf8, widePath) || !LocalFullPath(widePath, full)) {
129
+ Throw(env, "epoch native lock requires ordinary local-drive path"); return nullptr;
130
+ }
131
+ auto lease = std::make_unique<Lease>();
132
+ if (!PinAncestors(full, *lease)) {
133
+ ThrowWin32(env, "epoch native ancestor pin failed closed", GetLastError()); return nullptr;
134
+ }
135
+ DWORD existing = GetFileAttributesW(full.c_str());
136
+ if (existing != INVALID_FILE_ATTRIBUTES) {
137
+ napi_value result;
138
+ napi_get_null(env, &result);
139
+ return result;
140
+ }
141
+ DWORD existenceError = GetLastError();
142
+ if (existenceError != ERROR_FILE_NOT_FOUND && existenceError != ERROR_PATH_NOT_FOUND) {
143
+ ThrowWin32(env, "epoch native target existence uncertain", existenceError); return nullptr;
144
+ }
145
+ std::wstring nonceWide(nonce.begin(), nonce.end());
146
+ std::wstring temporary = full + L".r85-" + nonceWide + L".tmp";
147
+ lease->leaf = CreateFileW(temporary.c_str(), GENERIC_READ | GENERIC_WRITE | DELETE | FILE_READ_ATTRIBUTES,
148
+ 0, nullptr, CREATE_NEW, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OPEN_REPARSE_POINT, nullptr);
149
+ if (lease->leaf == INVALID_HANDLE_VALUE) {
150
+ ThrowWin32(env, "epoch native temp create failed closed", GetLastError()); return nullptr;
151
+ }
152
+ FILE_ATTRIBUTE_TAG_INFO tag{};
153
+ if (!GetFileInformationByHandleEx(lease->leaf, FileAttributeTagInfo, &tag, sizeof(tag)) ||
154
+ (tag.FileAttributes & (FILE_ATTRIBUTE_DIRECTORY | FILE_ATTRIBUTE_REPARSE_POINT))) {
155
+ DWORD error = GetLastError(); DWORD cleanup = DisposeHeldLeaf(*lease);
156
+ ThrowWin32WithCleanup(env, "epoch native leaf identity failed closed", error, cleanup); return nullptr;
157
+ }
158
+ const auto* bytes = static_cast<const unsigned char*>(content);
159
+ size_t written = 0;
160
+ while (written < contentLength) {
161
+ DWORD part = 0;
162
+ if (!WriteFile(lease->leaf, bytes + written, static_cast<DWORD>(contentLength - written), &part, nullptr) || !part) {
163
+ DWORD error = GetLastError(); DWORD cleanup = DisposeHeldLeaf(*lease);
164
+ ThrowWin32WithCleanup(env, "epoch native lock write failed closed", error, cleanup); return nullptr;
165
+ }
166
+ written += part;
167
+ }
168
+ if (!FlushFileBuffers(lease->leaf)) {
169
+ DWORD error = GetLastError(); DWORD cleanup = DisposeHeldLeaf(*lease);
170
+ ThrowWin32WithCleanup(env, "epoch native lock flush failed closed", error, cleanup); return nullptr;
171
+ }
172
+ const size_t renameSize = offsetof(FILE_RENAME_INFO, FileName) + (full.size() + 1) * sizeof(wchar_t);
173
+ std::vector<unsigned char> renameBuffer(renameSize, 0);
174
+ auto* renameInfo = reinterpret_cast<FILE_RENAME_INFO*>(renameBuffer.data());
175
+ renameInfo->ReplaceIfExists = FALSE;
176
+ renameInfo->RootDirectory = nullptr;
177
+ renameInfo->FileNameLength = static_cast<DWORD>(full.size() * sizeof(wchar_t));
178
+ std::memcpy(renameInfo->FileName, full.data(), renameInfo->FileNameLength);
179
+ if (!SetFileInformationByHandle(lease->leaf, FileRenameInfo, renameInfo, static_cast<DWORD>(renameSize))) {
180
+ DWORD error = GetLastError(); DWORD cleanup = DisposeHeldLeaf(*lease);
181
+ if (cleanup == ERROR_SUCCESS && (error == ERROR_FILE_EXISTS || error == ERROR_ALREADY_EXISTS)) {
182
+ napi_value result;
183
+ napi_get_null(env, &result);
184
+ return result;
185
+ }
186
+ ThrowWin32WithCleanup(env, "epoch native lock rename failed closed", error, cleanup); return nullptr;
187
+ }
188
+ lease->renamed = true;
189
+ napi_value result;
190
+ if (napi_create_external(env, lease.get(), Finalize, nullptr, &result) != napi_ok) {
191
+ DWORD cleanup = DisposeHeldLeaf(*lease);
192
+ ThrowWin32WithCleanup(env, "epoch native handle publication failed closed", ERROR_NOT_ENOUGH_MEMORY, cleanup);
193
+ return nullptr;
194
+ }
195
+ lease.release();
196
+ return result;
197
+ }
198
+
199
+ napi_value Release(napi_env env, napi_callback_info info) {
200
+ size_t argc = 1;
201
+ napi_value argv[1];
202
+ void* raw = nullptr;
203
+ if (napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr) != napi_ok || argc != 1 ||
204
+ napi_get_value_external(env, argv[0], &raw) != napi_ok || !raw) {
205
+ Throw(env, "epoch native release handle invalid"); return nullptr;
206
+ }
207
+ auto* lease = static_cast<Lease*>(raw);
208
+ if (lease->leaf == INVALID_HANDLE_VALUE || !lease->renamed) {
209
+ Throw(env, "epoch native lock already released"); return nullptr;
210
+ }
211
+ FILE_DISPOSITION_INFO disposition{};
212
+ disposition.DeleteFile = TRUE;
213
+ if (!SetFileInformationByHandle(lease->leaf, FileDispositionInfo, &disposition, sizeof(disposition))) {
214
+ ThrowWin32(env, "epoch native lock disposition failed closed", GetLastError()); return nullptr;
215
+ }
216
+ if (!CloseHandle(lease->leaf)) {
217
+ ThrowWin32(env, "epoch native lock close failed closed", GetLastError()); return nullptr;
218
+ }
219
+ lease->leaf = INVALID_HANDLE_VALUE;
220
+ lease->renamed = false;
221
+ DWORD ancestorCloseError = ClosePinnedAncestors(*lease);
222
+ if (ancestorCloseError != ERROR_SUCCESS) {
223
+ ThrowWin32(env, "epoch native ancestor close failed after lock release", ancestorCloseError);
224
+ return nullptr;
225
+ }
226
+ napi_value result;
227
+ napi_get_undefined(env, &result);
228
+ return result;
229
+ }
230
+
231
+ napi_value Init(napi_env env, napi_value exports) {
232
+ napi_value acquire, release;
233
+ if (napi_create_function(env, "acquire", NAPI_AUTO_LENGTH, Acquire, nullptr, &acquire) != napi_ok ||
234
+ napi_create_function(env, "release", NAPI_AUTO_LENGTH, Release, nullptr, &release) != napi_ok ||
235
+ napi_set_named_property(env, exports, "acquire", acquire) != napi_ok ||
236
+ napi_set_named_property(env, exports, "release", release) != napi_ok) return nullptr;
237
+ return exports;
238
+ }
239
+ } // namespace
240
+
241
+ NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deadbyte-mcp",
3
- "version": "0.15.47",
3
+ "version": "0.15.48",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Authenticated MCP agentic runtime with signed skills, persistent model backends, identity-bound remote devices, durable job supervision, immutable release slots, and separately revocable machine authority.",
@@ -0,0 +1,34 @@
1
+ param(
2
+ [Parameter(Mandatory=$true)][ValidatePattern('^[0-9a-f]{64}$')][string]$SourceSha256,
3
+ [string]$NodeHeaderRoot = (Join-Path $env:LOCALAPPDATA 'node-gyp\Cache\25.9.0')
4
+ )
5
+ $ErrorActionPreference='Stop'
6
+ Set-StrictMode -Version Latest
7
+ $root=Split-Path -Parent $PSScriptRoot
8
+ $source=Join-Path $root 'native\deadbyte-epoch-lock-r85.cpp'
9
+ $observed=(Get-FileHash -LiteralPath $source -Algorithm SHA256).Hash.ToLowerInvariant()
10
+ if($observed -ne $SourceSha256){throw 'R85 native source SHA mismatch; refusing build'}
11
+ $header=Join-Path $NodeHeaderRoot 'include\node\node_api.h'
12
+ $lib=Join-Path $NodeHeaderRoot 'x64\node.lib'
13
+ if(!(Test-Path -LiteralPath $header -PathType Leaf) -or !(Test-Path -LiteralPath $lib -PathType Leaf)){
14
+ throw 'R85 pinned Node-API headers/import library unavailable; no download fallback'
15
+ }
16
+ $vswhere='C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe'
17
+ if(!(Test-Path -LiteralPath $vswhere)){throw 'vswhere missing'}
18
+ $install=(& $vswhere -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath | Select-Object -First 1)
19
+ if(!$install){throw 'MSVC x64 C++ toolchain missing'}
20
+ $dev=Join-Path $install 'Common7\Tools\VsDevCmd.bat'
21
+ $bin=Join-Path $root 'bin'
22
+ New-Item -ItemType Directory -Path $bin -Force | Out-Null
23
+ $output=Join-Path $bin 'deadbyte-epoch-lock-r85.node'
24
+ $object=Join-Path $bin 'deadbyte-epoch-lock-r85.obj'
25
+ $include=Join-Path $NodeHeaderRoot 'include\node'
26
+ $common='/nologo /std:c++17 /permissive- /W4 /WX /O2 /MT /EHsc /guard:cf /diagnostics:caret /utf-8 /Brepro /DUNICODE /D_UNICODE /D_WIN32_WINNT=0x0A00 /DNODE_GYP_MODULE_NAME=deadbyte_epoch_lock_r85'
27
+ $compile='cl {0} /LD /I"{1}" "{2}" /Fo:"{3}" /Fe:"{4}" /link /Brepro /INCREMENTAL:NO "{5}"' -f $common,$include,$source,$object,$output,$lib
28
+ $command='call "{0}" -no_logo -arch=x64 -host_arch=x64 && {1}' -f $dev,$compile
29
+ $cmd=if($env:ComSpec){$env:ComSpec}else{Join-Path $env:SystemRoot 'System32\cmd.exe'}
30
+ & $cmd /d /s /c $command
31
+ if($LASTEXITCODE -ne 0){throw "R85 native build failed exit=$LASTEXITCODE"}
32
+ if(!(Test-Path -LiteralPath $output -PathType Leaf)){throw 'R85 native addon missing'}
33
+ $hash=(Get-FileHash -LiteralPath $output -Algorithm SHA256).Hash.ToLowerInvariant()
34
+ Write-Output "R85_NATIVE_BUILD_PASS source_sha256=$observed addon_sha256=$hash path=$output"
@@ -10,6 +10,7 @@ import {parseAuthorityObservation} from '../src/remote-authority.mjs';
10
10
  import {classifyActivationPreflight,classifyAuthorityContinuity,orchestrateActivation} from '../src/activation-preflight-r65.mjs';
11
11
  import {createEpochNativeContinuityEngine} from '../src/activation-continuity-engine-r65.mjs';
12
12
  import {createActivationClaimManager} from '../src/activation-lock-r65.mjs';
13
+ import {observeAuthorityEvidence} from '../src/activation-authority-proof-r75.mjs';
13
14
 
14
15
  const root=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'..');
15
16
  const state=path.resolve(process.env.DEADBYTE_STATE_ROOT||path.join(os.homedir(),'.deadbyte-mcp'));
@@ -57,7 +58,8 @@ async function observeController(){
57
58
  if(!info.isFile()||info.isSymbolicLink())throw new Error(`activation ${plane} policy must be a regular file`);
58
59
  policySha256[plane]=await sha(file);
59
60
  }
60
- return {...observed,policy_sha256:policySha256,policy_paths:policyPaths,active_release_id:active.release_id,active_manifest_sha256:active.manifest_sha256,
61
+ const authorityProof=await observeAuthorityEvidence({stateRoot:state,policyPaths,policySha256,authority:observed.authority});
62
+ return {...observed,authority_proof:authorityProof,policy_sha256:policySha256,policy_paths:policyPaths,active_release_id:active.release_id,active_manifest_sha256:active.manifest_sha256,
61
63
  active_generation_manifest_sha256:active.generation_manifest_sha256};
62
64
  }
63
65
 
@@ -95,6 +95,10 @@ async function main() {
95
95
  if (!isVerifyOnly && version==='0.15.45') assertR71NpmPublishReady(release);
96
96
  if (!isVerifyOnly && version==='0.15.46') await assertR72NpmPublishReady(release,{root});
97
97
  if (!isVerifyOnly && version==='0.15.47') await assertR74NpmPublishReady(release,{root});
98
+ if (version==='0.15.48') {
99
+ const {assertR108NpmPublishReady} = await import('./npm-release-readiness-r108.mjs');
100
+ await assertR108NpmPublishReady(release,{root,provenance});
101
+ }
98
102
  if (release.parent?.release_id !== provenance.parent_release_id ||
99
103
  release.parent?.manifest_sha256 !== provenance.parent_manifest_sha256 ||
100
104
  release.parent?.generation_manifest_sha256 !== provenance.parent_generation_manifest_sha256) {
@@ -109,6 +113,7 @@ async function main() {
109
113
 
110
114
  if (isAlreadyPublished) console.log(`Notice: deadbyte-mcp@${version} exists on registry (${existingText}); exact-byte reconciliation is required.`);
111
115
 
116
+ run(node, [path.join(root, 'scripts', 'r75-npm-bin-integrity-gate.mjs')]);
112
117
  console.log('[2/7] Pre-pack gates: verifying working tree manifest and doctor...');
113
118
  run(node, [path.join(root, 'scripts', 'release-manifest.mjs'), 'verify']);
114
119
  const doctorState=await mkdtemp(path.join(os.tmpdir(),'deadbyte-r65-release-doctor-'));
@@ -135,7 +140,8 @@ async function main() {
135
140
  try {
136
141
  console.log('[4/7] Verifying packed artifact file surface against MANIFEST.SHA256 and packaging contract...');
137
142
  const manifestText = await readFile(path.join(root, 'MANIFEST.SHA256'), 'utf8');
138
- const manifestSha256=createHash('sha256').update(manifestText).digest('hex');
143
+ const claimData = resuming ? JSON.parse(await readFile(path.join(publicationDir, 'claim.json'), 'utf8')) : null;
144
+ const manifestSha256 = resuming ? claimData.manifest_sha256 : createHash('sha256').update(manifestText).digest('hex');
139
145
  const manifestEntries = new Set();
140
146
  for (const line of manifestText.split(/\r?\n/)) {
141
147
  if (!line) continue;
@@ -223,12 +229,24 @@ async function main() {
223
229
  lookup:async()=>{
224
230
  try{
225
231
  const viewed=parseJson(runNpm(['view',`deadbyte-mcp@${version}`,'version','dist.integrity','dist.shasum','dist.tarball','--json'],{timeout:120000}));
226
- return Array.isArray(viewed)?viewed[0]:viewed;
232
+ const item = Array.isArray(viewed)?viewed[0]:viewed;
233
+ if (item && !item.dist && item['dist.tarball']) {
234
+ item.dist = {
235
+ tarball: item['dist.tarball'],
236
+ shasum: item['dist.shasum'],
237
+ integrity: item['dist.integrity']
238
+ };
239
+ }
240
+ return item;
227
241
  }catch(error){if(/E404|404 Not Found/.test(String(error?.message)))return null;throw error;}
228
242
  },
229
- tags:async()=>parseJson(runNpm(['view','deadbyte-mcp','dist-tags','--json'],{timeout:120000})),
243
+ tags:async()=>{
244
+ const parsed=parseJson(runNpm(['view','deadbyte-mcp','dist-tags','--json'],{timeout:120000}));
245
+ return Array.isArray(parsed)?parsed[0]:parsed;
246
+ },
230
247
  fetchTarball:async published=>{
231
- const response=await fetch(published?.dist?.tarball,{signal:AbortSignal.timeout(120000)});
248
+ const url = published?.dist?.tarball ?? published?.['dist.tarball'];
249
+ const response=await fetch(url,{signal:AbortSignal.timeout(120000)});
232
250
  if(!response.ok)throw Object.assign(new Error(`registry tarball fetch failed status=${response.status}`),{status:response.status});
233
251
  return Buffer.from(await response.arrayBuffer());
234
252
  }});
@@ -0,0 +1,134 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { readFile, writeFile, mkdir } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ const defaultRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
7
+ const sha256 = value => createHash('sha256').update(value).digest('hex');
8
+ const evidenceName = 'r108-protected-publication-evidence.json';
9
+ const norm = p => path.resolve(p).replace(/\\/g, '/').toLowerCase();
10
+ const assert = (ok, message) => { if (!ok) throw new Error('R108_PUBLICATION_GATE: ' + message); };
11
+ const json = async file => JSON.parse(await readFile(file, 'utf8'));
12
+
13
+ function manifestEntries(input) {
14
+ const entries = new Map();
15
+ for (const line of input.split(/\r?\n/)) {
16
+ if (!line.trim()) continue;
17
+ const match = /^([a-f0-9]{64})\s+\*?(.+)$/i.exec(line);
18
+ assert(match !== null, 'unrecognized manifest line format');
19
+ const rel = match[2].replace(/\\/g, '/');
20
+ assert(!entries.has(rel), 'duplicate manifest entry ' + rel);
21
+ entries.set(rel, match[1].toLowerCase());
22
+ }
23
+ return entries;
24
+ }
25
+
26
+ function testCounts(output) {
27
+ const values = {};
28
+ for (const line of output.split(/\r?\n/)) {
29
+ const match = /^\s*(?:ℹ|#)\s*(tests|pass|fail|skipped)\s+(\d+)\s*$/.exec(line);
30
+ if (match) values[match[1]] = Number(match[2]);
31
+ }
32
+ assert(['tests','pass','fail','skipped'].every(k => Number.isSafeInteger(values[k])), 'missing complete Node test-runner footer');
33
+ assert(values.tests === values.pass + values.fail + values.skipped, 'inconsistent test totals');
34
+ assert(values.fail === 0, 'fresh full regression reported failures');
35
+ assert(values.pass >= 1621, 'full regression below R107 established pass baseline');
36
+ assert(values.skipped <= 48, 'new unexplained skips relative to R107');
37
+ return values;
38
+ }
39
+
40
+ async function slotParity(root, protectedRoot, expectedRootSha, expectedSlotSha) {
41
+ assert(path.isAbsolute(protectedRoot), 'protected slot must be absolute');
42
+ assert(norm(root) !== norm(protectedRoot), 'protected test slot must differ from source');
43
+ if (process.platform === 'win32') {
44
+ assert(norm(protectedRoot).startsWith('c:/program files/'), 'protected slot must be under Program Files');
45
+ }
46
+ const sourceBytes = await readFile(path.join(root,'MANIFEST.SHA256'));
47
+ const slotBytes = await readFile(path.join(protectedRoot,'MANIFEST.SHA256'));
48
+ assert(sha256(sourceBytes) === expectedRootSha, 'R108 source manifest hash changed after test');
49
+ assert(sha256(slotBytes) === expectedSlotSha, 'protected test manifest hash changed after test');
50
+ const source = manifestEntries(sourceBytes.toString('utf8'));
51
+ const slot = manifestEntries(slotBytes.toString('utf8'));
52
+ assert(source.size === slot.size, 'source/protected manifest surface differs');
53
+ const allowed = 'controller/deadbyte-process-policy.json';
54
+ for (const [p,h] of source) {
55
+ assert(slot.has(p), 'protected fixture missing source entry ' + p);
56
+ if (p !== allowed) assert(slot.get(p) === h, 'protected fixture changes tested source bytes at ' + p);
57
+ }
58
+ assert(source.has(allowed) && slot.has(allowed), 'required policy fixture missing from manifest');
59
+ for (const [label,dir] of [['source',root],['protected',protectedRoot]]) {
60
+ const policy = await json(path.join(dir, 'controller', 'deadbyte-process-policy.json'));
61
+ assert(norm(policy?.roots?.core?.path ?? '.') === norm(dir), label + ' process policy core root not bound to its own checkout');
62
+ assert(norm(policy?.native_host?.path ?? '.') === norm(path.join(dir,'bin','deadbyte-process-host.exe')), label + ' native host not self-bound');
63
+ assert(norm(policy?.grant_file ?? '.') === norm(path.join(dir,'controller','runtime','process-grant.json')), label + ' grant path not self-bound');
64
+ assert(norm(policy?.state_dir ?? '.') === norm(path.join(dir,'controller','runtime','process-state')), label + ' state path not self-bound');
65
+ }
66
+ return { sourceFiles: source.size, protectedFiles: slot.size };
67
+ }
68
+
69
+ export async function assertR108NpmPublishReady(release, {root = defaultRoot,provenance = null} = {}) {
70
+ const rootPath = path.resolve(root);
71
+ const pkg = await json(path.join(rootPath,'package.json'));
72
+ const prov = provenance ?? await json(path.join(rootPath,'PROVENANCE.json'));
73
+ const lock = await json(path.join(rootPath,'package-lock.json'));
74
+ assert(pkg.name === 'deadbyte-mcp', 'package name mismatch');
75
+ assert([pkg.version,lock.version,lock.packages?.['']?.version,release.version,prov.version].every(v => v === '0.15.48'), 'package/release/provenance identity mismatch');
76
+ assert(release.train === 'R108', 'release train is not R108');
77
+ assert(release.r108_candidate?.status === 'CANDIDATE_UNSEALED', 'R108 pending-state metadata missing');
78
+ assert(release.source_predecessor?.version === '0.15.47' && prov.source_predecessor_version === '0.15.47', 'published source predecessor mismatch');
79
+ assert(release.parent?.version === '0.15.45' && release.runtime_predecessor?.version === '0.15.45', 'runtime predecessor mismatch');
80
+ for (const key of ['release_id','manifest_sha256','generation_manifest_sha256']) {
81
+ assert(release.parent?.[key] === prov['parent_'+key], 'sealed parent lineage mismatch at ' + key);
82
+ assert(release.runtime_predecessor?.[key] === release.parent?.[key], 'runtime predecessor drift at ' + key);
83
+ assert(prov.runtime_predecessor?.[key] === release.parent?.[key], 'provenance runtime predecessor drift at ' + key);
84
+ }
85
+ const proof = await json(path.join(rootPath,'testing',evidenceName));
86
+ assert(proof.schema === 'deadbyte.r108-protected-publication-evidence.v1', 'missing/fake R108 proof schema');
87
+ assert(proof.version === '0.15.48', 'proof version mismatch');
88
+ assert(norm(proof.candidate_root ?? '.') === norm(rootPath), 'proof candidate root changed');
89
+ assert(proof.test_exit_code === 0, 'test process did not finish with exit 0');
90
+ const testLog = await readFile(path.join(rootPath,'testing','r108-protected-full.tap'));
91
+ assert(sha256(testLog) === proof.test_log_sha256, 'test log bytes drifted');
92
+ const actualCounts = testCounts(testLog.toString('utf8'));
93
+ for (const key of ['tests','pass','fail','skipped']) assert(actualCounts[key] === proof.counts?.[key], 'proof disagrees with test log at '+key);
94
+ const parity = await slotParity(rootPath, proof.protected_root, proof.source_manifest_sha256, proof.protected_manifest_sha256);
95
+ assert(parity.sourceFiles >= 2339, 'manifest file count smaller than R107 baseline');
96
+ console.log('R108_PREPUBLISH_GATE_PASS '+JSON.stringify({version:'0.15.48',manifest_sha256:proof.source_manifest_sha256,test_log_sha256:proof.test_log_sha256,counts:actualCounts,source_files:parity.sourceFiles,protected_files:parity.protectedFiles}));
97
+ return true;
98
+ }
99
+
100
+ const invoked = process.argv[1] && norm(process.argv[1]) === norm(fileURLToPath(import.meta.url));
101
+ if (invoked) {
102
+ try {
103
+ const command = process.argv[2] ?? 'verify';
104
+ if (command === 'record') {
105
+ const [rootArg,protectedArg,exitCodeArg] = process.argv.slice(3);
106
+ assert(rootArg && protectedArg && exitCodeArg !== undefined, 'usage: node scripts/npm-release-readiness-r108.mjs record ROOT PROTECTED_ROOT TEST_EXIT');
107
+ const root = path.resolve(rootArg), protectedRoot = path.resolve(protectedArg);
108
+ assert(Number(exitCodeArg) === 0, 'test exit code not zero');
109
+ const testLog = await readFile(path.join(root,'testing','r108-protected-full.tap'));
110
+ const counts = testCounts(testLog.toString('utf8'));
111
+ const sourceManifest = sha256(await readFile(path.join(root,'MANIFEST.SHA256')));
112
+ const protectedManifest = sha256(await readFile(path.join(protectedRoot,'MANIFEST.SHA256')));
113
+ await slotParity(root,protectedRoot,sourceManifest,protectedManifest);
114
+ const receipt = {
115
+ schema:'deadbyte.r108-protected-publication-evidence.v1',
116
+ version:'0.15.48',candidate_root:root,protected_root:protectedRoot,
117
+ source_manifest_sha256:sourceManifest,protected_manifest_sha256:protectedManifest,
118
+ test_log_sha256:sha256(testLog),test_exit_code:0,counts,
119
+ limitations:'Locally hashed regression and source/protected manifest parity; not a hardware attestation or independent signature.'
120
+ };
121
+ const proofPath=path.join(root,'testing',evidenceName);
122
+ await mkdir(path.dirname(proofPath),{recursive:true});
123
+ await writeFile(proofPath,JSON.stringify(receipt,null,2)+'\n',{flag:'wx'});
124
+ console.log('R108_PROOF_WRITTEN '+JSON.stringify({path:proofPath,...receipt}));
125
+ } else if (command === 'verify') {
126
+ const root = path.resolve(process.argv[3] ?? defaultRoot);
127
+ const release = await json(path.join(root,'RELEASE.json'));
128
+ await assertR108NpmPublishReady(release,{root});
129
+ } else throw new Error('unknown command '+command);
130
+ } catch(error) {
131
+ console.error(error?.stack || String(error));
132
+ process.exitCode=1;
133
+ }
134
+ }
@@ -0,0 +1,33 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { createHash } from 'node:crypto';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
7
+ const pkg = JSON.parse(readFileSync(path.join(root, 'package.json'), 'utf8'));
8
+ const bin = typeof pkg.bin === 'string' ? { [pkg.name]: pkg.bin } : pkg.bin;
9
+ if (!bin || typeof bin !== 'object' || Array.isArray(bin) || !Object.keys(bin).length)
10
+ throw new Error('BIN_INTEGRITY: package.json.bin missing or invalid');
11
+ const lines = readFileSync(path.join(root, 'MANIFEST.SHA256'), 'utf8').split(/\r?\n/).filter(Boolean);
12
+ const entries = new Map();
13
+ for (const line of lines) {
14
+ const m = /^([0-9a-f]{64}) (.+)$/.exec(line);
15
+ if (!m || entries.has(m[2])) throw new Error('BIN_INTEGRITY: invalid or duplicate manifest entry');
16
+ entries.set(m[2], m[1]);
17
+ }
18
+ for (const [name, relRaw] of Object.entries(bin)) {
19
+ if (typeof relRaw !== 'string' || !relRaw || relRaw.includes('\\'))
20
+ throw new Error(`BIN_INTEGRITY: invalid bin path for ${name}`);
21
+ const rel = relRaw.replace(/^\.\//, '');
22
+ const full = path.resolve(root, rel);
23
+ if (!full.startsWith(root + path.sep) || !entries.has(rel))
24
+ throw new Error(`BIN_INTEGRITY: unmanifested or escaping bin '${name}'`);
25
+ const bytes = readFileSync(full);
26
+ const firstLf = bytes.indexOf(0x0a);
27
+ if (firstLf < 2 || bytes[0] !== 0x23 || bytes[1] !== 0x21 || bytes[firstLf - 1] === 0x0d)
28
+ throw new Error(`BIN_INTEGRITY: bin '${name}' must begin with shebang LF (no CRLF)`);
29
+ const sha = createHash('sha256').update(bytes).digest('hex');
30
+ if (sha !== entries.get(rel))
31
+ throw new Error(`BIN_INTEGRITY: manifest and bin bytes differ for '${name}'`);
32
+ process.stdout.write(`BIN_INTEGRITY_PASS ${name} ${rel} ${sha}\n`);
33
+ }
@@ -18,7 +18,7 @@ const requiredNativePayload = [
18
18
  'bin/bootstrap-kernel32.exe', 'bin/bootstrap-advapi32.exe',
19
19
  'bin/appcontainer-stage.exe', 'bin/bootstrap-exitcode.exe',
20
20
  'bin/child-control-stage.exe', 'bin/child-control-probe.exe',
21
- 'bin/deadbyte-exec.exe', 'bin/contained-transform-worker.exe',
21
+ 'bin/deadbyte-exec.exe', 'bin/deadbyte-epoch-lock-r85.node', 'bin/contained-transform-worker.exe',
22
22
  'bin/contained-reverse-worker.exe', 'bin/deadbyte-tunnel-host.exe', 'bin/WORKER-REGISTRY.txt'
23
23
  ];
24
24
  const requiredBundledDependencyRoots = [
@@ -1,6 +1,6 @@
1
1
  #requires -Version 5.1
2
2
  # R70 verifier artifact: runs the candidate test suite without altering release manifests.
3
- param([switch]$SelfTest)
3
+ param([switch]$SelfTest,[string]$EvidenceRoot='')
4
4
  Set-StrictMode -Version Latest
5
5
  $ErrorActionPreference = 'Continue'
6
6
  Set-Location -LiteralPath (Split-Path -Parent $PSScriptRoot)
@@ -36,7 +36,17 @@ $selected = @($lines | Where-Object {
36
36
  $summary = @($selected | Select-Object -Last 140 | ForEach-Object {
37
37
  if ($_.Length -gt 512) { $_.Substring(0, 512) } else { $_ }
38
38
  })
39
- $artifactDir = Join-Path (Get-Location).Path 'testing\r70-verifier'
39
+ if ($EvidenceRoot -and -not $SelfTest) { throw 'EvidenceRoot override is self-test only' }
40
+ $artifactDir = if ($EvidenceRoot) {
41
+ if (-not [System.IO.Path]::IsPathRooted($EvidenceRoot)) { throw 'EvidenceRoot must be absolute' }
42
+ $resolved = [System.IO.Path]::GetFullPath($EvidenceRoot)
43
+ $checkout = [System.IO.Path]::GetFullPath((Get-Location).Path).TrimEnd('\')
44
+ if ($resolved.TrimEnd('\').Equals($checkout,[System.StringComparison]::OrdinalIgnoreCase) -or
45
+ $resolved.StartsWith($checkout + '\',[System.StringComparison]::OrdinalIgnoreCase)) {
46
+ throw 'EvidenceRoot self-test override must be outside the checkout'
47
+ }
48
+ $resolved
49
+ } else { Join-Path (Get-Location).Path 'testing\r70-verifier' }
40
50
  if (-not (Test-Path -LiteralPath $artifactDir -PathType Container)) {
41
51
  New-Item -ItemType Directory -Path $artifactDir -Force | Out-Null
42
52
  }