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.
- package/MANIFEST.SHA256 +97 -45
- package/PROVENANCE.json +22 -11
- package/README.txt +18 -18
- package/RELEASE.json +378 -369
- package/SBOM.cdx.json +2 -2
- package/TEST-MATRIX.json +2 -2
- package/bin/deadbyte-epoch-lock-r85.node +0 -0
- package/bin/deadbyte-kernel-lock-r74.exe +0 -0
- package/bridge/package.json +1 -1
- package/controller/deadbyte-process-policy.json +7 -7
- package/docs/R73-CAPABILITY-GAP-MATRIX.json +17 -0
- package/docs/R73-EXECUTION-CONTRACT.md +11 -0
- package/docs/R73-IMPLEMENTATION-CHANGELOG.md +12 -0
- package/docs/R73-REMAINING-DEFECTS.md +9 -0
- package/docs/R74-KERNEL-LOCK-BINARY.json +11 -0
- package/docs/R75-RECOVERY-HANDOFF.md +16 -0
- package/docs/R85-EPOCH-LOCK-BINARY.json +13 -0
- package/native/deadbyte-epoch-lock-r85.cpp +241 -0
- package/native/deadbyte-kernel-lock-r74.cpp +65 -0
- package/package.json +1 -1
- package/scripts/build-epoch-lock-r85.ps1 +34 -0
- package/scripts/build-kernel-lock-r74.ps1 +28 -0
- package/scripts/deploy-r48.mjs +3 -1
- package/scripts/gate-windows-r72.ps1 +2 -2
- package/scripts/npm-prepublish-contract-r74.mjs +113 -0
- package/scripts/npm-release-r48.mjs +24 -4
- package/scripts/npm-release-readiness-r108.mjs +134 -0
- package/scripts/npm-release-readiness-r74.mjs +46 -0
- package/scripts/r73-atomic-benchmark.mjs +39 -0
- package/scripts/r75-npm-bin-integrity-gate.mjs +33 -0
- package/scripts/release-manifest.mjs +1 -1
- package/scripts/verify-r70-full-regression.ps1 +12 -2
- package/src/activation-authority-proof-r75.mjs +67 -0
- package/src/activation-continuity-engine-r65.mjs +17 -4
- package/src/activation-preflight-r65.mjs +13 -0
- package/src/atomic-mutation.mjs +99 -9
- package/src/authority-continuity-r65.mjs +265 -21
- package/src/deadbyte-cli.mjs +55 -18
- package/src/epoch-lock-r85.mjs +48 -0
- package/src/foreground-marker-cas-win32.mjs +284 -0
- package/src/foreground-process-identity.mjs +3 -3
- package/src/kernel-lock-r74.mjs +125 -0
- package/src/privileged-install-trust-r89.mjs +196 -0
- package/src/remote-session-guardian.mjs +9 -7
- package/src/runtime-update.mjs +5 -22
- package/src/version.mjs +1 -1
- package/src/windows-powershell-environment.mjs +24 -0
- package/test/atomic-mutation.test.mjs +68 -2
- package/test/cli-entrypoint.test.mjs +1 -1
- package/test/commander-schema-differential-r48.test.mjs +1 -1
- package/test/helpers/r75-authority-proof.mjs +9 -0
- package/test/r100-posix-owned-lock-race.test.mjs +72 -0
- package/test/r101-acl-probe-environment.test.mjs +51 -0
- package/test/r106-trusted-powershell-spawn.test.mjs +53 -0
- package/test/r107-updater-powershell-path.test.mjs +77 -0
- package/test/r33-closeout-regression.test.mjs +2 -1
- package/test/r54-offline-predecessor-self-exclusion.test.mjs +2 -2
- package/test/r55-offline-predecessor-guardian-family.test.mjs +2 -2
- package/test/r56-rollback-launcher-restore.test.mjs +2 -2
- package/test/r57-core-startup-race.test.mjs +2 -2
- package/test/r58-concurrent-recovery.test.mjs +10 -7
- package/test/r58-stale-owner.test.mjs +5 -6
- package/test/r59-tool-surface-compat.test.mjs +1 -1
- package/test/r65-activation-orchestration.test.mjs +2 -1
- package/test/r65-activation-preflight.test.mjs +7 -6
- package/test/r65-authority-epoch-recovery.test.mjs +3 -1
- package/test/r65-authority-restored-safe.test.mjs +2 -1
- package/test/r65-deploy-preflight.test.mjs +13 -14
- package/test/r65-epoch-native-activation-integration.test.mjs +5 -4
- package/test/r65-legacy-authority-migration.test.mjs +2 -1
- package/test/r67-npm-cross-volume.test.mjs +9 -2
- package/test/r71-full-verifier-artifact.test.mjs +21 -15
- package/test/r71-isolated-slot-rollback-restore.test.mjs +3 -1
- package/test/r71-release-identity.test.mjs +3 -1
- package/test/r73-package-boundary.test.mjs +22 -0
- package/test/r74-kernel-lock.test.mjs +156 -0
- package/test/r74-prepublish-contract.test.mjs +89 -0
- package/test/r75-activation-authority-proof.test.mjs +46 -0
- package/test/r75-authority-evidence.test.mjs +77 -0
- package/test/r76-pinned-marker-cas.test.mjs +78 -0
- package/test/r77-epoch-grant-cas.test.mjs +51 -0
- package/test/r78-parent-junction-cas.test.mjs +62 -0
- package/test/r79-orphan-revocation-reconcile.test.mjs +88 -0
- package/test/r79-windows-fsync-regression.test.mjs +45 -0
- package/test/r80-durable-epoch-commit.test.mjs +76 -0
- package/test/r82-epoch-lock-owned-cas.test.mjs +93 -0
- package/test/r83-epoch-lock-fileid-cas.test.mjs +92 -0
- package/test/r84-epoch-lock-created-fileid.test.mjs +112 -0
- package/test/r85-epoch-lock-release-lifetime.test.mjs +25 -0
- package/test/r85-epoch-repeat-append.test.mjs +30 -0
- package/test/r86-epoch-cleanup-and-loader.test.mjs +126 -0
- package/test/r87-crashed-orphan-revoke-reconcile.test.mjs +110 -0
- package/test/r88-native-disposition-failure.test.mjs +35 -0
- package/test/r89-privileged-install-boundary.test.mjs +123 -0
- package/test/r92-acl-ace-applicability.test.mjs +45 -0
- package/test/r94-win32-propagation-enum.test.mjs +51 -0
- package/test/r99-privileged-import-closure.test.mjs +45 -0
- package/test/remote-session-cli.test.mjs +69 -9
package/src/atomic-mutation.mjs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { createHash, randomBytes } from 'node:crypto';
|
|
2
|
-
import { open, rename, rm, stat } from 'node:fs/promises';
|
|
2
|
+
import { open, readFile, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import {performance} from 'node:perf_hooks';
|
|
5
|
+
import {acquireKernelExclusiveLock, verifyKernelLockBinary} from './kernel-lock-r74.mjs';
|
|
4
6
|
|
|
5
7
|
const sha256Bytes = bytes => createHash('sha256').update(bytes).digest('hex');
|
|
6
8
|
const LOCK_WAIT_MS = 10;
|
|
@@ -9,20 +11,90 @@ const LOCK_STALE_MS = 120000;
|
|
|
9
11
|
|
|
10
12
|
async function acquireMutationLock(target) {
|
|
11
13
|
const lock = `${target}.deadbyte-mutation.lock`;
|
|
12
|
-
const
|
|
14
|
+
const kernelLock = `${target}.deadbyte-mutation.lock.kernel`;
|
|
15
|
+
const ownership = `${process.pid}:${Date.now()}:${randomBytes(16).toString('hex')}`;
|
|
16
|
+
const verified = await verifyKernelLockBinary();
|
|
17
|
+
|
|
18
|
+
if (verified.ok && process.env.DEADBYTE_R74_KERNEL_LOCK !== '0') {
|
|
19
|
+
try {
|
|
20
|
+
const existing = await readFile(lock, 'utf8').catch(() => null);
|
|
21
|
+
if (existing && existing.includes('abandoned')) {
|
|
22
|
+
throw new Error('atomic mutation lock abandoned; reconciliation required');
|
|
23
|
+
}
|
|
24
|
+
} catch (err) {
|
|
25
|
+
if (err.message.includes('abandoned')) throw err;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const releaseKernel = await acquireKernelExclusiveLock(kernelLock, {
|
|
29
|
+
timeoutMs: LOCK_TIMEOUT_MS,
|
|
30
|
+
pollMs: LOCK_WAIT_MS
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
await writeFile(lock, ownership);
|
|
35
|
+
} catch (err) {
|
|
36
|
+
await releaseKernel().catch(() => {});
|
|
37
|
+
throw err;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return async () => {
|
|
41
|
+
let ownershipLost = false;
|
|
42
|
+
try {
|
|
43
|
+
const onDisk = await readFile(lock, 'utf8').catch(() => null);
|
|
44
|
+
if (onDisk !== ownership) {
|
|
45
|
+
ownershipLost = true;
|
|
46
|
+
}
|
|
47
|
+
} finally {
|
|
48
|
+
await releaseKernel().catch(() => {});
|
|
49
|
+
await rm(kernelLock, {force: true}).catch(() => {});
|
|
50
|
+
if (!ownershipLost) {
|
|
51
|
+
await rm(lock, {force: true}).catch(() => {});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (ownershipLost) {
|
|
55
|
+
throw new Error('atomic mutation lock ownership lost');
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const started = performance.now();
|
|
13
61
|
while (true) {
|
|
14
62
|
try {
|
|
15
63
|
const handle = await open(lock, 'wx', 0o600);
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
64
|
+
try {
|
|
65
|
+
await handle.writeFile(ownership);
|
|
66
|
+
await handle.sync();
|
|
67
|
+
await handle.close();
|
|
68
|
+
} catch (error) {
|
|
69
|
+
await handle.close().catch(() => {});
|
|
70
|
+
try { await rm(lock, {force:true}); }
|
|
71
|
+
catch { error.lock_cleanup_failed = true; }
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
return async () => {
|
|
75
|
+
if (await readFile(lock, 'utf8').catch(() => null) !== ownership)
|
|
76
|
+
throw new Error('atomic mutation lock ownership lost');
|
|
77
|
+
await rm(lock, { force: true });
|
|
78
|
+
};
|
|
20
79
|
} catch (error) {
|
|
21
80
|
if (error?.code !== 'EEXIST') throw error;
|
|
22
|
-
if (
|
|
81
|
+
if (performance.now() - started >= LOCK_TIMEOUT_MS) throw new Error('atomic mutation lock timed out');
|
|
23
82
|
try {
|
|
24
83
|
const info = await stat(lock);
|
|
25
|
-
if (Date.now() - info.mtimeMs > LOCK_STALE_MS)
|
|
84
|
+
if (Date.now() - info.mtimeMs > LOCK_STALE_MS) {
|
|
85
|
+
const contents = await readFile(lock, 'utf8').catch(() => null);
|
|
86
|
+
const owner = Number(/^\d+/.exec(contents??'')?.[0]);
|
|
87
|
+
let ownerAlive = true;
|
|
88
|
+
if (Number.isSafeInteger(owner) && owner > 0) {
|
|
89
|
+
try { process.kill(owner, 0); }
|
|
90
|
+
catch (error) { ownerAlive = error?.code !== 'ESRCH'; }
|
|
91
|
+
}
|
|
92
|
+
if (!ownerAlive) {
|
|
93
|
+
// A check-then-unlink race can remove a *new* writer's lock. Without
|
|
94
|
+
// an atomic compare-and-delete primitive, preserve evidence and stop.
|
|
95
|
+
throw new Error('atomic mutation lock abandoned; reconciliation required');
|
|
96
|
+
}
|
|
97
|
+
}
|
|
26
98
|
} catch (statError) {
|
|
27
99
|
if (statError?.code !== 'ENOENT') throw statError;
|
|
28
100
|
}
|
|
@@ -47,6 +119,8 @@ export async function atomicMutation({
|
|
|
47
119
|
throw new Error('atomic mutation requires readState and assertExpected');
|
|
48
120
|
}
|
|
49
121
|
const releaseLock = await acquireMutationLock(target);
|
|
122
|
+
let committed = false;
|
|
123
|
+
let operationFailure = null;
|
|
50
124
|
try {
|
|
51
125
|
const before = await readState(target);
|
|
52
126
|
assertExpected(before, expected);
|
|
@@ -68,6 +142,7 @@ export async function atomicMutation({
|
|
|
68
142
|
if (authorityCheck) await authorityCheck();
|
|
69
143
|
|
|
70
144
|
await rename(temp, target);
|
|
145
|
+
committed = true;
|
|
71
146
|
} catch (error) {
|
|
72
147
|
if (handle) await handle.close().catch(() => {});
|
|
73
148
|
await rm(temp, { force: true }).catch(() => {});
|
|
@@ -87,7 +162,22 @@ export async function atomicMutation({
|
|
|
87
162
|
failure.expected_after_sha256 = expectedAfter;
|
|
88
163
|
throw failure;
|
|
89
164
|
}
|
|
165
|
+
} catch (error) {
|
|
166
|
+
operationFailure = error;
|
|
167
|
+
throw error;
|
|
90
168
|
} finally {
|
|
91
|
-
await releaseLock();
|
|
169
|
+
try { await releaseLock(); }
|
|
170
|
+
catch (error) {
|
|
171
|
+
if (operationFailure) operationFailure.lock_cleanup_failed = true;
|
|
172
|
+
else {
|
|
173
|
+
error.lock_cleanup_failed = true;
|
|
174
|
+
if (committed) {
|
|
175
|
+
error.outcome = 'FAILED_AFTER_UNKNOWN_EFFECT';
|
|
176
|
+
error.side_effect_state = 'unknown';
|
|
177
|
+
error.expected_after_sha256 = sha256Bytes(bytes);
|
|
178
|
+
}
|
|
179
|
+
throw error;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
92
182
|
}
|
|
93
183
|
}
|
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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(
|
|
304
|
-
if(attempt===0&&owner&&Number.isInteger(owner.pid)
|
|
305
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
},
|
package/src/deadbyte-cli.mjs
CHANGED
|
@@ -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,
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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'&¤t.owner_pid===marker.owner_pid&¤t.session_nonce===marker.session_nonce;
|
|
515
|
-
if(same)
|
|
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(
|
|
535
|
-
|
|
536
|
-
await
|
|
537
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
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){
|