deadbyte-mcp 0.15.46 → 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 (98) hide show
  1. package/MANIFEST.SHA256 +97 -45
  2. package/PROVENANCE.json +22 -11
  3. package/README.txt +18 -18
  4. package/RELEASE.json +378 -369
  5. package/SBOM.cdx.json +2 -2
  6. package/TEST-MATRIX.json +2 -2
  7. package/bin/deadbyte-epoch-lock-r85.node +0 -0
  8. package/bin/deadbyte-kernel-lock-r74.exe +0 -0
  9. package/bridge/package.json +1 -1
  10. package/controller/deadbyte-process-policy.json +7 -7
  11. package/docs/R73-CAPABILITY-GAP-MATRIX.json +17 -0
  12. package/docs/R73-EXECUTION-CONTRACT.md +11 -0
  13. package/docs/R73-IMPLEMENTATION-CHANGELOG.md +12 -0
  14. package/docs/R73-REMAINING-DEFECTS.md +9 -0
  15. package/docs/R74-KERNEL-LOCK-BINARY.json +11 -0
  16. package/docs/R75-RECOVERY-HANDOFF.md +16 -0
  17. package/docs/R85-EPOCH-LOCK-BINARY.json +13 -0
  18. package/native/deadbyte-epoch-lock-r85.cpp +241 -0
  19. package/native/deadbyte-kernel-lock-r74.cpp +65 -0
  20. package/package.json +1 -1
  21. package/scripts/build-epoch-lock-r85.ps1 +34 -0
  22. package/scripts/build-kernel-lock-r74.ps1 +28 -0
  23. package/scripts/deploy-r48.mjs +3 -1
  24. package/scripts/gate-windows-r72.ps1 +2 -2
  25. package/scripts/npm-prepublish-contract-r74.mjs +113 -0
  26. package/scripts/npm-release-r48.mjs +24 -4
  27. package/scripts/npm-release-readiness-r108.mjs +134 -0
  28. package/scripts/npm-release-readiness-r74.mjs +46 -0
  29. package/scripts/r73-atomic-benchmark.mjs +39 -0
  30. package/scripts/r75-npm-bin-integrity-gate.mjs +33 -0
  31. package/scripts/release-manifest.mjs +1 -1
  32. package/scripts/verify-r70-full-regression.ps1 +12 -2
  33. package/src/activation-authority-proof-r75.mjs +67 -0
  34. package/src/activation-continuity-engine-r65.mjs +17 -4
  35. package/src/activation-preflight-r65.mjs +13 -0
  36. package/src/atomic-mutation.mjs +99 -9
  37. package/src/authority-continuity-r65.mjs +265 -21
  38. package/src/deadbyte-cli.mjs +55 -18
  39. package/src/epoch-lock-r85.mjs +48 -0
  40. package/src/foreground-marker-cas-win32.mjs +284 -0
  41. package/src/foreground-process-identity.mjs +3 -3
  42. package/src/kernel-lock-r74.mjs +125 -0
  43. package/src/privileged-install-trust-r89.mjs +196 -0
  44. package/src/remote-session-guardian.mjs +9 -7
  45. package/src/runtime-update.mjs +5 -22
  46. package/src/version.mjs +1 -1
  47. package/src/windows-powershell-environment.mjs +24 -0
  48. package/test/atomic-mutation.test.mjs +68 -2
  49. package/test/cli-entrypoint.test.mjs +1 -1
  50. package/test/commander-schema-differential-r48.test.mjs +1 -1
  51. package/test/helpers/r75-authority-proof.mjs +9 -0
  52. package/test/r100-posix-owned-lock-race.test.mjs +72 -0
  53. package/test/r101-acl-probe-environment.test.mjs +51 -0
  54. package/test/r106-trusted-powershell-spawn.test.mjs +53 -0
  55. package/test/r107-updater-powershell-path.test.mjs +77 -0
  56. package/test/r33-closeout-regression.test.mjs +2 -1
  57. package/test/r54-offline-predecessor-self-exclusion.test.mjs +2 -2
  58. package/test/r55-offline-predecessor-guardian-family.test.mjs +2 -2
  59. package/test/r56-rollback-launcher-restore.test.mjs +2 -2
  60. package/test/r57-core-startup-race.test.mjs +2 -2
  61. package/test/r58-concurrent-recovery.test.mjs +10 -7
  62. package/test/r58-stale-owner.test.mjs +5 -6
  63. package/test/r59-tool-surface-compat.test.mjs +1 -1
  64. package/test/r65-activation-orchestration.test.mjs +2 -1
  65. package/test/r65-activation-preflight.test.mjs +7 -6
  66. package/test/r65-authority-epoch-recovery.test.mjs +3 -1
  67. package/test/r65-authority-restored-safe.test.mjs +2 -1
  68. package/test/r65-deploy-preflight.test.mjs +13 -14
  69. package/test/r65-epoch-native-activation-integration.test.mjs +5 -4
  70. package/test/r65-legacy-authority-migration.test.mjs +2 -1
  71. package/test/r67-npm-cross-volume.test.mjs +9 -2
  72. package/test/r71-full-verifier-artifact.test.mjs +21 -15
  73. package/test/r71-isolated-slot-rollback-restore.test.mjs +3 -1
  74. package/test/r71-release-identity.test.mjs +3 -1
  75. package/test/r73-package-boundary.test.mjs +22 -0
  76. package/test/r74-kernel-lock.test.mjs +156 -0
  77. package/test/r74-prepublish-contract.test.mjs +89 -0
  78. package/test/r75-activation-authority-proof.test.mjs +46 -0
  79. package/test/r75-authority-evidence.test.mjs +77 -0
  80. package/test/r76-pinned-marker-cas.test.mjs +78 -0
  81. package/test/r77-epoch-grant-cas.test.mjs +51 -0
  82. package/test/r78-parent-junction-cas.test.mjs +62 -0
  83. package/test/r79-orphan-revocation-reconcile.test.mjs +88 -0
  84. package/test/r79-windows-fsync-regression.test.mjs +45 -0
  85. package/test/r80-durable-epoch-commit.test.mjs +76 -0
  86. package/test/r82-epoch-lock-owned-cas.test.mjs +93 -0
  87. package/test/r83-epoch-lock-fileid-cas.test.mjs +92 -0
  88. package/test/r84-epoch-lock-created-fileid.test.mjs +112 -0
  89. package/test/r85-epoch-lock-release-lifetime.test.mjs +25 -0
  90. package/test/r85-epoch-repeat-append.test.mjs +30 -0
  91. package/test/r86-epoch-cleanup-and-loader.test.mjs +126 -0
  92. package/test/r87-crashed-orphan-revoke-reconcile.test.mjs +110 -0
  93. package/test/r88-native-disposition-failure.test.mjs +35 -0
  94. package/test/r89-privileged-install-boundary.test.mjs +123 -0
  95. package/test/r92-acl-ace-applicability.test.mjs +45 -0
  96. package/test/r94-win32-propagation-enum.test.mjs +51 -0
  97. package/test/r99-privileged-import-closure.test.mjs +45 -0
  98. package/test/remote-session-cli.test.mjs +69 -9
