mcp-pacemaker 2.0.1

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 (47) hide show
  1. package/CHANGELOG.md +452 -0
  2. package/CONTRIBUTING.md +222 -0
  3. package/LICENSE +21 -0
  4. package/README.md +621 -0
  5. package/SECURITY.md +120 -0
  6. package/THIRD_PARTY_NOTICES.txt +366 -0
  7. package/autostart/windows/launcher.vbs +11 -0
  8. package/autostart/windows/register-task.ps1 +63 -0
  9. package/bin/cli.mjs +880 -0
  10. package/bin/config-checks.mjs +62 -0
  11. package/bin/logs.mjs +465 -0
  12. package/bin/mcp-bridge.mjs +2162 -0
  13. package/bin/pooling-batch-scheduler.mjs +129 -0
  14. package/bin/pooling-batches.mjs +270 -0
  15. package/bin/pooling-config.mjs +577 -0
  16. package/bin/pooling-editor.mjs +128 -0
  17. package/bin/pooling-errors.mjs +23 -0
  18. package/bin/pooling-execution.mjs +104 -0
  19. package/bin/pooling-files.mjs +541 -0
  20. package/bin/pooling-trace.mjs +218 -0
  21. package/bin/pooling-writer.mjs +242 -0
  22. package/bin/prewarm.mjs +233 -0
  23. package/bin/server-metrics.mjs +69 -0
  24. package/bin/service-control.mjs +124 -0
  25. package/bin/shared-sessions.mjs +1083 -0
  26. package/bin/top.mjs +115 -0
  27. package/bin/update-check.mjs +153 -0
  28. package/bin/windows/PoolingSecurityHelper.build.json +42 -0
  29. package/bin/windows/PoolingSecurityHelper.exe +0 -0
  30. package/bin/windows/src/AssemblyInfo.cs +11 -0
  31. package/bin/windows/src/PoolingNativeFiles.cs +678 -0
  32. package/bin/windows/src/PoolingSecurityHelper.cs +178 -0
  33. package/bin/windows/src/PoolingSecurityReader.cs +50 -0
  34. package/docs/shared-sessions.md +181 -0
  35. package/examples/servers.example.json +31 -0
  36. package/package.json +77 -0
  37. package/supervisor/bridge-child.mjs +14 -0
  38. package/supervisor/supervise.mjs +172 -0
  39. package/supervisor/supervise.ps1 +23 -0
  40. package/supervisor/supervise.sh +11 -0
  41. package/tools/windows-security-helper/README.md +238 -0
  42. package/tools/windows-security-helper/build.ps1 +262 -0
  43. package/ui/dist/THIRD_PARTY_NOTICES.txt +366 -0
  44. package/ui/dist/assets/index-CaLPg0c-.css +1 -0
  45. package/ui/dist/assets/index-CgPruWYi.js +9 -0
  46. package/ui/dist/index.html +14 -0
  47. package/ui/dist/third-party-manifest.json +597 -0
