deadbyte-mcp 0.15.53 → 0.15.55
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 +31 -26
- package/PROVENANCE.json +21 -10
- package/README.txt +22 -18
- package/RELEASE.json +21 -8
- package/SBOM.cdx.json +2 -2
- package/TEST-MATRIX.json +17 -2
- package/controller/deadbyte-process-policy.json +4 -4
- package/package.json +1 -1
- package/scripts/deploy-r48.mjs +51 -5
- package/src/activation-preflight-r65.mjs +41 -6
- package/src/commander-remote-bridge-r47.mjs +20 -2
- package/src/commander-remote-relay-r47.mjs +13 -2
- package/src/deadbyte-cli.mjs +5 -1
- package/src/legacy-offline-policy-proof-r146.mjs +204 -0
- package/src/release-generation.mjs +20 -1
- package/src/remote-device-r44.mjs +1 -1
- package/src/remote-live-log.mjs +43 -7
- package/src/version.mjs +1 -1
- package/test/commander-remote-bridge-r47.test.mjs +20 -1
- package/test/commander-remote-plane-r47.test.mjs +11 -0
- package/test/r121-signed-transition-invariant.test.mjs +7 -8
- package/test/r130-release-security-contract.test.mjs +7 -7
- package/test/r132-release-identity.test.mjs +8 -8
- package/test/r146-generation-scope-narrowing.test.mjs +64 -0
- package/test/r146-legacy-offline-policy-proof.test.mjs +199 -0
- package/test/r146-livelog-backpressure.test.mjs +105 -0
- package/test/r147-remote-request-hash.test.mjs +54 -0
- package/test/r59-tool-surface-compat.test.mjs +1 -1
- package/test/r65-activation-orchestration.test.mjs +53 -0
- package/test/r65-activation-preflight.test.mjs +35 -0
- package/test/r74-prepublish-contract.test.mjs +1 -1
- package/test/release-generation.test.mjs +8 -1
|
@@ -45,7 +45,12 @@ export function classifyActivationPreflight({pointer,expectedParent,candidate,ex
|
|
|
45
45
|
if(!expectedParent||!expectedCandidate||!candidate||!pointer)throw new Error('activation preflight subject missing');
|
|
46
46
|
const authority=vector(observation);
|
|
47
47
|
if(PLANES.some(plane=>typeof authority[plane]!=='boolean'))return Object.freeze({state:'UNKNOWN',safe_to_activate:false,reason:'authority observation incomplete'});
|
|
48
|
-
|
|
48
|
+
// Observed time is freshness metadata, not a runtime or authority identity.
|
|
49
|
+
// Rebinding to it makes a second read reject an otherwise identical signed approval.
|
|
50
|
+
// Keep every other observed field pinned, including runtime IDs and grant proofs.
|
|
51
|
+
const stableObservation={...observation};
|
|
52
|
+
delete stableObservation.observed_at_utc;
|
|
53
|
+
const subject={pointer,expectedParent,candidate,expectedCandidate,observation:stableObservation};
|
|
49
54
|
const bound={subject_sha256:digest(subject),authority:Object.freeze(authority)};
|
|
50
55
|
for(const plane of PLANES){
|
|
51
56
|
const entry=observation?.authority?.[plane],proof=observation?.authority_proof?.[plane];
|
|
@@ -85,12 +90,25 @@ export function classifyActivationPreflight({pointer,expectedParent,candidate,ex
|
|
|
85
90
|
return Object.freeze({state:'ONLINE_READY',safe_to_activate:true,...bound});
|
|
86
91
|
}
|
|
87
92
|
|
|
88
|
-
|
|
93
|
+
function validateLegacyOfflineRecoveryProof(proof,{preflightSubjectSha256,candidate}){
|
|
94
|
+
if(!proof||proof.state!=='VERIFIED_OFFLINE_LEGACY_SCOPE_REPAIR'||
|
|
95
|
+
proof.preflight_subject_sha256!==preflightSubjectSha256||
|
|
96
|
+
proof.candidate_manifest_sha256!==candidate.manifest_sha256||
|
|
97
|
+
proof.owner_approval_verified!==true||!HEX64.test(String(proof.subject_sha256??'')))
|
|
98
|
+
throw new Error('legacy offline scope recovery proof invalid or unbound');
|
|
99
|
+
return proof;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function orchestrateActivation({pointer,expectedParent,candidate,expectedCandidate,observe,command,activate,claim=null,continuityEngine=null,recordOutcome=null,legacyOfflineRecovery=null}){
|
|
89
103
|
if([observe,command,activate].some(fn=>typeof fn!=='function'))throw new Error('activation orchestrator callbacks invalid');
|
|
90
104
|
const input={pointer,expectedParent,candidate,expectedCandidate};
|
|
91
105
|
const firstObservation=await observe();
|
|
92
106
|
const initial=classifyActivationPreflight({...input,observation:firstObservation});
|
|
93
|
-
|
|
107
|
+
const legacyScopeRepair=initial.state==='AUTHORITY_UNVERIFIED'&&typeof legacyOfflineRecovery==='function'&&
|
|
108
|
+
firstObservation?.state==='OFFLINE'&&firstObservation?.local_ready===false&&
|
|
109
|
+
!firstObservation?.bridge?.alive&&!firstObservation?.core?.alive&&!firstObservation?.cloud?.alive&&
|
|
110
|
+
firstObservation?.supervisor?.running===false&&PLANES.every(plane=>initial.authority?.[plane]===false);
|
|
111
|
+
if(!legacyScopeRepair&&!['ONLINE_READY','ONLINE_HOST_ARMED','PARKED_DIRTY','OFFLINE_CLEAN'].includes(initial.state))
|
|
94
112
|
return Object.freeze({state:'ACTIVATION_REJECTED',preflight:initial.state,subject_sha256:initial.subject_sha256??null});
|
|
95
113
|
const continuity=classifyAuthorityContinuity(firstObservation);
|
|
96
114
|
if(continuity.state==='UNVERIFIED')
|
|
@@ -112,6 +130,12 @@ export async function orchestrateActivation({pointer,expectedParent,candidate,ex
|
|
|
112
130
|
let continuityCapture=null;
|
|
113
131
|
const outcome=await (async()=>{
|
|
114
132
|
try{
|
|
133
|
+
let legacyProof=null;
|
|
134
|
+
if(legacyScopeRepair){
|
|
135
|
+
legacyProof=validateLegacyOfflineRecoveryProof(await legacyOfflineRecovery({phase:'claimed',observation:firstObservation,
|
|
136
|
+
preflight_subject_sha256:initial.subject_sha256,pointer,expectedParent,candidate,expectedCandidate}),
|
|
137
|
+
{preflightSubjectSha256:initial.subject_sha256,candidate});
|
|
138
|
+
}
|
|
115
139
|
const requiresReauth=continuity.state==='LEGACY_REAUTH_REQUIRED'||continuity.state==='MIXED_REAUTH_REQUIRED';
|
|
116
140
|
const epochNative=continuity.state==='EPOCH_NATIVE';
|
|
117
141
|
if(epochNative){
|
|
@@ -134,8 +158,18 @@ export async function orchestrateActivation({pointer,expectedParent,candidate,ex
|
|
|
134
158
|
await command('supervisorstop');
|
|
135
159
|
await command('stop');
|
|
136
160
|
}
|
|
137
|
-
const
|
|
138
|
-
|
|
161
|
+
const readyObservation=await observe();
|
|
162
|
+
const ready=classifyActivationPreflight({...input,observation:readyObservation});
|
|
163
|
+
let readySafe=ready.safe_to_activate;
|
|
164
|
+
if(legacyScopeRepair){
|
|
165
|
+
if(ready.subject_sha256!==initial.subject_sha256)throw new Error('legacy offline scope recovery observation drift');
|
|
166
|
+
const second=validateLegacyOfflineRecoveryProof(await legacyOfflineRecovery({phase:'preactivate',observation:readyObservation,
|
|
167
|
+
preflight_subject_sha256:initial.subject_sha256,pointer,expectedParent,candidate,expectedCandidate,prior_proof:legacyProof}),
|
|
168
|
+
{preflightSubjectSha256:initial.subject_sha256,candidate});
|
|
169
|
+
if(second.subject_sha256!==legacyProof.subject_sha256)throw new Error('legacy offline scope recovery proof drift');
|
|
170
|
+
readySafe=true;
|
|
171
|
+
}
|
|
172
|
+
if(!readySafe)
|
|
139
173
|
return Object.freeze({state:transitionAttempted?'ACTIVATION_OUTCOME_UNKNOWN':'ACTIVATION_REJECTED',
|
|
140
174
|
preflight:ready.state,subject_sha256:initial.subject_sha256});
|
|
141
175
|
if(PLANES.some(plane=>ready.authority[plane]!==false))
|
|
@@ -180,7 +214,8 @@ export async function orchestrateActivation({pointer,expectedParent,candidate,ex
|
|
|
180
214
|
reauth_planes:Object.freeze([...(restored.reauth_planes??[])])});
|
|
181
215
|
}
|
|
182
216
|
const final=beforeRestore;
|
|
183
|
-
const authorityOutcome=
|
|
217
|
+
const authorityOutcome=legacyScopeRepair?'LEGACY_SCOPE_REPAIRED_DISARMED':
|
|
218
|
+
continuity.state==='NOT_APPLICABLE'?'NOT_APPLICABLE':'REAUTH_REQUIRED';
|
|
184
219
|
return Object.freeze({
|
|
185
220
|
state:'ACTIVATED',runtime_activation:'ACTIVATED',authority_continuity:authorityOutcome,
|
|
186
221
|
subject_sha256:initial.subject_sha256,release_id:activation.release_id,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as z from 'zod/v4';
|
|
2
|
+
import { payloadHash } from './remote-device-r44.mjs';
|
|
2
3
|
|
|
3
4
|
const HEX64=/^[0-9a-f]{64}$/;
|
|
4
5
|
const CAP=/^[A-Za-z0-9._-]{1,128}$/;
|
|
@@ -9,12 +10,25 @@ function requirePlane(plane,name){
|
|
|
9
10
|
if(!plane) return {status:'unavailable',tool:name,error:'remote-device-r44 plane is not configured'};
|
|
10
11
|
return null;
|
|
11
12
|
}
|
|
13
|
+
async function probeBoundConnection(plane,{device_id,epoch,connection_id,nonce}){
|
|
14
|
+
if(!HEX64.test(device_id??''))throw new Error('remote readiness device identity invalid');
|
|
15
|
+
if(!Number.isInteger(epoch)||epoch<1)throw new Error('remote readiness epoch invalid');
|
|
16
|
+
if(typeof connection_id!=='string'||connection_id.length<1||connection_id.length>256)throw new Error('remote readiness connection identity invalid');
|
|
17
|
+
if(!HEX64.test(nonce??''))throw new Error('remote readiness nonce invalid');
|
|
18
|
+
const result=await plane.probe({deviceId:device_id,epoch});
|
|
19
|
+
if(!result||result.device_id!==device_id)throw new Error('remote readiness device identity mismatch');
|
|
20
|
+
if(result.epoch!==epoch)throw new Error('remote readiness epoch mismatch');
|
|
21
|
+
if(result.connection_id!==connection_id)throw new Error('remote readiness connection identity mismatch');
|
|
22
|
+
if(result.reachable!==true)throw new Error('remote readiness probe not reachable');
|
|
23
|
+
return {...result,nonce,ready:true};
|
|
24
|
+
}
|
|
12
25
|
export function registerCommanderRemoteBridge(server,{remoteDevicePlane=null}={}){
|
|
13
26
|
register(server,'remote_device',z.strictObject({
|
|
14
27
|
op:z.enum(['pair','status','report_online','probe','dispatch','revoke','rotate_credential','list']),
|
|
15
28
|
device_id:z.string().regex(HEX64).optional(),epoch:z.number().int().min(1).optional(),
|
|
16
29
|
public_key_spki_base64:z.string().min(1).max(4096).optional(),display_name:z.string().min(1).max(128).optional(),
|
|
17
30
|
connection_id:z.string().min(1).max(256).optional(),capability_id:z.string().regex(CAP).optional(),
|
|
31
|
+
nonce:z.string().regex(HEX64).optional(),
|
|
18
32
|
payload:z.record(z.string(),z.unknown()).optional(),request_id:z.string().regex(HEX64).optional(),
|
|
19
33
|
encrypted_bundle_base64:z.string().min(1).max(1024*1024).optional(),expected_previous_sha256:z.string().regex(HEX64).nullable().optional()
|
|
20
34
|
}),async a=>{
|
|
@@ -23,7 +37,7 @@ export function registerCommanderRemoteBridge(server,{remoteDevicePlane=null}={}
|
|
|
23
37
|
case 'pair': return remoteDevicePlane.pair({publicKeySpkiBase64:a.public_key_spki_base64,displayName:a.display_name});
|
|
24
38
|
case 'status': return remoteDevicePlane.status({deviceId:a.device_id});
|
|
25
39
|
case 'report_online': return remoteDevicePlane.reportOnline({deviceId:a.device_id,epoch:a.epoch,connectionId:a.connection_id});
|
|
26
|
-
case 'probe': return remoteDevicePlane
|
|
40
|
+
case 'probe': return probeBoundConnection(remoteDevicePlane,a);
|
|
27
41
|
case 'dispatch': return remoteDevicePlane.dispatch({deviceId:a.device_id,epoch:a.epoch,capabilityId:a.capability_id,payload:a.payload??{},requestId:a.request_id});
|
|
28
42
|
case 'revoke': return remoteDevicePlane.revoke({deviceId:a.device_id,epoch:a.epoch});
|
|
29
43
|
case 'rotate_credential': return remoteDevicePlane.rotateCredential({deviceId:a.device_id,epoch:a.epoch,encryptedBundleBase64:a.encrypted_bundle_base64,expectedPreviousSha256:a.expected_previous_sha256??null});
|
|
@@ -33,9 +47,13 @@ export function registerCommanderRemoteBridge(server,{remoteDevicePlane=null}={}
|
|
|
33
47
|
});
|
|
34
48
|
register(server,'remote_mcp',z.strictObject({
|
|
35
49
|
device_id:z.string().regex(HEX64),epoch:z.number().int().min(1),capability_id:z.string().regex(CAP),
|
|
36
|
-
payload:z.record(z.string(),z.unknown()).default({}),request_id:z.string().regex(HEX64)
|
|
50
|
+
payload:z.record(z.string(),z.unknown()).default({}),request_id:z.string().regex(HEX64),
|
|
51
|
+
request_sha256:z.string().regex(HEX64).optional()
|
|
37
52
|
}),async a=>{
|
|
38
53
|
const unavailable=requirePlane(remoteDevicePlane,'remote_mcp');if(unavailable)return unavailable;
|
|
54
|
+
if(a.request_sha256&&payloadHash({deviceId:a.device_id,epoch:a.epoch,capabilityId:a.capability_id,payload:a.payload})!==a.request_sha256){
|
|
55
|
+
throw new Error('remote request hash mismatch');
|
|
56
|
+
}
|
|
39
57
|
return remoteDevicePlane.dispatch({deviceId:a.device_id,epoch:a.epoch,capabilityId:a.capability_id,payload:a.payload,requestId:a.request_id});
|
|
40
58
|
});
|
|
41
59
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client';
|
|
2
|
+
import { DEADBYTE_VERSION } from './version.mjs';
|
|
2
3
|
|
|
3
4
|
function insist(ok,message){if(!ok)throw new Error(message);}
|
|
4
5
|
function structured(result,label){
|
|
@@ -8,11 +9,21 @@ function structured(result,label){
|
|
|
8
9
|
return value;
|
|
9
10
|
}
|
|
10
11
|
|
|
12
|
+
export function validateRemoteProbeResponseR47(result,{deviceId,epoch,connectionId,nonce}){
|
|
13
|
+
insist(result&&typeof result==='object','remote readiness response missing');
|
|
14
|
+
insist(result.device_id===deviceId,'remote readiness device identity mismatch');
|
|
15
|
+
insist(result.epoch===epoch,'remote readiness epoch mismatch');
|
|
16
|
+
insist(result.connection_id===connectionId,'remote readiness connection identity mismatch');
|
|
17
|
+
insist(result.nonce===nonce,'remote readiness nonce mismatch');
|
|
18
|
+
insist(result.ready===true,'remote readiness response not ready');
|
|
19
|
+
return result;
|
|
20
|
+
}
|
|
21
|
+
|
|
11
22
|
export async function createCommanderRemoteRelayR47({endpoint,protocolVersion='2026-07-28',timeoutMs=15000}={}){
|
|
12
23
|
const url=new URL(String(endpoint??''));
|
|
13
24
|
insist(url.protocol==='https:','remote relay endpoint must use HTTPS');
|
|
14
25
|
insist(Number.isInteger(timeoutMs)&&timeoutMs>=100&&timeoutMs<=120000,'remote relay timeout invalid');
|
|
15
|
-
const client=new Client({name:'deadbyte-r47-remote-relay',version:
|
|
26
|
+
const client=new Client({name:'deadbyte-r47-remote-relay',version:DEADBYTE_VERSION},{versionNegotiation:{mode:{pin:protocolVersion}}});
|
|
16
27
|
const transport=new StreamableHTTPClientTransport(url);
|
|
17
28
|
let connected=false;
|
|
18
29
|
let connecting=null;
|
|
@@ -25,7 +36,7 @@ export async function createCommanderRemoteRelayR47({endpoint,protocolVersion='2
|
|
|
25
36
|
async function probe({device_id,epoch,connection_id,nonce}){
|
|
26
37
|
const value=await call('remote_device',{op:'probe',device_id,epoch,connection_id,nonce},'remote readiness probe');
|
|
27
38
|
const result=value.result??value;
|
|
28
|
-
return {device_id,epoch,nonce
|
|
39
|
+
return validateRemoteProbeResponseR47(result,{deviceId:device_id,epoch,connectionId:connection_id,nonce});
|
|
29
40
|
}
|
|
30
41
|
async function request({device_id,epoch,capability_id,payload,request_id,request_sha256}){
|
|
31
42
|
insist(typeof request_sha256==='string'&&/^[0-9a-f]{64}$/.test(request_sha256),'remote request hash invalid');
|
package/src/deadbyte-cli.mjs
CHANGED
|
@@ -635,7 +635,11 @@ async function runRemoteSession({resumeCursor=null,verbose=false,trace=false,fil
|
|
|
635
635
|
const wasAborted=aborter.signal.aborted;
|
|
636
636
|
aborter.abort();
|
|
637
637
|
if(logTask) await logTask.catch(()=>{});
|
|
638
|
-
timelineWriter.flush();
|
|
638
|
+
timelineWriter.flush();
|
|
639
|
+
const liveLogDelivery=timelineWriter.retention();
|
|
640
|
+
if(liveLogDelivery.suppressed_since_drain>0){
|
|
641
|
+
process.stderr.write(`[WARN] LIVELOG ${liveLogDelivery.suppressed_since_drain} display lines still suppressed at disconnect due to stdout backpressure; inspect signed audit and runtime logs.\n`);
|
|
642
|
+
}
|
|
639
643
|
for(const line of activityTracker.reconcileInterrupted('session_end')) process.stdout.write(`${line}\n`);
|
|
640
644
|
const filterRetention=timelineFilter?.retention();
|
|
641
645
|
if(filterRetention?.evicted>0) process.stdout.write(`[WARN] LIVELOG filter evicted ${filterRetention.evicted} presentation identities; full audit remains on disk.\n`);
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import {createHash,createPublicKey,verify as verifySignature} from 'node:crypto';
|
|
2
|
+
import {lstat,open,readFile} from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import {verifyGenerationBundle} from './release-generation.mjs';
|
|
5
|
+
import {canonicalJson} from './canonical-json.mjs';
|
|
6
|
+
|
|
7
|
+
const PLANES=['process','coding','autonomous','desktop','host'];
|
|
8
|
+
const GRANT_FIELDS={process:'grant_file',coding:'lease_file',autonomous:'lease_file',desktop:'grant_file',host:'arm_file'};
|
|
9
|
+
const HEX=/^[0-9a-f]{64}$/;
|
|
10
|
+
const APPROVAL_DOMAIN=Buffer.from('DEADBYTE-R146-POLICY-MIGRATION-APPROVAL\0','utf8');
|
|
11
|
+
const APPROVAL_SCHEMA='deadbyte.r146-policy-migration-approval.v1';
|
|
12
|
+
const SUBJECT_SCHEMA='deadbyte.r146-policy-migration-subject.v1';
|
|
13
|
+
const APPROVAL_FIELDS=['schema','subject_sha256','owner_key_id','subject','signature_base64'];
|
|
14
|
+
const SUBJECT_FIELDS=['schema','action','predecessor_release_id','predecessor_version','predecessor_manifest_sha256',
|
|
15
|
+
'predecessor_generation_manifest_sha256','candidate_version','candidate_manifest_sha256','predecessor_policy_sha256',
|
|
16
|
+
'preflight_subject_sha256','issued_at_utc','expires_at_utc'];
|
|
17
|
+
const sha=bytes=>createHash('sha256').update(bytes).digest('hex');
|
|
18
|
+
|
|
19
|
+
function sameFile(a,b){
|
|
20
|
+
return a.isFile()&&b.isFile()&&!a.isSymbolicLink()&&!b.isSymbolicLink()&&
|
|
21
|
+
a.dev===b.dev&&a.ino===b.ino&&a.size===b.size&&a.mtimeMs===b.mtimeMs&&a.ctimeMs===b.ctimeMs;
|
|
22
|
+
}
|
|
23
|
+
async function readPinned(file,expected,label){
|
|
24
|
+
if(!path.isAbsolute(file)||!HEX.test(expected??''))throw new Error(label+' identity incomplete');
|
|
25
|
+
const before=await lstat(file);
|
|
26
|
+
if(!before.isFile()||before.isSymbolicLink())throw new Error(label+' file invalid');
|
|
27
|
+
const handle=await open(file,'r');
|
|
28
|
+
try{
|
|
29
|
+
const opened=await handle.stat();
|
|
30
|
+
if(!sameFile(before,opened))throw new Error(label+' identity moved');
|
|
31
|
+
const bytes=await handle.readFile();
|
|
32
|
+
const after=await handle.stat(),onPath=await lstat(file);
|
|
33
|
+
if(!sameFile(opened,after)||!sameFile(after,onPath))throw new Error(label+' changed');
|
|
34
|
+
if(sha(bytes)!==expected)throw new Error(label+' hash mismatch');
|
|
35
|
+
return JSON.parse(bytes.toString('utf8').replace(/^\uFEFF/,''));
|
|
36
|
+
}finally{await handle.close();}
|
|
37
|
+
}
|
|
38
|
+
async function readPinnedBytes(file,expected,label){
|
|
39
|
+
if(!path.isAbsolute(file)||!HEX.test(expected??''))throw new Error(label+' identity incomplete');
|
|
40
|
+
const before=await lstat(file);
|
|
41
|
+
if(!before.isFile()||before.isSymbolicLink())throw new Error(label+' file invalid');
|
|
42
|
+
const handle=await open(file,'r');
|
|
43
|
+
try{
|
|
44
|
+
const opened=await handle.stat();
|
|
45
|
+
if(!sameFile(before,opened))throw new Error(label+' identity moved');
|
|
46
|
+
const bytes=await handle.readFile();
|
|
47
|
+
const after=await handle.stat(),onPath=await lstat(file);
|
|
48
|
+
if(!sameFile(opened,after)||!sameFile(after,onPath))throw new Error(label+' changed');
|
|
49
|
+
if(sha(bytes)!==expected)throw new Error(label+' hash mismatch');
|
|
50
|
+
return bytes;
|
|
51
|
+
}finally{await handle.close();}
|
|
52
|
+
}
|
|
53
|
+
async function readRegularBytes(file,label){
|
|
54
|
+
if(!path.isAbsolute(file))throw new Error(label+' path invalid');
|
|
55
|
+
const before=await lstat(file);
|
|
56
|
+
if(!before.isFile()||before.isSymbolicLink())throw new Error(label+' file invalid');
|
|
57
|
+
const handle=await open(file,'r');
|
|
58
|
+
try{
|
|
59
|
+
const opened=await handle.stat();
|
|
60
|
+
if(!sameFile(before,opened))throw new Error(label+' identity moved');
|
|
61
|
+
const bytes=await handle.readFile();
|
|
62
|
+
const after=await handle.stat(),onPath=await lstat(file);
|
|
63
|
+
if(!sameFile(opened,after)||!sameFile(after,onPath))throw new Error(label+' changed');
|
|
64
|
+
return bytes;
|
|
65
|
+
}finally{await handle.close();}
|
|
66
|
+
}
|
|
67
|
+
async function mustBeAbsent(file,label){
|
|
68
|
+
try{await lstat(file);throw new Error(label+' present');}
|
|
69
|
+
catch(error){if(error?.code!=='ENOENT')throw error;}
|
|
70
|
+
}
|
|
71
|
+
function exactDrive(root,drive){
|
|
72
|
+
return typeof root?.path==='string'&&path.win32.normalize(root.path).toLowerCase()===drive.toLowerCase();
|
|
73
|
+
}
|
|
74
|
+
function exactObject(value,fields,label){
|
|
75
|
+
if(!value||typeof value!=='object'||Array.isArray(value))throw new Error(label+' invalid');
|
|
76
|
+
const keys=Object.keys(value);
|
|
77
|
+
if(keys.some(key=>!fields.includes(key))||fields.some(key=>!Object.hasOwn(value,key)))throw new Error(label+' shape invalid');
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
80
|
+
function instant(value,label){
|
|
81
|
+
if(typeof value!=='string'||!/^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3}Z$/.test(value))throw new Error(label+' invalid');
|
|
82
|
+
const parsed=Date.parse(value);
|
|
83
|
+
if(!Number.isFinite(parsed)||new Date(parsed).toISOString()!==value)throw new Error(label+' invalid');
|
|
84
|
+
return parsed;
|
|
85
|
+
}
|
|
86
|
+
async function verifyMigrationApproval({approvalPath,approvalSha256,ownerPublicKeyPath,expected,now}){
|
|
87
|
+
const approval=JSON.parse((await readPinnedBytes(approvalPath,approvalSha256,'policy migration approval')).toString('utf8').replace(/^\uFEFF/,''));
|
|
88
|
+
exactObject(approval,APPROVAL_FIELDS,'policy migration approval');
|
|
89
|
+
const subject=exactObject(approval.subject,SUBJECT_FIELDS,'policy migration approval subject');
|
|
90
|
+
if(approval.schema!==APPROVAL_SCHEMA||subject.schema!==SUBJECT_SCHEMA||subject.action!=='legacy_offline_scope_repair')throw new Error('policy migration approval scope invalid');
|
|
91
|
+
for(const [key,value] of Object.entries(expected)){
|
|
92
|
+
if(key==='predecessor_policy_sha256'){
|
|
93
|
+
if(canonicalJson(subject[key])!==canonicalJson(value))throw new Error('policy migration approval policy binding mismatch');
|
|
94
|
+
}else if(subject[key]!==value)throw new Error('policy migration approval subject mismatch: '+key.replaceAll('_',' '));
|
|
95
|
+
}
|
|
96
|
+
const issued=instant(subject.issued_at_utc,'policy migration approval issued time');
|
|
97
|
+
const expires=instant(subject.expires_at_utc,'policy migration approval expiry');
|
|
98
|
+
const observed=now instanceof Date?now.getTime():NaN;
|
|
99
|
+
if(!Number.isFinite(observed)||expires<=issued||expires-issued>3_600_000||observed<issued||observed>=expires)throw new Error('policy migration approval expired or invalid');
|
|
100
|
+
const subjectSha256=sha(Buffer.from(canonicalJson(subject),'utf8'));
|
|
101
|
+
if(approval.subject_sha256!==subjectSha256)throw new Error('policy migration approval subject hash mismatch');
|
|
102
|
+
const keyBytes=await readRegularBytes(ownerPublicKeyPath,'policy migration owner key');
|
|
103
|
+
let publicKey;
|
|
104
|
+
try{publicKey=createPublicKey(keyBytes);}catch{throw new Error('policy migration owner key invalid');}
|
|
105
|
+
if(publicKey.asymmetricKeyType!=='ed25519')throw new Error('policy migration owner key must be Ed25519');
|
|
106
|
+
const ownerKeyId=sha(publicKey.export({type:'spki',format:'der'}));
|
|
107
|
+
if(approval.owner_key_id!==ownerKeyId)throw new Error('policy migration owner key identity mismatch');
|
|
108
|
+
if(typeof approval.signature_base64!=='string')throw new Error('policy migration approval signature invalid');
|
|
109
|
+
const signature=Buffer.from(approval.signature_base64,'base64');
|
|
110
|
+
const signed={schema:APPROVAL_SCHEMA,subject_sha256:subjectSha256,owner_key_id:ownerKeyId};
|
|
111
|
+
if(signature.length!==64||!verifySignature(null,Buffer.concat([APPROVAL_DOMAIN,Buffer.from(canonicalJson(signed),'utf8')]),publicKey,signature))throw new Error('policy migration approval signature invalid');
|
|
112
|
+
return {subject_sha256:subjectSha256,owner_key_id:ownerKeyId,expires_at_utc:subject.expires_at_utc};
|
|
113
|
+
}
|
|
114
|
+
export async function proveLegacyOfflineScopeRepair({
|
|
115
|
+
stateRoot,pointer,expectedParent,candidate,expectedCandidate,preflightSubjectSha256,
|
|
116
|
+
observation,policyPaths,policySha256,approvalPath,approvalSha256,ownerPublicKeyPath,
|
|
117
|
+
now=new Date()
|
|
118
|
+
}){
|
|
119
|
+
if(!stateRoot||!path.isAbsolute(stateRoot))throw new Error('state root invalid');
|
|
120
|
+
if(!pointer||!expectedParent||!observation)throw new Error('legacy scope proof subject missing');
|
|
121
|
+
if(expectedParent.version!=='0.15.45'||pointer.version!==expectedParent.version||
|
|
122
|
+
pointer.release_id!==expectedParent.release_id||
|
|
123
|
+
pointer.manifest_sha256!==expectedParent.manifest_sha256||
|
|
124
|
+
pointer.generation_manifest_sha256!==expectedParent.generation_manifest_sha256)
|
|
125
|
+
throw new Error('legacy scope proof predecessor mismatch');
|
|
126
|
+
if(!candidate||!expectedCandidate||candidate.version!==expectedCandidate.version||
|
|
127
|
+
candidate.manifest_sha256!==expectedCandidate.manifest_sha256||!HEX.test(candidate.manifest_sha256??''))
|
|
128
|
+
throw new Error('legacy scope proof candidate mismatch');
|
|
129
|
+
if(!HEX.test(preflightSubjectSha256??''))throw new Error('legacy scope proof preflight subject invalid');
|
|
130
|
+
if(observation.state!=='OFFLINE'||observation.local_ready!==false||
|
|
131
|
+
['bridge','core','cloud'].some(key=>observation[key]?.alive!==false)||
|
|
132
|
+
observation.supervisor?.running!==false)
|
|
133
|
+
throw new Error('legacy scope proof runtime is not cleanly offline');
|
|
134
|
+
if(observation.active_release_id!==pointer.release_id||
|
|
135
|
+
observation.active_manifest_sha256!==pointer.manifest_sha256||
|
|
136
|
+
observation.active_generation_manifest_sha256!==pointer.generation_manifest_sha256)
|
|
137
|
+
throw new Error('legacy scope proof observation generation drift');
|
|
138
|
+
if(!observation.authority||!observation.authority_proof||!observation.policy_sha256)
|
|
139
|
+
throw new Error('legacy scope proof missing authority evidence');
|
|
140
|
+
for(const plane of PLANES){
|
|
141
|
+
const entry=observation.authority[plane],proof=observation.authority_proof[plane];
|
|
142
|
+
const reason=plane==='autonomous'?'unverified':'disarmed';
|
|
143
|
+
const state=plane==='autonomous'?'UNVERIFIED':'VERIFIED_DISARMED';
|
|
144
|
+
if(entry?.configured!==true||entry.armed!==false||entry.reason!==reason||
|
|
145
|
+
proof?.state!==state||proof.grant_absent!==true||
|
|
146
|
+
proof.policy_sha256!==observation.policy_sha256[plane]||
|
|
147
|
+
policySha256?.[plane]!==proof.policy_sha256)
|
|
148
|
+
throw new Error('legacy scope proof authority not physically inactive: '+plane);
|
|
149
|
+
}
|
|
150
|
+
const storedPointer=JSON.parse(await readFile(path.join(stateRoot,'active-release.json'),'utf8'));
|
|
151
|
+
if(storedPointer.release_id!==pointer.release_id||
|
|
152
|
+
storedPointer.generation_manifest_sha256!==pointer.generation_manifest_sha256||
|
|
153
|
+
storedPointer.manifest_sha256!==pointer.manifest_sha256)
|
|
154
|
+
throw new Error('legacy scope proof pointer changed');
|
|
155
|
+
await verifyGenerationBundle(pointer.generation_root,pointer.generation_manifest_sha256);
|
|
156
|
+
await mustBeAbsent(path.join(stateRoot,'runtime','remote-session.json'),'foreground ownership marker');
|
|
157
|
+
for(const plane of PLANES)await mustBeAbsent(path.join(stateRoot,'runtime','authority-epochs',plane,'.lock'),plane+' authority epoch lock');
|
|
158
|
+
const policies={};
|
|
159
|
+
for(const plane of PLANES){
|
|
160
|
+
const file=policyPaths?.[plane],digest=policySha256?.[plane];
|
|
161
|
+
if(path.resolve(path.dirname(file??''))!==path.resolve(pointer.generation_root))throw new Error('legacy scope proof policy outside predecessor generation: '+plane);
|
|
162
|
+
policies[plane]=await readPinned(file,digest,plane+' policy');
|
|
163
|
+
const grant=policies[plane][GRANT_FIELDS[plane]];
|
|
164
|
+
if(typeof grant!=='string'||!path.isAbsolute(grant)||
|
|
165
|
+
path.win32.resolve(path.win32.dirname(grant)).toLowerCase()!==
|
|
166
|
+
path.win32.resolve(path.join(stateRoot,'runtime')).toLowerCase())
|
|
167
|
+
throw new Error('legacy scope proof grant path invalid: '+plane);
|
|
168
|
+
await mustBeAbsent(grant,plane+' grant');
|
|
169
|
+
}
|
|
170
|
+
const coding=policies.coding,autonomous=policies.autonomous;
|
|
171
|
+
if(coding.schema!=='deadbyte.coding-policy.v1'||autonomous.schema!=='deadbyte.autonomous-policy.v1'||
|
|
172
|
+
coding.enabled!==true||autonomous.enabled!==true||
|
|
173
|
+
autonomous.coding_policy_sha256!==policySha256.coding)
|
|
174
|
+
throw new Error('legacy scope proof Coding/Autonomous binding invalid');
|
|
175
|
+
if(!exactDrive(coding.roots?.local_c,'C:\\')||!exactDrive(coding.roots?.local_d,'D:\\'))
|
|
176
|
+
throw new Error('legacy scope proof expected drive roots missing');
|
|
177
|
+
for(const [id,root] of Object.entries(coding.roots)){
|
|
178
|
+
const normalized=path.win32.normalize(String(root?.path??''));
|
|
179
|
+
if(/^[a-z]:\\$/i.test(normalized)&&!['local_c','local_d'].includes(id))
|
|
180
|
+
throw new Error('legacy scope proof unexpected drive root: '+id);
|
|
181
|
+
}
|
|
182
|
+
if(!Array.isArray(autonomous.root_ids)||autonomous.root_ids.length===0||
|
|
183
|
+
autonomous.root_ids.some(id=>id==='local_c'||id==='local_d'||!Object.hasOwn(coding.roots,id)))
|
|
184
|
+
throw new Error('legacy scope proof autonomous roots unsafe');
|
|
185
|
+
const approval=await verifyMigrationApproval({approvalPath,approvalSha256,ownerPublicKeyPath,now,expected:{
|
|
186
|
+
predecessor_release_id:pointer.release_id,predecessor_version:pointer.version,
|
|
187
|
+
predecessor_manifest_sha256:pointer.manifest_sha256,
|
|
188
|
+
predecessor_generation_manifest_sha256:pointer.generation_manifest_sha256,
|
|
189
|
+
candidate_version:candidate.version,candidate_manifest_sha256:candidate.manifest_sha256,
|
|
190
|
+
predecessor_policy_sha256:policySha256,preflight_subject_sha256:preflightSubjectSha256
|
|
191
|
+
}});
|
|
192
|
+
const subject={release_id:pointer.release_id,version:pointer.version,
|
|
193
|
+
generation_manifest_sha256:pointer.generation_manifest_sha256,
|
|
194
|
+
coding_policy_sha256:policySha256.coding,autonomous_policy_sha256:policySha256.autonomous,
|
|
195
|
+
candidate_version:candidate.version,candidate_manifest_sha256:candidate.manifest_sha256,
|
|
196
|
+
preflight_subject_sha256:preflightSubjectSha256,owner_approval_subject_sha256:approval.subject_sha256,
|
|
197
|
+
grant_absent_planes:PLANES};
|
|
198
|
+
return Object.freeze({
|
|
199
|
+
state:'VERIFIED_OFFLINE_LEGACY_SCOPE_REPAIR',
|
|
200
|
+
...subject,owner_approval_verified:true,owner_key_id:approval.owner_key_id,
|
|
201
|
+
approval_expires_at_utc:approval.expires_at_utc,
|
|
202
|
+
subject_sha256:sha(Buffer.from(canonicalJson(subject),'utf8'))
|
|
203
|
+
});
|
|
204
|
+
}
|
|
@@ -51,11 +51,30 @@ export async function verifyGenerationBundle(root,expectedManifestSha256){
|
|
|
51
51
|
return {files,generation_manifest_sha256:manifestSha256};
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
function rebindCodingPolicy(raw,{developmentRoot,stateRoot,parityScratchRoot}){
|
|
54
|
+
export function rebindCodingPolicy(raw,{developmentRoot,stateRoot,parityScratchRoot}){
|
|
55
55
|
const policy=clone(raw);
|
|
56
56
|
policy.lease_file=path.join(stateRoot,'runtime','coding-grant.json');
|
|
57
57
|
policy.evidence_dir=path.join(stateRoot,'evidence','coding');
|
|
58
58
|
if(!policy.roots?.core) throw new Error('coding policy requires core root for R27');
|
|
59
|
+
// A prior generation may contain legacy whole-drive roots. Never let those
|
|
60
|
+
// capabilities propagate into a new signed release generation.
|
|
61
|
+
const removed=new Set();
|
|
62
|
+
for(const [id,root] of Object.entries(policy.roots)){
|
|
63
|
+
if(typeof root?.path!=='string') throw new Error('coding root path invalid: '+id);
|
|
64
|
+
const full=path.win32.normalize(root.path),drive=path.win32.parse(full).root;
|
|
65
|
+
if(!/^[a-z]:\\$/i.test(drive)||full.toLowerCase()!==drive.toLowerCase()) continue;
|
|
66
|
+
if(!((id==='local_c'&&drive.toLowerCase()==='c:\\')||
|
|
67
|
+
(id==='local_d'&&drive.toLowerCase()==='d:\\')))
|
|
68
|
+
throw new Error('unrecognized whole-drive Coding root: '+id);
|
|
69
|
+
removed.add(id);
|
|
70
|
+
delete policy.roots[id];
|
|
71
|
+
}
|
|
72
|
+
for(const [id,command] of Object.entries(policy.commands??{})){
|
|
73
|
+
if(!Array.isArray(command.root_ids)) continue;
|
|
74
|
+
const next=command.root_ids.filter(rootId=>!removed.has(rootId));
|
|
75
|
+
if(next.length===0) throw new Error('Coding command would lose every scoped root: '+id);
|
|
76
|
+
command.root_ids=next;
|
|
77
|
+
}
|
|
59
78
|
policy.roots.core.path=developmentRoot;
|
|
60
79
|
policy.roots.parity_scratch={path:parityScratchRoot,read:true,write:true,exec:false,deny_paths:[]};
|
|
61
80
|
return policy;
|
|
@@ -73,7 +73,7 @@ async function withTimeout(operation,timeoutMs,label){
|
|
|
73
73
|
]);
|
|
74
74
|
}finally{if(timer)clearTimeout(timer);}
|
|
75
75
|
}
|
|
76
|
-
function payloadHash({deviceId,epoch,capabilityId,payload}){
|
|
76
|
+
export function payloadHash({deviceId,epoch,capabilityId,payload}){
|
|
77
77
|
return sha(Buffer.from(canonicalJson({device_id:deviceId,epoch,capability_id:capabilityId,payload}),'utf8'));
|
|
78
78
|
}
|
|
79
79
|
|
package/src/remote-live-log.mjs
CHANGED
|
@@ -325,9 +325,40 @@ export function createLiveTimelineWriter({stream=process.stdout,displayMode='uni
|
|
|
325
325
|
if(!stream||typeof stream.write!=='function') throw new Error('timeline writer stream invalid');
|
|
326
326
|
if(!REMOTE_DISPLAY_MODES.includes(displayMode)) throw new Error('timeline writer display mode invalid');
|
|
327
327
|
if(typeof rewrite!=='boolean') throw new Error('timeline writer rewrite invalid');
|
|
328
|
-
let active=null;
|
|
328
|
+
let active=null,congested=false,needsNewline=false,suppressedSinceDrain=0,totalSuppressed=0;
|
|
329
|
+
// Display output is lossy under a stalled consumer; the on-disk signed audit
|
|
330
|
+
// and runtime logs remain authoritative. Do not enqueue unbounded text in
|
|
331
|
+
// Node's Writable internal buffer after write() returns false.
|
|
332
|
+
function emit(chunk){
|
|
333
|
+
const ok=stream.write(chunk);
|
|
334
|
+
if(ok===false){
|
|
335
|
+
if(typeof stream.once!=='function') throw new Error('timeline writer requires drain-capable stream under backpressure');
|
|
336
|
+
congested=true;
|
|
337
|
+
stream.once('drain',onDrain);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
function onDrain(){
|
|
341
|
+
congested=false;
|
|
342
|
+
if(active||needsNewline){
|
|
343
|
+
active=null;needsNewline=false;
|
|
344
|
+
emit('\n');
|
|
345
|
+
if(congested) return;
|
|
346
|
+
}
|
|
347
|
+
if(suppressedSinceDrain){
|
|
348
|
+
const skipped=suppressedSinceDrain;
|
|
349
|
+
suppressedSinceDrain=0;
|
|
350
|
+
emit('[WARN] LIVELOG '+skipped+' presentation lines suppressed by stdout backpressure; inspect signed audit and runtime logs.\n');
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
function noteSuppressed(){
|
|
354
|
+
suppressedSinceDrain++;
|
|
355
|
+
totalSuppressed++;
|
|
356
|
+
}
|
|
329
357
|
function flush(){
|
|
330
|
-
if(active)
|
|
358
|
+
if(!active) return;
|
|
359
|
+
active=null;
|
|
360
|
+
if(congested){needsNewline=true;return;}
|
|
361
|
+
emit('\n');
|
|
331
362
|
}
|
|
332
363
|
// Inspect terminal width for every emission: resize must not erase a wrapped prior line.
|
|
333
364
|
// Unknown Unicode cell width falls back to append-only output, never truncation.
|
|
@@ -337,20 +368,24 @@ export function createLiveTimelineWriter({stream=process.stdout,displayMode='uni
|
|
|
337
368
|
}
|
|
338
369
|
function write(line,{event_type=null,request_id=null}={}){
|
|
339
370
|
if(typeof line!=='string'||!line) return;
|
|
371
|
+
if(congested){noteSuppressed();return;}
|
|
340
372
|
const tty=stream.isTTY===true&&rewrite;
|
|
341
373
|
if(!tty||!fitsOneRow(line)){
|
|
342
374
|
if(active) flush();
|
|
343
|
-
|
|
375
|
+
if(congested){noteSuppressed();return;}
|
|
376
|
+
emit(line+'\n');
|
|
344
377
|
return;
|
|
345
378
|
}
|
|
346
379
|
if(active&&event_type==='tool.result'&&request_id&&request_id===active.request_id){
|
|
347
|
-
|
|
380
|
+
active=null;emit('\r\x1b[2K'+line+'\n');return;
|
|
348
381
|
}
|
|
349
382
|
if(active) flush();
|
|
350
|
-
if(
|
|
351
|
-
|
|
383
|
+
if(congested){noteSuppressed();return;}
|
|
384
|
+
if(event_type==='tool.call'&&request_id){emit(line);active={request_id};return;}
|
|
385
|
+
emit(line+'\n');
|
|
352
386
|
}
|
|
353
|
-
return Object.freeze({write,flush,pending(){return active?{...active}:null;}
|
|
387
|
+
return Object.freeze({write,flush,pending(){return active?{...active}:null;},
|
|
388
|
+
retention(){return Object.freeze({congested,suppressed_since_drain:suppressedSinceDrain,total_suppressed:totalSuppressed});}});
|
|
354
389
|
}
|
|
355
390
|
|
|
356
391
|
export function createLiveTimelineTracker({clock=()=>Date.now(),verbose=false,foldHeartbeats=false,heartbeatBurstLimit=10,heartbeatWindowMs=3000,displayMode='unicode',rootPaths={},trace=false,slowCallMs=1000}={}){
|
|
@@ -549,6 +584,7 @@ export function createLiveTimelineTracker({clock=()=>Date.now(),verbose=false,fo
|
|
|
549
584
|
(p50!==null?' | p50 '+formatDuration(p50)+' | p95 '+formatDuration(p95)+' | p99 '+formatDuration(p99)+' | sample '+durationSamples.length+'/'+durationObservedCount:'')+
|
|
550
585
|
(slowest?' | slowest '+slowest.tool+' '+formatDuration(slowest.duration_ms):'')+
|
|
551
586
|
' | peak in-flight '+peakInFlight+
|
|
587
|
+
(pendingOverflow?' | '+pendingOverflow+' presentation spans untracked (overflow); inspect signed audit':'')+
|
|
552
588
|
(foldedHeartbeatLines?' | folded '+foldedHeartbeatLines+' heartbeat lines':'')+
|
|
553
589
|
(top?' | top: '+top:''),displayMode);
|
|
554
590
|
}
|
package/src/version.mjs
CHANGED
|
@@ -14,12 +14,13 @@ function fakePlane(){
|
|
|
14
14
|
|
|
15
15
|
test('R47 remote Commander bridge routes remote_device directly to remote-device-r44 plane',async()=>{
|
|
16
16
|
const server=fakeServer(),plane=fakePlane();
|
|
17
|
+
plane.probe=async value=>{plane.calls.push({op:'probe',value});return {device_id:value.deviceId,epoch:value.epoch,connection_id:'c',reachable:true};};
|
|
17
18
|
registerCommanderRemoteBridge(server,{remoteDevicePlane:plane});
|
|
18
19
|
assert.ok(server.tools.has('remote_device'));assert.ok(server.tools.has('remote_mcp'));
|
|
19
20
|
const tool=server.tools.get('remote_device').handler;const id='a'.repeat(64);
|
|
20
21
|
await tool({op:'pair',public_key_spki_base64:'ZmFrZQ==',display_name:'lab'});
|
|
21
22
|
await tool({op:'status',device_id:id});await tool({op:'report_online',device_id:id,epoch:1,connection_id:'c'});
|
|
22
|
-
await tool({op:'probe',device_id:id,epoch:1});await tool({op:'dispatch',device_id:id,epoch:1,capability_id:'machine.status',payload:{},request_id:'b'.repeat(64)});
|
|
23
|
+
await tool({op:'probe',device_id:id,epoch:1,connection_id:'c',nonce:'f'.repeat(64)});await tool({op:'dispatch',device_id:id,epoch:1,capability_id:'machine.status',payload:{},request_id:'b'.repeat(64)});
|
|
23
24
|
await tool({op:'revoke',device_id:id,epoch:1});await tool({op:'rotate_credential',device_id:id,epoch:1,encrypted_bundle_base64:'dG9rZW4=',expected_previous_sha256:null});await tool({op:'list'});
|
|
24
25
|
assert.deepEqual(plane.calls.map(x=>x.op),['pair','status','report_online','probe','dispatch','revoke','rotate_credential','list']);
|
|
25
26
|
});
|
|
@@ -37,3 +38,21 @@ test('R47 remote surfaces fail closed as unavailable when no remote-device-r44 p
|
|
|
37
38
|
const result=await server.tools.get('remote_device').handler({op:'list'});
|
|
38
39
|
assert.equal(result.status,'unavailable');assert.match(result.error,/remote-device-r44 plane is not configured/);
|
|
39
40
|
});
|
|
41
|
+
|
|
42
|
+
test('R146 remote readiness bridge binds the caller nonce and exact live connection',async()=>{
|
|
43
|
+
const server=fakeServer(),id='e'.repeat(64),nonce='f'.repeat(64),calls=[];
|
|
44
|
+
const plane={
|
|
45
|
+
async probe(value){calls.push(value);return {device_id:id,epoch:3,connection_id:'conn-3',reachable:true};}
|
|
46
|
+
};
|
|
47
|
+
registerCommanderRemoteBridge(server,{remoteDevicePlane:plane});
|
|
48
|
+
const registered=server.tools.get('remote_device');
|
|
49
|
+
const parsed=registered.meta.inputSchema.safeParse({op:'probe',device_id:id,epoch:3,connection_id:'conn-3',nonce});
|
|
50
|
+
assert.equal(parsed.success,true,'strict remote_device schema must accept the readiness challenge');
|
|
51
|
+
const result=await registered.handler(parsed.data);
|
|
52
|
+
assert.deepEqual(calls,[{deviceId:id,epoch:3}]);
|
|
53
|
+
assert.equal(result.device_id,id);assert.equal(result.epoch,3);
|
|
54
|
+
assert.equal(result.connection_id,'conn-3');assert.equal(result.nonce,nonce);assert.equal(result.ready,true);
|
|
55
|
+
|
|
56
|
+
plane.probe=async()=>({device_id:id,epoch:3,connection_id:'superseded',reachable:true});
|
|
57
|
+
await assert.rejects(registered.handler(parsed.data),/connection identity mismatch/i);
|
|
58
|
+
});
|
|
@@ -2,6 +2,7 @@ import test from 'node:test';
|
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
3
|
import { readFile } from 'node:fs/promises';
|
|
4
4
|
import { createHash } from 'node:crypto';
|
|
5
|
+
import { validateRemoteProbeResponseR47 } from '../src/commander-remote-relay-r47.mjs';
|
|
5
6
|
|
|
6
7
|
const sha=value=>createHash('sha256').update(value).digest('hex');
|
|
7
8
|
|
|
@@ -23,3 +24,13 @@ test('R47 relay is native MCP client and contains no Desktop Commander dependenc
|
|
|
23
24
|
assert.doesNotMatch(relay,/@wonderwhy-er\/desktop-commander/);
|
|
24
25
|
assert.equal(sha(relay).length,64);
|
|
25
26
|
});
|
|
27
|
+
|
|
28
|
+
test('R146 remote relay rejects fabricated or stale readiness acknowledgments',()=>{
|
|
29
|
+
const deviceId='a'.repeat(64),nonce='b'.repeat(64),connectionId='conn-live';
|
|
30
|
+
const good={device_id:deviceId,epoch:4,connection_id:connectionId,nonce,ready:true};
|
|
31
|
+
assert.deepEqual(validateRemoteProbeResponseR47(good,{deviceId,epoch:4,connectionId,nonce}),good);
|
|
32
|
+
assert.throws(()=>validateRemoteProbeResponseR47({...good,nonce:'c'.repeat(64)},{deviceId,epoch:4,connectionId,nonce}),/nonce mismatch/i);
|
|
33
|
+
assert.throws(()=>validateRemoteProbeResponseR47({...good,connection_id:'conn-stale'},{deviceId,epoch:4,connectionId,nonce}),/connection identity mismatch/i);
|
|
34
|
+
assert.throws(()=>validateRemoteProbeResponseR47({...good,device_id:'d'.repeat(64)},{deviceId,epoch:4,connectionId,nonce}),/device identity mismatch/i);
|
|
35
|
+
assert.throws(()=>validateRemoteProbeResponseR47({...good,ready:false},{deviceId,epoch:4,connectionId,nonce}),/not ready/i);
|
|
36
|
+
});
|
|
@@ -76,18 +76,17 @@ test('R121 correctly signed restore after user revoke is still an invalid author
|
|
|
76
76
|
await assert.rejects(candidate.auditHistory({plane:'process'}), /authority epoch transition invalid/);
|
|
77
77
|
assert.equal((await readFile(path.join(dir, 'events', eventFile))).equals(eventBytes), true);
|
|
78
78
|
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
// hot path checks the newest pair, while explicit audit must detect the
|
|
82
|
-
// invalid transition that is now deeper in history.
|
|
79
|
+
// Current protected writers must reject the already-forged transition
|
|
80
|
+
// rather than extending it and making the corrupted head appear trusted.
|
|
83
81
|
const finalNonce = randomBytes(32).toString('hex');
|
|
84
82
|
const finalGrant = Buffer.from(JSON.stringify({instance_nonce:finalNonce, fixture:'R121 final'}));
|
|
85
|
-
|
|
86
|
-
grantBytes:finalGrant, grantInstanceNonce:finalNonce});
|
|
87
|
-
assert.
|
|
83
|
+
await assert.rejects(writer.append({plane:'process', action:'grant',
|
|
84
|
+
grantBytes:finalGrant, grantInstanceNonce:finalNonce}), /authority epoch transition invalid/);
|
|
85
|
+
await assert.rejects(candidate.readHead({plane:'process'}), /authority epoch transition invalid/);
|
|
88
86
|
assert.equal((await candidate.verifyGrant({plane:'process', grantBytes:finalGrant,
|
|
89
|
-
grantInstanceNonce:finalNonce})).trusted,
|
|
87
|
+
grantInstanceNonce:finalNonce})).trusted, false);
|
|
90
88
|
await assert.rejects(candidate.auditHistory({plane:'process'}), /authority epoch transition invalid/);
|
|
89
|
+
assert.equal((await readFile(path.join(dir, 'events', eventFile))).equals(eventBytes), true);
|
|
91
90
|
} finally {
|
|
92
91
|
await rm(root, {recursive:true, force:true, maxRetries:8, retryDelay:100});
|
|
93
92
|
}
|