@@ -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 started = Date.now();
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
- await handle.writeFile(`${process.pid}\\n${Date.now()}\\n`);
17
- await handle.sync();
18
- await handle.close();
19
- return async () => { await rm(lock, { force: true }).catch(() => {}); };
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 (Date.now() - started >= LOCK_TIMEOUT_MS) throw new Error('atomic mutation lock timed out');
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) await rm(lock, { force: true }).catch(() => {});
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
  }
@@ -1,5 +1,5 @@
1
1
  import {createHash,createPrivateKey,createPublicKey,randomBytes,sign as cryptoSign,verify as cryptoVerify} from 'node:crypto';
2
- import {mkdir,open,readFile,readdir,rename,rm,writeFile} from 'node:fs/promises';
2
+ import {lstat,mkdir,open,readFile,readdir,rename,rm,writeFile} from 'node:fs/promises';
3
3
  import path from 'node:path';
4
4
  import {canonicalJson} from './canonical-json.mjs';
5
5
  import {decodeUtf8Strict} from './text-policy.mjs';
@@ -271,38 +271,159 @@ function processAlive(pid){
271
271
  if(pid===process.pid)return true;
272
272
  try{process.kill(pid,0);return true;}catch(error){return error?.code==='EPERM';}
273
273
  }
274
- async function atomicJsonWrite(file,value){
274
+ let cachedEpochOwnerIdentity=null;
275
+ async function windowsEpochOwnerIdentity(pid){
276
+ const {queryForegroundIdentity,validForegroundIdentity}=await import('./foreground-process-identity.mjs');
277
+ const identity=queryForegroundIdentity(pid);
278
+ if(identity!==null&&!validForegroundIdentity(identity))throw new Error('authority epoch owner process identity incomplete');
279
+ return identity;
280
+ }
281
+ async function currentEpochOwnerIdentity(){
282
+ if(process.platform!=='win32')return null;
283
+ if(!cachedEpochOwnerIdentity)cachedEpochOwnerIdentity=await windowsEpochOwnerIdentity(process.pid);
284
+ if(!cachedEpochOwnerIdentity||cachedEpochOwnerIdentity.pid!==process.pid)
285
+ throw new Error('authority epoch owner process identity unavailable');
286
+ return cachedEpochOwnerIdentity;
287
+ }
288
+ // R80: A successful file write is not a durability barrier. Flush the exact
289
+ // open file handle BEFORE publishing an event or replacing the signed head.
290
+ // Node on Windows does not expose a verified directory-metadata flush: keep
291
+ // that limitation explicit instead of treating rename() as a power-loss proof.
292
+ export function authorityEpochDurabilityAssessment(){
293
+ return Object.freeze({
294
+ event_file:'FLUSH_REQUIRED',head_temp_file:'FLUSH_REQUIRED',
295
+ directory_metadata:process.platform==='win32'?'UNVERIFIED_ON_WINDOWS':'FSYNC_REQUIRED',
296
+ power_loss:'NOT_VERIFIED_BY_TESTS'
297
+ });
298
+ }
299
+ async function syncDirectoryMetadata(dir){
300
+ if(process.platform==='win32')return false; // No verified Node directory FlushFileBuffers contract.
301
+ const handle=await open(dir,'r');
302
+ try{await handle.sync();}
303
+ finally{await handle.close();}
304
+ return true;
305
+ }
306
+ async function writeDurableEvent(file,bytes,{stage=async()=>{}}={}){
307
+ const handle=await open(file,'wx'); // immutable identity, exclusive creation
308
+ try{
309
+ await stage('event_created');
310
+ await handle.writeFile(bytes);
311
+ await stage('event_written');
312
+ await handle.sync(); // event bytes must be flushed BEFORE head visibility
313
+ await stage('event_file_synced');
314
+ }finally{await handle.close();}
315
+ await syncDirectoryMetadata(path.dirname(file));
316
+ await stage('event_directory_barrier');
317
+ }
318
+ async function atomicJsonWrite(file,value,{stage=async()=>{}}={}){
275
319
  const dir=path.dirname(file);
276
320
  await mkdir(dir,{recursive:true});
277
321
  const tmp=`${file}.tmp-${process.pid}-${randomBytes(8).toString('hex')}`;
278
322
  try{
279
- await writeFile(tmp,canonicalJson(value),{encoding:'utf8',flag:'wx'});
323
+ const handle=await open(tmp,'wx');
324
+ try{
325
+ await stage('head_temp_created');
326
+ await handle.writeFile(Buffer.from(canonicalJson(value),'utf8'));
327
+ await stage('head_temp_written');
328
+ await handle.sync(); // temp head content flushed BEFORE rename
329
+ await stage('head_temp_file_synced');
330
+ }finally{await handle.close();}
280
331
  await rename(tmp,file);
332
+ await stage('head_renamed'); // rename succeeded: caller outcome may now be ambiguous
333
+ await syncDirectoryMetadata(dir);
334
+ await stage('head_directory_barrier');
281
335
  }finally{
282
336
  await rm(tmp,{force:true}).catch(()=>{});
283
337
  }
284
338
  }
285
- async function acquireEpochLock(dir){
339
+ // R82: On Windows, lock reclamation/release MUST be conditional on the
340
+ // exact observed lock bytes and must dispose through one pinned native handle.
341
+ // The R78 implementation also pins every non-reparse ancestor directory.
342
+ // R83: Windows lock snapshots now bind owner bytes + native FileIdInfo to
343
+ // the same pinned handle. The later pinned deletion compares BOTH identity
344
+ // and content before disposition, defeating byte-identical file replacement.
345
+ // POSIX remains legacy/unsupported against adversarial lock-path replacement.
346
+ async function epochLockSnapshot(lockPath){
347
+ if(process.platform==='win32'){
348
+ const {readPinnedEpochLockSnapshot}=await import('./foreground-marker-cas-win32.mjs');
349
+ return readPinnedEpochLockSnapshot(lockPath);
350
+ }
351
+ try{return {bytes:await readFile(lockPath),fileId:null};}
352
+ catch(error){if(error?.code==='ENOENT')return null;throw error;}
353
+ }
354
+ async function disposeObservedEpochLock(lockPath,snapshot){
355
+ if(process.platform==='win32'){
356
+ if(!snapshot?.fileId)throw new Error('epoch lock native identity snapshot is required');
357
+ const {deletePinnedRemoteMarker}=await import('./foreground-marker-cas-win32.mjs');
358
+ return deletePinnedRemoteMarker(lockPath,hashBytes(snapshot.bytes),{expectedFileId:snapshot.fileId});
359
+ }
360
+ throw new Error('authority epoch POSIX lock disposition unsupported');
361
+ }
362
+
363
+ async function acquireEpochLock(dir,{allowStaleRecovery=true,onLockDispositionStage=null}={}){
364
+ // POSIX open('wx') protects creation, but a later read/check followed by
365
+ // unlink(path) cannot atomically prove the pathname still denotes our lock.
366
+ // Until a kernel-held, identity-bound POSIX protocol exists, no authority
367
+ // epoch mutation may start there. Read-only head verification remains usable.
368
+ if(process.platform!=='win32')
369
+ throw new Error('authority epoch POSIX mutation unsupported: lock pathname cannot be disposed atomically');
286
370
  await mkdir(dir,{recursive:true});
287
371
  const lockPath=path.join(dir,'.lock');
372
+ const stage=async label=>{if(onLockDispositionStage)await onLockDispositionStage(label,lockPath);};
288
373
  for(let attempt=0;attempt<2;attempt++){
289
374
  const token=randomBytes(16).toString('hex');
290
375
  try{
291
- const handle=await open(lockPath,'wx');
292
- try{await handle.writeFile(canonicalJson({schema:'deadbyte.authority-epoch-lock.r65.v1',pid:process.pid,token,created_at_utc:new Date().toISOString()}),'utf8');}
293
- finally{await handle.close();}
294
- return async()=>{
295
- try{
296
- const current=JSON.parse(await readFile(lockPath,'utf8'));
297
- if(current?.token===token)await rm(lockPath,{force:true});
298
- }catch(error){if(error?.code!=='ENOENT')throw error;}
376
+ const ownerIdentity=await currentEpochOwnerIdentity();
377
+ const lockBytes=Buffer.from(canonicalJson({schema:'deadbyte.authority-epoch-lock.r65.v1',pid:process.pid,token,
378
+ created_at_utc:new Date().toISOString(),...(ownerIdentity?{owner_identity:ownerIdentity}:{})}),'utf8');
379
+ let nativeLease=null;
380
+ if(process.platform==='win32'){
381
+ const {acquireHeldEpochLock}=await import('./epoch-lock-r85.mjs');
382
+ nativeLease=await acquireHeldEpochLock(lockPath,lockBytes);
383
+ if(!nativeLease)throw Object.assign(new Error('authority epoch lock exists'),{code:'EEXIST'});
384
+ }else throw new Error('authority epoch POSIX mutation unsupported');
385
+ const verifyOwned=async()=>{
386
+ // On Windows the in-process native handle holds the leaf and every
387
+ // non-reparse ancestor through the mutation. No pathname recheck can
388
+ // improve that atomic ownership guarantee.
389
+ if(!nativeLease)throw new Error('authority epoch lock ownership unverified');
390
+ return null;
391
+ };
392
+ const release=async()=>{
393
+ if(nativeLease){
394
+ let stageError=null;
395
+ try{await stage('release_before_cas');}catch(error){stageError=error;}
396
+ let releaseError=null;
397
+ try{await nativeLease.release();}catch(error){releaseError=error;}
398
+ if(stageError&&releaseError)throw new AggregateError([stageError,releaseError],
399
+ 'authority epoch release observer and native cleanup both failed');
400
+ if(stageError)throw stageError;
401
+ if(releaseError)throw releaseError;
402
+ return;
403
+ }
404
+ throw new Error('authority epoch lock release without native ownership denied');
299
405
  };
406
+ release.verifyOwned=verifyOwned;
407
+ return release;
300
408
  }catch(error){
301
409
  if(error?.code!=='EEXIST')throw error;
410
+ const snapshot=await epochLockSnapshot(lockPath);
302
411
  let owner=null;
303
- try{owner=JSON.parse(await readFile(lockPath,'utf8'));}catch{}
304
- if(attempt===0&&owner&&Number.isInteger(owner.pid)&&!processAlive(owner.pid)){
305
- await rm(lockPath,{force:true});
412
+ try{if(snapshot)owner=JSON.parse(snapshot.bytes.toString('utf8'));}catch{}
413
+ if(attempt===0&&allowStaleRecovery&&snapshot&&owner&&Number.isInteger(owner.pid)&&owner.pid>0){
414
+ if(process.platform!=='win32')throw new Error('authority epoch mutation locked (POSIX automatic recovery disabled)');
415
+ const {validForegroundIdentity,foregroundIdentityMatches}=await import('./foreground-process-identity.mjs');
416
+ if(!validForegroundIdentity(owner.owner_identity)||owner.owner_identity.pid!==owner.pid)
417
+ throw new Error('authority epoch mutation locked (owner identity unverified)');
418
+ const observed=await windowsEpochOwnerIdentity(owner.pid);
419
+ if(observed){
420
+ if(!foregroundIdentityMatches(owner.owner_identity,observed))
421
+ throw new Error('authority epoch mutation locked (owner process birth changed)');
422
+ throw new Error('authority epoch mutation locked (owner still active)');
423
+ }
424
+ await stage('stale_before_cas');
425
+ const disposition=await disposeObservedEpochLock(lockPath,snapshot);
426
+ if(disposition!=='REMOVED')throw new Error(`authority epoch mutation locked (stale owner changed: ${disposition})`);
306
427
  continue;
307
428
  }
308
429
  throw new Error('authority epoch mutation locked');
@@ -310,7 +431,7 @@ async function acquireEpochLock(dir){
310
431
  }
311
432
  throw new Error('authority epoch mutation locked');
312
433
  }
313
- async function readDurableHead(runtimeDir,plane){
434
+ async function readDurableHead(runtimeDir,plane,{recoveryOrphanFile=null}={}){
314
435
  const dir=durableEpochDir(runtimeDir,plane),headPath=path.join(dir,'head.json');
315
436
  let head;
316
437
  try{head=JSON.parse(await readFile(headPath,'utf8'));}
@@ -342,7 +463,7 @@ async function readDurableHead(runtimeDir,plane){
342
463
  const match=/^(\d{16})-([0-9a-f]{64})\.json$/.exec(name);
343
464
  if(!match)throw new Error('authority epoch unexpected event file');
344
465
  const seq=Number(match[1]);
345
- if(!Number.isSafeInteger(seq)||seq>head.seq||(seq===head.seq&&name!==head.event_file))
466
+ if(!Number.isSafeInteger(seq)||(seq>head.seq&&name!==recoveryOrphanFile)||(seq===head.seq&&name!==head.event_file))
346
467
  throw new Error('authority epoch head replay or incomplete write');
347
468
  }
348
469
  const eventBytes=eventRead.bytes;
@@ -379,14 +500,134 @@ async function readDurableHead(runtimeDir,plane){
379
500
  return Object.freeze({...head});
380
501
  }
381
502
 
382
- export function createAuthorityEpochStore({runtimeDir,policyBytes=null}={}){
503
+ // R79: An orphan event is not a committed head. Ordinary reads, verification
504
+ // and appends MUST keep rejecting it. Only explicit revocation-only recovery
505
+ // can use the bounded exception to inspect an older, still-signed head.
506
+ async function inspectOrphanRevocation(runtimeDir,plane,{expectedHeadSha256,expectedOrphanSha256}){
507
+ if(!SCHEMAS[plane]||!SHAPAT.test(String(expectedHeadSha256))||!SHAPAT.test(String(expectedOrphanSha256)))
508
+ throw new Error('authority epoch recovery requires exact signed event identities');
509
+ const dir=durableEpochDir(runtimeDir,plane),events=path.join(dir,'events');
510
+ // Reject known static reparse/junction boundaries. Handle-pinned directory
511
+ // protection for concurrent substitutions is a separate Windows requirement.
512
+ for(const parent of [path.resolve(runtimeDir),path.join(path.resolve(runtimeDir),'authority-epochs'),dir,events]){
513
+ const st=await lstat(parent);
514
+ if(!st.isDirectory()||st.isSymbolicLink())throw new Error('authority epoch recovery directory identity invalid');
515
+ }
516
+ const headPath=path.join(dir,'head.json');
517
+ const headStat=await lstat(headPath);
518
+ if(!headStat.isFile()||headStat.isSymbolicLink())throw new Error('authority epoch recovery head identity invalid');
519
+ const rawHead=await readFile(headPath);
520
+ let untrusted;
521
+ try{untrusted=JSON.parse(decodeUtf8Strict(rawHead));}catch{throw new Error('authority epoch recovery head invalid');}
522
+ if(!Number.isSafeInteger(untrusted?.seq)||untrusted.seq<1)
523
+ throw new Error('authority epoch recovery head invalid');
524
+ const names=await readdir(events);
525
+ // Do not accept more than one event beyond the old head, even if signed.
526
+ const orphanNames=names.filter(name=>{
527
+ const m=/^(\d{16})-([0-9a-f]{64})\.json$/.exec(name);
528
+ return m&&Number(m[1])>untrusted.seq;
529
+ });
530
+ if(orphanNames.length!==1)throw new Error('authority epoch recovery requires exactly one orphan event');
531
+ const orphanFile=orphanNames[0];
532
+ if(orphanFile!==durableEventName(untrusted.seq+1,expectedOrphanSha256))
533
+ throw new Error('authority epoch recovery orphan identity mismatch');
534
+ const previous=await readDurableHead(runtimeDir,plane,{recoveryOrphanFile:orphanFile});
535
+ if(!previous||previous.event_sha256!==expectedHeadSha256||previous.seq!==untrusted.seq)
536
+ throw new Error('authority epoch recovery previous head mismatch');
537
+ if(!['grant','transaction_restore','transaction_revoke'].includes(previous.action))
538
+ throw new Error('authority epoch recovery previous action invalid');
539
+ const orphanPath=path.join(events,orphanFile);
540
+ const orphanStat=await lstat(orphanPath);
541
+ if(!orphanStat.isFile()||orphanStat.isSymbolicLink())throw new Error('authority epoch recovery orphan file identity invalid');
542
+ const orphanBytes=await readFile(orphanPath);
543
+ if(hashBytes(orphanBytes)!==expectedOrphanSha256)
544
+ throw new Error('authority epoch recovery orphan bytes mismatch');
545
+ let event;
546
+ try{event=JSON.parse(decodeUtf8Strict(orphanBytes));}catch{throw new Error('authority epoch recovery orphan invalid');}
547
+ const {publicKey,keyId}=await durableEpochKeys(runtimeDir);
548
+ if(!verifyDurableEvent(event,publicKey,keyId))
549
+ throw new Error('authority epoch recovery orphan signature invalid');
550
+ if(event.schema!==DURABLE_EPOCH_SCHEMA||event.plane!==plane||event.seq!==previous.seq+1||
551
+ !['transaction_revoke','user_revoke'].includes(event.action)||
552
+ (event.action==='transaction_revoke'&&!/^[A-Za-z0-9._-]{1,96}$/.test(String(event.update_id??'')))||
553
+ (event.action==='user_revoke'&&event.update_id!==null)||
554
+ event.previous_event_sha256!==previous.event_sha256||
555
+ event.grant_sha256!==previous.grant_sha256||event.grant_instance_nonce!==previous.grant_instance_nonce||
556
+ event.policy_sha256!==previous.policy_sha256||event.policy_envelope_sha256!==previous.policy_envelope_sha256||
557
+ event.restore_binding!==null||
558
+ (event.action==='transaction_revoke'&&!['grant','transaction_restore'].includes(previous.action)))
559
+ throw new Error('authority epoch recovery orphan revocation binding invalid');
560
+ // Head cannot be promoted unless it is still exactly the same committed
561
+ // predecessor. No authority-creating action is ever eligible.
562
+ const replay=await readFile(path.join(dir,'head.json'));
563
+ if(!replay.equals(rawHead))throw new Error('authority epoch recovery head changed');
564
+ const newHead={schema:DURABLE_HEAD_SCHEMA,plane,seq:event.seq,action:event.action,
565
+ update_id:event.update_id,grant_sha256:event.grant_sha256,
566
+ grant_instance_nonce:event.grant_instance_nonce,restore_binding:null,
567
+ ...(event.policy_sha256===undefined?{}:{policy_sha256:event.policy_sha256,policy_envelope_sha256:event.policy_envelope_sha256}),
568
+ previous_event_sha256:event.previous_event_sha256,event_sha256:expectedOrphanSha256,event_file:orphanFile};
569
+ return Object.freeze({previous,newHead,headBytes:rawHead,orphanBytes});
570
+ }
571
+
572
+ export function createAuthorityEpochStore({runtimeDir,policyBytes=null,onDurabilityStage=null,onLockDispositionStage=null}={}){
383
573
  const root=path.resolve(String(runtimeDir??''));
384
574
  if(!runtimeDir)throw new Error('authority epoch runtime directory invalid');
385
575
  if(policyBytes!==null&&!Buffer.isBuffer(policyBytes))throw new Error('authority epoch policy bytes invalid');
576
+ if(onDurabilityStage!==null&&(typeof onDurabilityStage!=='function'||process.env.NODE_TEST_CONTEXT!=='child-v8'))
577
+ throw new Error('authority epoch stage observer is restricted to node test workers');
578
+ if(onLockDispositionStage!==null&&(typeof onLockDispositionStage!=='function'||process.env.NODE_TEST_CONTEXT!=='child-v8'))
579
+ throw new Error('authority epoch lock observer is restricted to node test workers');
580
+ // Node-test-worker-only fault observer. It can abort, never bypass, flush/order checks.
581
+ const stage=async name=>{if(onDurabilityStage)await onDurabilityStage(name);};
386
582
  return Object.freeze({
387
583
  async readHead({plane}){
388
584
  return readDurableHead(root,plane);
389
585
  },
586
+ async reconcileOrphanedRevocation({plane,expectedHeadSha256,expectedOrphanSha256,operatorConfirmed=false}){
587
+ // NOT called from grant verification, append, startup or automatic
588
+ // recovery. The operator must supply two observed event identities.
589
+ if(operatorConfirmed!==true)throw new Error('authority epoch recovery requires explicit operator confirmation');
590
+ const dir=durableEpochDir(root,plane);
591
+ // Reject wrong or stale operator evidence before even reclaiming a dead
592
+ // owner's lock. This is a read-only preflight, not an ownership proof;
593
+ // the same exact evidence is independently rechecked under the lock.
594
+ await inspectOrphanRevocation(root,plane,{expectedHeadSha256,expectedOrphanSha256});
595
+ // Explicit, identity-bound revocation reconciliation may reclaim a
596
+ // verified dead owner's lock through the same native stale-owner CAS as
597
+ // append. A live or unverified owner still fails closed. The orphan
598
+ // itself is inspected only after exclusive ownership is established.
599
+ const release=await acquireEpochLock(dir,{allowStaleRecovery:true,onLockDispositionStage});
600
+ try{
601
+ const {newHead,headBytes,orphanBytes}=await inspectOrphanRevocation(root,plane,{expectedHeadSha256,expectedOrphanSha256});
602
+ // Windows FlushFileBuffers requires a writable file handle. The old
603
+ // read-only ('r') handle returned EPERM on Windows even for a valid,
604
+ // signed revocation. 'r+' grants the handle sync rights; we do not
605
+ // write event bytes. Revalidate exact bytes through this same handle
606
+ // before syncing, refusing a replaced or modified orphan event.
607
+ // Directory/power-loss durability is NOT proven here.
608
+ const fh=await open(path.join(dir,'events',newHead.event_file),'r+');
609
+ try{
610
+ const st=await fh.stat();
611
+ if(!st.isFile()||st.size!==orphanBytes.length)
612
+ throw new Error('authority epoch recovery pinned orphan identity invalid');
613
+ const pinnedBytes=await fh.readFile();
614
+ if(!pinnedBytes.equals(orphanBytes)||hashBytes(pinnedBytes)!==expectedOrphanSha256)
615
+ throw new Error('authority epoch recovery orphan changed before sync');
616
+ await fh.sync();
617
+ }finally{await fh.close();}
618
+ if(!(await readFile(path.join(dir,'head.json'))).equals(headBytes))
619
+ throw new Error('authority epoch recovery head changed before commit');
620
+ await release.verifyOwned();
621
+ await atomicJsonWrite(path.join(dir,'head.json'),newHead,{stage});
622
+ const committed=await readDurableHead(root,plane);
623
+ if(committed.event_sha256!==expectedOrphanSha256||
624
+ !['transaction_revoke','user_revoke'].includes(committed.action))
625
+ throw new Error('authority epoch recovered head re-verification failed');
626
+ return Object.freeze({status:'REVOKED_ONLY_RECONCILED',plane,seq:committed.seq,
627
+ action:committed.action,event_sha256:committed.event_sha256,
628
+ standing_grant_authorized:false});
629
+ }finally{await release();}
630
+ },
390
631
  async append({plane,action,grantBytes,grantInstanceNonce,updateId=null,expectedHeadSha256=undefined,restoreBinding=null}){
391
632
  const transactional=action?.startsWith('transaction_')===true;
392
633
  if(!SCHEMAS[plane]||!DURABLE_EPOCH_ACTIONS.has(action)||!Buffer.isBuffer(grantBytes)||
@@ -410,7 +651,7 @@ export function createAuthorityEpochStore({runtimeDir,policyBytes=null}={}){
410
651
  policyBinding={policy_sha256:policySha256,policy_envelope_sha256:deriveSecurityEnvelope(plane,parsedPolicy).sha256};
411
652
  }
412
653
  const dir=durableEpochDir(root,plane),events=path.join(dir,'events');
413
- const release=await acquireEpochLock(dir);
654
+ const release=await acquireEpochLock(dir,{onLockDispositionStage});
414
655
  try{
415
656
  await mkdir(events,{recursive:true});
416
657
  const previous=await readDurableHead(root,plane);
@@ -445,12 +686,15 @@ export function createAuthorityEpochStore({runtimeDir,policyBytes=null}={}){
445
686
  const event={...body,attestation:{algorithm:'ed25519',domain:'DEADBYTE-AUTHORITY-DURABLE-EPOCH-R65-V1',key_id:keyId,signature_base64:signature.toString('base64')}};
446
687
  const eventBytes=Buffer.from(canonicalJson(event),'utf8'),eventSha256=hashBytes(eventBytes);
447
688
  const eventFile=durableEventName(seq,eventSha256);
448
- await writeFile(path.join(events,eventFile),eventBytes,{flag:'wx'});
689
+ await writeDurableEvent(path.join(events,eventFile),eventBytes,{stage});
449
690
  const head={schema:DURABLE_HEAD_SCHEMA,plane,seq,action,update_id:updateId,
450
691
  grant_sha256:grantSha256,grant_instance_nonce:grantInstanceNonce,restore_binding:normalizedRestoreBinding,
451
692
  ...(policyBinding??{}),
452
693
  previous_event_sha256:event.previous_event_sha256,event_sha256:eventSha256,event_file:eventFile};
453
- await atomicJsonWrite(path.join(dir,'head.json'),head);
694
+ // A replaced lock observed before publication must not authorize a
695
+ // head commit. This narrows the window but is not a held-handle lock.
696
+ await release.verifyOwned();
697
+ await atomicJsonWrite(path.join(dir,'head.json'),head,{stage});
454
698
  return Object.freeze({...head});
455
699
  }finally{await release();}
456
700
  },
@@ -1,6 +1,6 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
  import { closeSync, openSync } from 'node:fs';
3
- import { lstat, mkdir, open, readFile, readdir, rm, stat, writeFile } from 'node:fs/promises';
3
+ import { lstat, mkdir, open, readFile, readdir, stat, writeFile } from 'node:fs/promises';
4
4
  import { spawn, spawnSync } from 'node:child_process';
5
5
  import { createHash, randomBytes } from 'node:crypto';
6
6
  import os from 'node:os';
@@ -22,6 +22,7 @@ import { ensureRuntimeCoherence } from './runtime-coherence.mjs';
22
22
  import { windowsPowerShellEnvironment } from './runtime-update.mjs';
23
23
  import { resolveStateRoot } from './state-root.mjs';
24
24
  import {foregroundIdentityMatches,queryForegroundIdentity,validForegroundIdentity} from './foreground-process-identity.mjs';
25
+ import {readRemoteMarkerSnapshot,deletePinnedRemoteMarker} from './foreground-marker-cas-win32.mjs';
25
26
  import {acquireForegroundSessionLock} from './foreground-session-lock.mjs';
26
27
  import { AUTHORITY_PLANES, authorityVector, authorityRestorePlan, parseAuthorityObservation, assertAuthorityVector, assertRuntimeReadyObservation, authorityBanner } from './remote-authority.mjs';
27
28
  import { decodeRemoteLogBytes, boundedRemoteDiagnostic, safeRemoteLogPrefixLength } from './remote-log-decode-r60.mjs';
@@ -456,8 +457,9 @@ async function recoverRemoteSession(ctx,{displayMode='unicode'}={}){
456
457
  const ownerPid=remoteOwnerPid(prior);
457
458
  if(pidAlive(ownerPid)){
458
459
  if(!prior.owner_identity)throw new Error(`remote session ownership unverified for legacy pid=${ownerPid}; marker retained`);
459
- if(prior.owner_identity.pid!==ownerPid)throw new Error('remote session owner identity PID binding mismatch');
460
- if(foregroundIdentityMatches(prior.owner_identity,queryForegroundIdentity(ownerPid)))throw new Error(`remote session already active pid=${ownerPid}`);
460
+ if(prior.owner_identity.pid!==ownerPid)throw new Error('remote session owner identity PID binding mismatch; marker retained');
461
+ if(!foregroundIdentityMatches(prior.owner_identity,queryForegroundIdentity(ownerPid)))throw new Error('remote session owner identity mismatch; marker retained');
462
+ throw new Error(`remote session already active pid=${ownerPid}`);
461
463
  }
462
464
  let guardianAlive=prior.schema==='deadbyte.remote-session.v2'&&pidAlive(prior.guardian_pid);
463
465
  if(guardianAlive){
@@ -475,9 +477,7 @@ async function recoverRemoteSession(ctx,{displayMode='unicode'}={}){
475
477
  }
476
478
  throw new Error(`remote session guardian cleanup still active pid=${prior.guardian_pid}`);
477
479
  }
478
- writeRemote(process.stdout,`${remoteTag('recovery',displayMode)} Stale remote session pid=${ownerPid}; revoking authority before restart.\n`,displayMode);
479
- await cleanupRemoteSession(ctx,{displayMode});
480
- await rm(remoteSessionMarker(ctx.root),{force:true});
480
+ throw new Error(`remote session ownership unverified for stale pid=${ownerPid}; marker retained`);
481
481
  }
482
482
  async function claimRemoteSession(ctx){
483
483
  const dir=path.join(ctx.root,'runtime');await mkdir(dir,{recursive:true});
@@ -505,14 +505,28 @@ async function claimRemoteSession(ctx){
505
505
  return marker;
506
506
  }catch(error){
507
507
  try{guardian?.kill();}catch{}
508
- await rm(markerPath,{force:true});
508
+ try{
509
+ const snapshot=await readRemoteMarkerSnapshot(markerPath);
510
+ if(!snapshot||snapshot.marker?.owner_pid!==marker.owner_pid||snapshot.marker?.session_nonce!==marker.session_nonce)
511
+ throw new Error('claim marker ownership changed or missing');
512
+ if(deletePinnedRemoteMarker(markerPath,snapshot.sha256)!=='REMOVED')
513
+ throw new Error('claim marker changed before native deletion');
514
+ }catch(cleanupError){
515
+ throw new AggregateError([error,cleanupError],
516
+ 'remote session claim failed; marker cleanup unverified and marker retained');
517
+ }
509
518
  throw error;
510
519
  }
511
520
  }
512
521
  async function releaseRemoteSession(ctx,marker,{displayMode='unicode'}={}){
513
- let current=null;try{current=await readRemoteSessionMarker(ctx.root);}catch(error){writeRemote(process.stderr,`${remoteTag('warn',displayMode)} remote session marker cleanup: ${String(error?.message||error)}\n`,displayMode);return;}
522
+ let snapshot;
523
+ try{snapshot=await readRemoteMarkerSnapshot(remoteSessionMarker(ctx.root));}
524
+ catch(error){writeRemote(process.stderr,`${remoteTag('warn',displayMode)} remote session marker cleanup: ${String(error?.message||error)}\n`,displayMode);throw new Error(`remote session ownership marker retained: ${String(error?.message||error)}`);}
525
+ const current=snapshot?.marker;
514
526
  const same=current?.schema==='deadbyte.remote-session.v2'&&current.owner_pid===marker.owner_pid&&current.session_nonce===marker.session_nonce;
515
- if(same) await rm(remoteSessionMarker(ctx.root),{force:true});
527
+ if(!same)throw new Error('remote session ownership marker changed or missing; marker retained');
528
+ if(deletePinnedRemoteMarker(remoteSessionMarker(ctx.root),snapshot.sha256)!=='REMOVED')
529
+ throw new Error('remote session ownership marker replaced before native deletion; marker retained');
516
530
  }
517
531
  async function runRemoteSession({resumeCursor=null,verbose=false,trace=false,filterExpression='',mode='default'}={}){
518
532
  const display=prepareRemoteConsole();
@@ -531,10 +545,21 @@ async function runRemoteSession({resumeCursor=null,verbose=false,trace=false,fil
531
545
  }finally{await startupLock.release();}
532
546
  try{
533
547
  await synchronizeForegroundRuntime(ctx,{displayMode});
534
- }catch(error){
535
- await cleanupRemoteSession(ctx,{displayMode});
536
- await releaseRemoteSession(ctx,marker,{displayMode});
537
- throw error;
548
+ }catch(primaryError){
549
+ let cleanupError=null;
550
+ try{await cleanupRemoteSession(ctx,{displayMode});}
551
+ catch(error){cleanupError=error;}
552
+ if(!cleanupError){
553
+ try{await releaseRemoteSession(ctx,marker,{displayMode});}
554
+ catch(error){cleanupError=error;}
555
+ }
556
+ if(cleanupError){
557
+ const primaryDetail=String(primaryError?.message||primaryError);
558
+ const cleanupDetail=String(cleanupError?.message||cleanupError);
559
+ throw new AggregateError([primaryError,cleanupError],
560
+ `remote activation failed: ${primaryDetail}; cleanup failed, ownership marker retained: ${cleanupDetail}`);
561
+ }
562
+ throw primaryError;
538
563
  }
539
564
  const resultJournal=await openRemoteResultJournal({
540
565
  filePath:remoteResultJournalPath(ctx.root,marker.result_session_id),sessionId:marker.result_session_id,maxEntries:2048
@@ -550,6 +575,7 @@ async function runRemoteSession({resumeCursor=null,verbose=false,trace=false,fil
550
575
  if(watchStdin){process.stdin.once('end',abort);process.stdin.once('close',abort);process.stdin.resume();}
551
576
  let ready=false;
552
577
  let logTask=null;
578
+ let sessionError=null;
553
579
  try{
554
580
  process.stdout.write('DEADBYTE MCP\n');
555
581
  process.stdout.write(`${formatRemoteLifecycle('device_start',{displayMode})}\n`);
@@ -609,8 +635,9 @@ async function runRemoteSession({resumeCursor=null,verbose=false,trace=false,fil
609
635
  process.stdout.write('[OK] ONLINE\n');
610
636
  process.stdout.write(`${formatRemoteLifecycle('ready',{displayMode})}\n`);
611
637
  if(url) writeRemote(process.stdout,`URL: ${url}\n`,displayMode);
612
- await logTask;
613
- }finally{
638
+ await logTask;
639
+ }catch(error){sessionError=error;throw error;}
640
+ finally{
614
641
  const wasAborted=aborter.signal.aborted;
615
642
  aborter.abort();
616
643
  if(logTask) await logTask.catch(()=>{});
@@ -623,8 +650,18 @@ async function runRemoteSession({resumeCursor=null,verbose=false,trace=false,fil
623
650
  if(watchStdin){process.stdin.removeListener('end',abort);process.stdin.removeListener('close',abort);process.stdin.pause();}
624
651
  process.stdout.write(`${formatRemoteLifecycle('cleanup',{displayMode})}\n`);
625
652
  if(ready||!wasAborted) process.stdout.write('[INFO] Disconnecting; revoking authority...\n');
626
- await cleanupRemoteSession(ctx,{displayMode});
627
- await releaseRemoteSession(ctx,marker,{displayMode});
653
+ try{await cleanupRemoteSession(ctx,{displayMode});}
654
+ catch(cleanupError){
655
+ if(sessionError) throw new AggregateError([sessionError,cleanupError],
656
+ `remote activation failed: ${String(sessionError?.message||sessionError)}; cleanup failed, ownership marker retained: ${String(cleanupError?.message||cleanupError)}`);
657
+ throw cleanupError;
658
+ }
659
+ try{await releaseRemoteSession(ctx,marker,{displayMode});}
660
+ catch(markerError){
661
+ if(sessionError)throw new AggregateError([sessionError,markerError],
662
+ `remote activation failed: ${String(sessionError?.message||sessionError)}; marker cleanup failed: ${String(markerError?.message||markerError)}`);
663
+ throw markerError;
664
+ }
628
665
  }
629
666
  }
630
667
  async function runRemote(args){