@@ -0,0 +1,172 @@
1
+ import { fork } from 'node:child_process';
2
+ import { randomBytes, randomUUID } from 'node:crypto';
3
+ import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
4
+ import { createServer } from 'node:net';
5
+ import { homedir } from 'node:os';
6
+ import { dirname, resolve, join } from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+ import { parseArgs } from 'node:util';
9
+ import { servicePaths, assertDurableRoot } from '../bin/service-control.mjs';
10
+
11
+ const { values } = parseArgs({
12
+ options: { port: { type: 'string', default: '8791' }, config: { type: 'string' } },
13
+ });
14
+ const port = Number(values.port);
15
+ const root = realpathSync(resolve(dirname(fileURLToPath(import.meta.url)), '..'));
16
+ const config = resolve(values.config || join(homedir(), '.mcp-pacemaker', 'servers.json'));
17
+ const paths = servicePaths(config, port);
18
+ assertDurableRoot(root);
19
+ if (Number(process.versions.node.split('.')[0]) < 20) throw new Error('Node.js >=20 is required.');
20
+
21
+ if (existsSync(paths.hold)) {
22
+ console.log(`Service :${port} is held stopped. Use the matching CLI start --port ${port} to resume deliberately.`);
23
+ } else {
24
+ await supervise();
25
+ }
26
+
27
+ async function supervise() {
28
+ const identity = {
29
+ protocol: 1, root, config, port, socket: paths.socket, pid: process.pid, execPath: process.execPath,
30
+ id: randomUUID(), token: randomBytes(32).toString('hex'),
31
+ version: JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')).version,
32
+ };
33
+ let child;
34
+ let childExit;
35
+ let retry;
36
+ let stopping;
37
+ let stopRequested = false;
38
+ let holdRequested = false;
39
+ const clients = new Set();
40
+ const server = createServer((socket) => {
41
+ clients.add(socket);
42
+ socket.on('close', () => clients.delete(socket));
43
+ socket.on('error', () => socket.destroy());
44
+ socket.setTimeout(15000, () => socket.destroy());
45
+ let data = '';
46
+ let handled = false;
47
+ socket.on('data', (chunk) => {
48
+ if (handled) return;
49
+ data += chunk;
50
+ if (data.length > 4096) { socket.destroy(); return; }
51
+ if (!data.includes('\n')) return;
52
+ handled = true;
53
+ let request;
54
+ try { request = JSON.parse(data.slice(0, data.indexOf('\n'))); }
55
+ catch { socket.destroy(); return; }
56
+ if (request.id !== identity.id ||
57
+ request.token !== identity.token ||
58
+ request.action !== 'stop') {
59
+ socket.destroy();
60
+ return;
61
+ }
62
+ stop().then(() => {
63
+ socket.end(JSON.stringify({ id: identity.id, stopped: true, autostartHeld: true }) + '\n');
64
+ }, (error) => {
65
+ console.error(error.message);
66
+ socket.end(JSON.stringify({ id: identity.id, stopped: false }) + '\n');
67
+ });
68
+ });
69
+ });
70
+
71
+ mkdirSync(dirname(config), { recursive: true, mode: 0o700 });
72
+ await new Promise((resolveListen, reject) => {
73
+ server.once('error', reject);
74
+ server.listen(paths.socket, resolveListen);
75
+ });
76
+ server.on('error', (error) => { console.error(error.message); process.exitCode = 1; });
77
+ try {
78
+ writeFileSync(paths.record, JSON.stringify(identity) + '\n', { mode: 0o600 });
79
+ if (existsSync(paths.hold)) {
80
+ server.close();
81
+ return;
82
+ }
83
+ launch();
84
+ } catch (error) {
85
+ server.close();
86
+ throw error;
87
+ }
88
+ process.on('SIGINT', onSignal);
89
+ process.on('SIGTERM', onSignal);
90
+
91
+ function launch() {
92
+ if (stopRequested ||
93
+ existsSync(paths.hold)) {
94
+ server.close();
95
+ return;
96
+ }
97
+ child = fork(join(root, 'supervisor', 'bridge-child.mjs'), ['--port', String(port), '--config', config], {
98
+ execPath: process.execPath, execArgv: [], stdio: ['ignore', 'inherit', 'inherit', 'ipc'], windowsHide: true,
99
+ });
100
+ childExit = new Promise((resolveExit, reject) => {
101
+ child.once('error', reject);
102
+ child.once('exit', (code) => {
103
+ child = undefined;
104
+ resolveExit();
105
+ if (stopRequested) return;
106
+ if (code === 3) server.close();
107
+ else retry = setTimeout(launch, 2000);
108
+ });
109
+ });
110
+ childExit.catch((error) => {
111
+ console.error(`Bridge launch failed: ${error.message}`);
112
+ process.exitCode = 1;
113
+ server.close();
114
+ });
115
+ }
116
+
117
+ async function stop(hold = true) {
118
+ // Persist before touching the child so logon/unlock and OS retries cannot restart it.
119
+ if (hold &&
120
+ !holdRequested) {
121
+ writeFileSync(paths.hold, JSON.stringify({ id: identity.id, root, config, port }) + '\n', { mode: 0o600 });
122
+ holdRequested = true;
123
+ }
124
+ if (stopping) return stopping;
125
+ stopRequested = true;
126
+ clearTimeout(retry);
127
+ stopping = (async () => {
128
+ if (child) {
129
+ if (child.connected) {
130
+ try {
131
+ await new Promise((resolveSend, reject) => child.send('stop', (error) => error ? reject(error) : resolveSend()));
132
+ } catch (error) {
133
+ if (error.code !== 'ERR_IPC_CHANNEL_CLOSED' &&
134
+ error.code !== 'EPIPE') throw error;
135
+ }
136
+ }
137
+ let timeout;
138
+ try {
139
+ await Promise.race([
140
+ childExit,
141
+ new Promise((_, reject) => {
142
+ timeout = setTimeout(() => reject(new Error('Bridge did not exit; do not replace its root. No force-kill attempted.')), 10000);
143
+ }),
144
+ ]);
145
+ } finally {
146
+ clearTimeout(timeout);
147
+ }
148
+ }
149
+ if (holdRequested) {
150
+ writeFileSync(paths.hold, JSON.stringify({ id: identity.id, root, config, port, stopped: true }) + '\n', { mode: 0o600 });
151
+ }
152
+ server.close();
153
+ process.removeListener('SIGINT', onSignal);
154
+ process.removeListener('SIGTERM', onSignal);
155
+ })().catch((error) => {
156
+ // Keep restart suppressed, but let a later explicit stop verify the real child exit.
157
+ // A timeout must not write a completed receipt or permanently poison reconciliation.
158
+ stopping = undefined;
159
+ throw error;
160
+ });
161
+ return stopping;
162
+ }
163
+
164
+ function onSignal() {
165
+ stop(false).then(() => {
166
+ for (const socket of clients) socket.destroy();
167
+ }, (error) => {
168
+ console.error(error.message);
169
+ process.exitCode = 1;
170
+ });
171
+ }
172
+ }
@@ -0,0 +1,23 @@
1
+ <#
2
+ .SYNOPSIS Supervisor for the mcp-pacemaker bridge (Windows). Keeps it alive across crashes.
3
+ .DESCRIPTION
4
+ Runs the managed supervisor. A targeted CLI stop holds automatic restarts until an
5
+ explicit CLI start. Node.js 20 or newer must be on PATH.
6
+ .PARAMETER Port Port the bridge listens on (default 8791). Must match your client config.
7
+ .OUTPUTS
8
+ Supervisor diagnostics.
9
+ .EXAMPLE
10
+ .\supervise.ps1 -Port 8791
11
+ #>
12
+ [CmdletBinding()]
13
+ param([ValidateRange(1, 65535)][int]$Port = 8791)
14
+
15
+ $ErrorActionPreference = 'Stop'
16
+ $supervisor = Join-Path -Path $PSScriptRoot -ChildPath 'supervise.mjs'
17
+
18
+ if (-not (Get-Command node -ErrorAction SilentlyContinue)) {
19
+ throw "node is not on PATH. Install Node.js (>=20) and retry."
20
+ }
21
+
22
+ & node $supervisor --port $Port
23
+ exit $LASTEXITCODE
@@ -0,0 +1,11 @@
1
+ #!/bin/sh
2
+ # Starts the managed supervisor. Arg 1 = port.
3
+ PORT="${1:-8791}"
4
+ DIR="$(cd "$(dirname "$0")/.." && pwd)"
5
+
6
+ if ! command -v node >/dev/null 2>&1; then
7
+ echo "node is not on PATH. Install Node.js (>=20) and retry." >&2
8
+ exit 1
9
+ fi
10
+
11
+ exec node "$DIR/supervisor/supervise.mjs" --port "$PORT"
@@ -0,0 +1,238 @@
1
+ # Windows security helper
2
+
3
+ `bin/windows/PoolingSecurityHelper.exe` is an own-source, AnyCPU .NET Framework
4
+ console program. It contains both the native security reader and the operation
5
+ code; there is no companion reader DLL or third-party runtime dependency.
6
+ The `bin/windows/src/` source and build metadata ship alongside the executable.
7
+
8
+ ## Runtime
9
+
10
+ Requires Windows with .NET Framework 4.6.2 or later. Windows 10 1607+, Windows 11,
11
+ and Windows Server 2016+ include that version or newer. Windows PowerShell 5.1
12
+ alone on older Windows does not establish this prerequisite.
13
+ PowerShell is **not** launched by the helper.
14
+ There is no runtime compilation, download, elevation, or policy change.
15
+ AnyCPU uses the installed Framework's process architecture; native calls use
16
+ pointer-sized handles. Windows ARM64 execution has not been validated.
17
+
18
+ The Node caller must launch the package-relative executable directly (no shell)
19
+ with exactly one action argument. Paths are passed in the environment, never in
20
+ arguments. No action enables privileges.
21
+
22
+ ### Batch file operations
23
+
24
+ | Action | Environment | UTF-8 stdin |
25
+ | --- | --- | --- |
26
+ | `inspect-access` | `MCP_POOL_SOURCE` | Unused |
27
+ | `stage` | `MCP_POOL_SOURCE`, `MCP_POOL_TEMP` (new destination) | `{"expected":{"identity":"…","revision":"…","security":"…"},"bytes":"<base64>"}` |
28
+ | `move-no-replace` | `MCP_POOL_SOURCE`, `MCP_POOL_TEMP` (destination) | `{"expected":{"identity":"…","revision":"…","security":"…"}}` |
29
+
30
+ Each successful action writes one JSON line with exactly
31
+ `{identity, revision, security, size}`. Pass only the first three fields back in
32
+ `expected`, not the complete result. `identity` is lowercase hexadecimal
33
+ `volumeSerial:fileIndexHighAndLow` (8 digits, colon, 16 digits). `revision` is
34
+ lowercase SHA256 of the complete bytes; `size` is their integer byte count.
35
+ `security` is a lowercase SHA256 comparison value, not a descriptor to log.
36
+ Custom config paths, including spaces and Unicode, use the same environment API.
37
+
38
+ All three actions require a disk file, no final-component reparse point, exactly
39
+ one hard link, and at most 1 MiB. Only Normal/Archive file attributes are supported;
40
+ read-only, compressed, encrypted, sparse, hidden, and other attributes fail closed.
41
+ The input envelope is limited to 1,400,000 bytes, strict UTF-8 and depth 8.
42
+ Edited bytes must be canonical base64 decoding to at most 1 MiB of UTF-8.
43
+ The helper does not parse the edited configuration; complete JSON validation and
44
+ allowed field edits remain the Node coordinator's responsibility.
45
+
46
+ Native `GetSecurityInfo` queries the held handle with `READ_CONTROL` sections
47
+ `0x1f7`. The new actions never call the legacy privileged audit reader.
48
+ The security hash frames `mcp-access-v1`, native owner SID, group SID,
49
+ DACL-protection bit, raw DACL, native label ACL, and attributes with `BinaryWriter`.
50
+ Byte arrays have signed Int32 lengths (`-1` for null). The discriminator is a
51
+ BinaryWriter UTF-8 string; protection and attributes are UInt32, all little-endian.
52
+ The DACL's `SE_DACL_AUTO_INHERITED` (`0x400`) bookkeeping is not hashed: Windows can
53
+ clear it while retaining the exact grants. Defaulted/auto-inherit-request and
54
+ other unsupported descriptor control state is refused. Audit control flags are
55
+ outside this non-audit fingerprint; label ACE bytes and flags remain exact.
56
+
57
+ Supported DACLs have non-null raw ACLs containing ordinary non-callback allow/deny
58
+ ACEs. Unsupported object/callback/policy ACEs fail closed. No label, or one
59
+ low/medium label with a nonzero mask composed only of NW/NR/NX (`1` through `7`),
60
+ is supported. Only the inherited label ACE flag is allowed. Other native non-audit
61
+ policy is refused.
62
+
63
+ `stage` opens the source with actual `FILE_READ_DATA | FILE_WRITE_DATA |
64
+ READ_CONTROL` access. Parent DeleteChild authority does not bypass source write
65
+ denial. The held source handle denies other data writers/deleters during staging.
66
+ Expected identity, bytes, and security are checked before creation and again
67
+ around writing. When the source owner equals the caller's user SID or current
68
+ token default owner, that owner is retained. Retaining an already assignable
69
+ default owner avoids an unnecessary ownership change across a version roundtrip.
70
+ Otherwise the caller must already obtain source `READ_CONTROL | WRITE_DAC` through
71
+ `ReOpenFile` on the same object; only simple allow DACLs without OWNER RIGHTS or
72
+ CREATOR special SIDs qualify. The candidate owner then becomes the creator.
73
+ The source group must remain exact or creation/verification fails.
74
+
75
+ The candidate uses native `CREATE_NEW` with `SECURITY_ATTRIBUTES` containing the
76
+ accepted owner/group/DACL/protection and supported labels **before any bytes**.
77
+ It never clones audit entries or sets SACL protection. A label-bearing creation
78
+ descriptor marks the SACL defaulted (`SE_SACL_DEFAULTED`); this lets Windows inherit
79
+ folder auditing while retaining the supplied mandatory label. The label remains
80
+ part of the exact empty-file verification. Windows inherits the
81
+ destination folder's auditing, including when the source protects its audit ACL.
82
+ File-specific auditing is not detected or promised to carry forward. The UI must
83
+ provide the agreed nonblocking audit-inheritance warning without claiming
84
+ custom rules were detected.
85
+
86
+ The empty candidate's security must equal the accepted fingerprint before
87
+ `WriteFile`. Bytes are written through that owned handle, flushed using
88
+ `FlushFileBuffers`, and then hashed and security-checked again. Security mismatch
89
+ before writing can leave an empty, securely created candidate. A later failure
90
+ can leave a partially/fully written candidate; callers must validate ownership
91
+ before cleanup, never blindly delete a destination on any failure.
92
+
93
+ `move-no-replace` holds a source handle with share-read/share-delete, denies data
94
+ writers, and checks identity/content/security before `MoveFileExW(..., 0)`.
95
+ It checks the destination and held object afterward and returns the moved
96
+ descriptor. Existing destinations are never overwritten. There is no exists-check
97
+ followed by an overwriting rename.
98
+
99
+ **Limits:** the rename API is path-based. Another actor with delete/rename or
100
+ directory/security authority can still swap the source path/ancestors between
101
+ validation and the move, or change security during an operation. Postchecks
102
+ detect some races but cannot undo them. This is not CAS, an atomic two-file
103
+ transaction, a power-loss guarantee, or a complete metadata clone. A failed move
104
+ postcheck can mean the move already happened; the coordinator must inspect
105
+ transaction state before recovery. Deadline/commit gating, rollback/recovery,
106
+ watcher coordination, and the missing-active interval belong to Node.
107
+
108
+ | Exit | New-action meaning |
109
+ | --- | --- |
110
+ | `0` | One descriptor JSON line; empty stderr |
111
+ | `3` | `PoolingConflictException` for an expected/state mismatch, or `Win32Exception` with native `80`/`183` for an existing destination |
112
+ | `1` | Policy, request, I/O, access, or other failure |
113
+
114
+ Failures have empty stdout. Stderr is exactly `MCPERR type=<exception type>` and,
115
+ for a `Win32Exception`, a second line `MCPERR nativeError=<integer>`. Relevant
116
+ types are `PoolingPolicyException` (unsupported or mismatched creation security),
117
+ `PoolingConflictException`, `Win32Exception`, `ArgumentException`,
118
+ `FormatException`, `DecoderFallbackException`, and `IOException`. Native `5` is
119
+ access denied; `32` is a sharing violation; `2`/`3` are missing path components.
120
+ No exception messages, paths, token information, config bytes, or descriptors are
121
+ printed. Legacy stale-security conflicts retain their empty-stderr behavior.
122
+
123
+ ### Legacy compatibility
124
+
125
+ `inspect` and `copy` remain available with their existing contract:
126
+
127
+ | Environment variable | Used by | Value |
128
+ | --- | --- | --- |
129
+ | `MCP_POOL_SOURCE` | Both | Source config path |
130
+ | `MCP_POOL_SECURITY` | Copy | Previously inspected fingerprint |
131
+ | `MCP_POOL_TEMP` | Copy | Existing empty replacement file |
132
+ | `MCP_POOL_BACKUP` | Copy | Existing empty backup file |
133
+
134
+ `inspect` writes one line: `F:<base64 SHA256>` for complete security or
135
+ `P:<base64 SHA256>` when audit read is denied. These are comparison values,
136
+ not log fields. The caller must refuse automatic writes for partial (`P:`)
137
+ state. Successful `copy` writes nothing to stdout.
138
+
139
+ | Exit code | Meaning |
140
+ | --- | --- |
141
+ | 0 | Inspection or copy/verification succeeded |
142
+ | 3 | Source fingerprint changed before or after copy; caller reports conflict |
143
+ | 1 | Invalid input, I/O failure, or security cannot be preserved |
144
+
145
+ On failure, stderr contains only `MCPERR type=<exception type>` and, for native
146
+ errors, `MCPERR nativeError=<integer>`. No path, descriptor, token, config bytes,
147
+ or exception message is logged. Caller owns process timeout and error mapping.
148
+
149
+ Fingerprint framing is unchanged: canonical native descriptor (mask `0x1f7`),
150
+ separate managed audit descriptor, and integer file attributes, written with
151
+ `BinaryWriter` length prefixes before SHA256. Copy preserves owner/group/DACL
152
+ through SDDL and verifies the entire fingerprint on both staging files, then
153
+ rechecks the source. It does not copy unsupported audit/integrity/attribute
154
+ state or weaken it to force success. It never writes config contents.
155
+
156
+ ## Build and verify
157
+
158
+ Prerequisites, installed by the developer or CI image:
159
+
160
+ 1. Visual Studio or Build Tools with MSBuild and its Roslyn `csc.exe`, supporting
161
+ `/deterministic` and `/pathmap`. `vswhere` discovers current releases, including
162
+ Visual Studio 2026; an explicit compiler path is also supported.
163
+ 2. The .NET Framework **4.6.2 targeting pack** (reference assemblies, not a new
164
+ runtime). The script reports missing prerequisites; it installs nothing.
165
+ 3. Windows PowerShell 5.1 or PowerShell 7 for the build script only.
166
+
167
+ From the repository root:
168
+
169
+ ```powershell
170
+ # Update executable and metadata after source changes.
171
+ .\tools\windows-security-helper\build.ps1
172
+
173
+ # Read-only source/toolchain/hash check plus byte-for-byte rebuild.
174
+ .\tools\windows-security-helper\build.ps1 -Verify
175
+
176
+ # Select an already-installed compiler explicitly when reproducing recorded bytes.
177
+ .\tools\windows-security-helper\build.ps1 -Verify -CompilerPath 'C:\path\to\Roslyn\csc.exe'
178
+ ```
179
+
180
+ The build uses `/noconfig /nostdlib+` with explicit `mscorlib.dll`, `System.dll`,
181
+ `System.Core.dll`, and `System.Web.Extensions.dll` references. JSON serialization
182
+ uses .NET Framework's built-in `JavaScriptSerializer`, not an npm dependency.
183
+ Sources are `AssemblyInfo.cs`, `PoolingSecurityReader.cs`,
184
+ `PoolingSecurityHelper.cs`, and `PoolingNativeFiles.cs`, with fixed options,
185
+ LF-normalized UTF-8 source, and mapped source paths.
186
+ It compiles twice under different temporary paths and refuses unequal bytes.
187
+ Temporary build files are removed by explicit path. No `dotnet build`, runtime
188
+ Framework compiler, NuGet restore, or external binary is involved.
189
+
190
+ `PoolingSecurityHelper.build.json` records compiler version/hash, reference
191
+ hashes, options, normalized source/build-script hashes, and executable SHA256.
192
+ `-Verify` checks all inputs and requires rebuilt bytes to match the packaged
193
+ binary. Verification never updates the binary or metadata.
194
+
195
+ Reproducibility is scoped to the **recorded compiler and references**. A newer
196
+ CI image/compiler may produce different bytes; do not treat a changed toolchain
197
+ as verification of the old binary. Use the recorded installed toolchain or
198
+ explicitly rebuild, review, and validate the updated assets. This is integrity
199
+ checking, not code signing or protection against someone replacing all assets.
200
+
201
+ The published executable must contain only the production sources above.
202
+ Audit-denial/delay test variants belong in disposable directories outside `bin/`
203
+ and must never be included in an npm tarball. Release validation must also run
204
+ the real package on supported Windows/Node versions and verify packaged file
205
+ inclusion. This build alone does not establish those platform results.
206
+
207
+ ## Targeted tests
208
+
209
+ ```powershell
210
+ npm exec --no --offline --package=node@20.20.2 -- node --test test/windows-security-helper.test.mjs test/windows-batch-security.test.mjs
211
+ npm exec --no --offline --package=node@22.23.2 -- node --test test/windows-security-helper.test.mjs test/windows-batch-security.test.mjs
212
+ ```
213
+
214
+ The real-helper tests cover descriptor stability, full-byte staging, exact 1 MiB
215
+ bounds, stale identity/revision/security, malformed/bounded input, held writers,
216
+ hard links, and both collision points. There are no successful-path mocks.
217
+
218
+ The isolated `TEST-SETUP` audit fixture requires an audit-reading oracle token
219
+ and an existing Explorer window. Otherwise that one test explicitly skips.
220
+ Explorer discovery first checks for a process in the current Windows session,
221
+ before COM access or fixture policy changes. An absent process/window produces
222
+ a structured capability reason; discovery exceptions and later worker failures
223
+ remain failures. A skip does not establish audit-oracle coverage: that requires
224
+ a non-skipped result on a machine with both prerequisites.
225
+ It launches the **same Node executable** through Explorer, verifies medium
226
+ integrity and absence of SeSecurityPrivilege, then runs the packaged helper
227
+ directly. The elevated oracle separately verifies raw DACL/group/labels,
228
+ owner rules, unchanged failed sources, and the retained original's complete
229
+ audit descriptor. The mixed audit case retains one explicit plus one inherited
230
+ source rule on `previous`; its new candidate has only the parent's inherited
231
+ rule. A protected audit source also permits folder inheritance on the candidate.
232
+ An incompatible inherited label must leave the candidate empty.
233
+
234
+ `test/fixtures/windows-batch-security/FixturePolicy.cs` is compiled only by
235
+ TEST-SETUP to establish synthetic raw DACLs and labels; it is not linked into,
236
+ loaded by, or shipped as a dependency of the production helper. All fixture
237
+ policy changes are confined to a fresh owned temporary directory, removed by
238
+ the test. No live files, tasks, bridge, or token privileges are changed.