deadbyte-mcp 0.15.68 → 0.15.70
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 +85 -38
- package/PROVENANCE.json +52 -24
- package/README.txt +44 -31
- package/RELEASE.json +47 -17
- package/SBOM.cdx.json +2 -2
- package/TEST-MATRIX.json +44 -2
- package/bridge/package.json +1 -1
- package/controller/deadbyte-controller.ps1 +65 -1
- package/controller/deadbyte-process-policy.json +4 -4
- package/docs/R184-package-lock.json +97 -0
- package/docs/R186-OWNER-CONSENT-HANDOFF.txt +87 -0
- package/package.json +1 -1
- package/scripts/install-r186-owner-trust.ps1 +59 -0
- package/scripts/r175-focused-runner.mjs +38 -0
- package/scripts/r176-log-preview.mjs +18 -0
- package/scripts/r177-owner-trust-inventory.ps1 +36 -0
- package/scripts/r178-full-regression.mjs +21 -0
- package/scripts/r178-npm-ci.mjs +10 -0
- package/scripts/r178-npm-preflight.mjs +16 -0
- package/scripts/r178-release-gates-readonly.mjs +9 -0
- package/scripts/r178-shard-regression.mjs +25 -0
- package/scripts/r179-predecessor-registry.mjs +24 -0
- package/scripts/r179-protected-inventory.ps1 +21 -0
- package/scripts/r186-focused-runner.mjs +22 -0
- package/scripts/r186-owner-anchor-inspect.mjs +6 -0
- package/scripts/r186-owner-consent-check.mjs +86 -0
- package/scripts/r186-owner-grant-cli.mjs +45 -0
- package/scripts/r186-owner-key-local.mjs +22 -0
- package/scripts/r186-owner-review-export.mjs +34 -0
- package/src/deadbyte-cli.mjs +130 -56
- package/src/foreground-owner-recovery-r190.mjs +117 -0
- package/src/r174-owner-webauthn.mjs +176 -0
- package/src/r175-foreground-boundary.mjs +28 -0
- package/src/r186-owner-anchor.mjs +12 -0
- package/src/r186-owner-consent-preflight.mjs +63 -0
- package/src/r186-owner-grant-broker.mjs +96 -0
- package/src/r186-owner-local-signing.mjs +25 -0
- package/src/r186-owner-request.mjs +40 -0
- package/src/r186-owner-review-export.mjs +49 -0
- package/src/remote-live-log.mjs +81 -5
- package/src/remote-session-guardian.mjs +23 -0
- package/src/version.mjs +1 -1
- package/test/helpers/r175-active-pointer.mjs +28 -0
- package/test/helpers/release-lock.mjs +12 -0
- package/test/r129-sdk-security-baseline.test.mjs +2 -1
- package/test/r130-release-security-contract.test.mjs +11 -10
- package/test/r132-release-identity.test.mjs +12 -11
- package/test/r153-prepublish-identity.test.mjs +1 -1
- package/test/r155-default-live-log.test.mjs +17 -16
- package/test/r160-package-decision-manifest.test.mjs +1 -1
- package/test/r160-release-contract.test.mjs +4 -5
- package/test/r163-isolated-slot-rollback-restore.test.mjs +8 -8
- package/test/r165-live-cli-skew.test.mjs +6 -6
- package/test/r167-release-contract.test.mjs +15 -13
- package/test/r168-protected-entrypoint.test.mjs +5 -2
- package/test/r168-release-contract.test.mjs +4 -3
- package/test/r168-remote-one-shot-coherence.test.mjs +2 -1
- package/test/r172-release-contract.test.mjs +7 -7
- package/test/r173-release-contract.test.mjs +18 -17
- package/test/r174-owner-webauthn.test.mjs +144 -0
- package/test/r175-cli-flags.test.mjs +23 -0
- package/test/r175-foreground-boundary.test.mjs +87 -0
- package/test/r175-guardian-owner-boundary.test.mjs +56 -0
- package/test/r176-pretty-live-log.test.mjs +54 -0
- package/test/r177-windows-absolute-live-path.test.mjs +37 -0
- package/test/r179-absolute-path-live-log.test.mjs +40 -0
- package/test/r179-release-contract.test.mjs +62 -0
- package/test/r186-controller-owner-grant.test.mjs +20 -0
- package/test/r186-owner-consent-preflight.test.mjs +98 -0
- package/test/r186-owner-enrollment.test.mjs +20 -0
- package/test/r186-owner-grant-broker.test.mjs +85 -0
- package/test/r186-owner-local-signing.test.mjs +30 -0
- package/test/r186-owner-request.test.mjs +28 -0
- package/test/r186-owner-review-export.test.mjs +52 -0
- package/test/r190-foreground-cli.test.mjs +41 -0
- package/test/r190-foreground-owner-recovery.test.mjs +61 -0
- package/test/r190-release-contract.test.mjs +33 -0
- package/test/r48-release-contract.test.mjs +2 -1
- package/test/r51-runtime-coherence.test.mjs +4 -2
- package/test/r58-cleanup-retention.test.mjs +5 -12
- package/test/r58-concurrent-recovery.test.mjs +5 -1
- package/test/r58-stale-owner.test.mjs +9 -3
- package/test/r59-tool-surface-compat.test.mjs +1 -1
- package/test/remote-live-log-r37.test.mjs +2 -2
- package/test/remote-live-log-recovery-r36.test.mjs +1 -1
- package/test/remote-session-cli.test.mjs +92 -126
- package/node_modules/which/CHANGELOG.md +0 -166
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import {createHash,createPublicKey} from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
export function createR186OwnerAnchor(publicKeyPem){
|
|
4
|
+
if(typeof publicKeyPem!=='string'||publicKeyPem.length<32||publicKeyPem.length>8192||/PRIVATE KEY/.test(publicKeyPem))
|
|
5
|
+
throw new Error('R186 owner enrollment requires a public PEM only');
|
|
6
|
+
let key;try{key=createPublicKey(publicKeyPem);}catch{throw new Error('R186 owner public key invalid');}
|
|
7
|
+
if(key.asymmetricKeyType!=='ed25519')throw new Error('R186 owner public key must be Ed25519');
|
|
8
|
+
const canonicalPem=key.export({type:'spki',format:'pem'}).toString();
|
|
9
|
+
const owner_key_id=createHash('sha256').update(key.export({type:'spki',format:'der'})).digest('hex');
|
|
10
|
+
return Object.freeze({anchor:Object.freeze({schema:'deadbyte.r186-owner-anchor.v1',owner_key_id}),
|
|
11
|
+
public_key_pem:canonicalPem});
|
|
12
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// R186: only a read-only verifier for independently enrolled owner consent.
|
|
2
|
+
// A successful result MUST NOT be treated as an Autonomous grant. The caller
|
|
3
|
+
// must separately prove protected enrollment, single-use consumption and epoch.
|
|
4
|
+
import {createHash,createPublicKey} from 'node:crypto';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import {verifyProjectOwnerApproval} from './r70-owner-approval.mjs';
|
|
7
|
+
|
|
8
|
+
const SHA=/^[0-9a-f]{64}$/;
|
|
9
|
+
function exact(value,fields,label){
|
|
10
|
+
if(!value||typeof value!=='object'||Array.isArray(value)||
|
|
11
|
+
Object.keys(value).length!==fields.length||
|
|
12
|
+
fields.some(field=>!Object.hasOwn(value,field)))
|
|
13
|
+
throw new Error('R186 '+label+' invalid');
|
|
14
|
+
}
|
|
15
|
+
export async function verifyR186OwnerConsent({ownerTrust,ownerPublicKeyPem,approval,expected,now=new Date()}={}){
|
|
16
|
+
exact(ownerTrust,['schema','owner_key_id'],'owner anchor');
|
|
17
|
+
if(ownerTrust.schema!=='deadbyte.r186-owner-anchor.v1'||
|
|
18
|
+
typeof ownerTrust.owner_key_id!=='string'||!SHA.test(ownerTrust.owner_key_id))
|
|
19
|
+
throw new Error('R186 owner anchor schema or fingerprint invalid');
|
|
20
|
+
if(typeof ownerPublicKeyPem!=='string'||ownerPublicKeyPem.length>8192)
|
|
21
|
+
throw new Error('R186 owner anchor public key invalid');
|
|
22
|
+
let pub;
|
|
23
|
+
try{pub=createPublicKey(ownerPublicKeyPem);}catch{throw new Error('R186 owner anchor public key invalid');}
|
|
24
|
+
if(pub.asymmetricKeyType!=='ed25519')throw new Error('R186 owner anchor must be Ed25519');
|
|
25
|
+
const keySha=createHash('sha256').update(pub.export({type:'spki',format:'der'})).digest('hex');
|
|
26
|
+
if(ownerTrust.owner_key_id!==keySha)
|
|
27
|
+
throw new Error('R186 owner anchor public key mismatch');
|
|
28
|
+
const approved=await verifyProjectOwnerApproval({approval,ownerPublicKey:pub,expected,now});
|
|
29
|
+
return Object.freeze({status:'verified_readonly',authority_granted:false,
|
|
30
|
+
owner_key_id:keySha,subject_sha256:approved.subject_sha256,
|
|
31
|
+
root_id:approved.root_id,project_root:approved.project_root,
|
|
32
|
+
expires_at_utc:approved.expires_at_utc,
|
|
33
|
+
next_gate:'PROTECTED_OWNER_ENROLLMENT_AND_SINGLE_USE_EPOCH_ISSUANCE_REQUIRED'});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function verifyR186PolicyRootBinding({
|
|
37
|
+
codingPolicy,autonomousPolicy,rootId,projectRoot,codingPolicySha256
|
|
38
|
+
}={}){
|
|
39
|
+
if(!codingPolicy||typeof codingPolicy!=='object'||
|
|
40
|
+
codingPolicy.schema!=='deadbyte.coding-policy.v1'||codingPolicy.enabled!==true)
|
|
41
|
+
throw new Error('R186 coding policy schema invalid');
|
|
42
|
+
if(!autonomousPolicy||typeof autonomousPolicy!=='object'||
|
|
43
|
+
autonomousPolicy.schema!=='deadbyte.autonomous-policy.v1'||autonomousPolicy.enabled!==true)
|
|
44
|
+
throw new Error('R186 autonomous policy schema invalid');
|
|
45
|
+
if(typeof rootId!=='string'||!/^[A-Za-z0-9._-]{1,64}$/.test(rootId)||
|
|
46
|
+
!Array.isArray(autonomousPolicy.root_ids)||
|
|
47
|
+
autonomousPolicy.root_ids.length!==1||autonomousPolicy.root_ids[0]!==rootId)
|
|
48
|
+
throw new Error('R186 Autonomous root vector exceeds owner-reviewed scope');
|
|
49
|
+
if(typeof codingPolicySha256!=='string'||!SHA.test(codingPolicySha256)||
|
|
50
|
+
autonomousPolicy.coding_policy_sha256!==codingPolicySha256)
|
|
51
|
+
throw new Error('R186 coding-policy digest binding mismatch');
|
|
52
|
+
const bound=codingPolicy.roots?.[rootId]?.path;
|
|
53
|
+
if(typeof bound!=='string'||typeof projectRoot!=='string'||
|
|
54
|
+
!path.win32.isAbsolute(bound)||!path.win32.isAbsolute(projectRoot)||
|
|
55
|
+
bound.startsWith('\\\\')||projectRoot.startsWith('\\\\'))
|
|
56
|
+
throw new Error('R186 coding project root is not a real absolute drive path');
|
|
57
|
+
const actual=path.win32.normalize(bound).toLowerCase();
|
|
58
|
+
const requested=path.win32.normalize(projectRoot).toLowerCase();
|
|
59
|
+
if(actual!==requested||
|
|
60
|
+
actual===path.win32.parse(actual).root.toLowerCase())
|
|
61
|
+
throw new Error('R186 reviewed root must match selected Coding project root');
|
|
62
|
+
return Object.freeze({root_id:rootId,project_root:bound});
|
|
63
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// Single-use R186 Autonomous grant issuance. This module does not enroll owner
|
|
2
|
+
// keys and accepts no caller-selected authority scope: the policy, reviewed
|
|
3
|
+
// manifest and independently protected owner anchor are the complete subject.
|
|
4
|
+
import {createHash,createPrivateKey,createPublicKey,randomBytes,sign,verify} from 'node:crypto';
|
|
5
|
+
import {mkdir,open,readFile,writeFile} from 'node:fs/promises';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import {canonicalJson} from './canonical-json.mjs';
|
|
8
|
+
import {createAuthorityEpochStore} from './authority-continuity-r65.mjs';
|
|
9
|
+
import {verifyR186OwnerConsent,verifyR186PolicyRootBinding} from './r186-owner-consent-preflight.mjs';
|
|
10
|
+
|
|
11
|
+
const SHA=/^[0-9a-f]{64}$/;
|
|
12
|
+
const TERMINAL_DOMAIN=Buffer.from('DEADBYTE-R186-OWNER-GRANT-TERMINAL\0','utf8');
|
|
13
|
+
const sha=bytes=>createHash('sha256').update(bytes).digest('hex');
|
|
14
|
+
const bytes=value=>Buffer.from(canonicalJson(value),'utf8');
|
|
15
|
+
async function writeExclusive(file,data){
|
|
16
|
+
await mkdir(path.dirname(file),{recursive:true});
|
|
17
|
+
const handle=await open(file,'wx',0o600);
|
|
18
|
+
try{await handle.writeFile(data);await handle.sync();}
|
|
19
|
+
finally{await handle.close();}
|
|
20
|
+
}
|
|
21
|
+
function insist(condition,message){if(!condition)throw new Error(`R186 owner grant blocked: ${message}`);}
|
|
22
|
+
async function signTerminal(runtimeDir,body){
|
|
23
|
+
const trustDir=path.join(path.dirname(path.resolve(runtimeDir)),'trust');
|
|
24
|
+
const [privatePem,publicPem]=await Promise.all([
|
|
25
|
+
readFile(path.join(trustDir,'ed25519-private.pem')),readFile(path.join(trustDir,'ed25519-public.pem'))]);
|
|
26
|
+
const privateKey=createPrivateKey(privatePem),publicKey=createPublicKey(publicPem);
|
|
27
|
+
insist(privateKey.asymmetricKeyType==='ed25519'&&publicKey.asymmetricKeyType==='ed25519','machine receipt key type invalid');
|
|
28
|
+
const privateKeyId=sha(createPublicKey(privateKey).export({type:'spki',format:'der'}));
|
|
29
|
+
const publicKeyId=sha(publicKey.export({type:'spki',format:'der'}));
|
|
30
|
+
insist(privateKeyId===publicKeyId,'machine receipt key pair mismatch');
|
|
31
|
+
const signature=sign(null,Buffer.concat([TERMINAL_DOMAIN,bytes(body)]),privateKey);
|
|
32
|
+
insist(verify(null,Buffer.concat([TERMINAL_DOMAIN,bytes(body)]),publicKey,signature),'machine terminal signature self-check failed');
|
|
33
|
+
return Object.freeze({algorithm:'ed25519',domain:'DEADBYTE-R186-OWNER-GRANT-TERMINAL',key_id:publicKeyId,signature_base64:signature.toString('base64')});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function issueR186AutonomousGrant({
|
|
37
|
+
runtimeDir,grantFile,codingPolicy,codingPolicyBytes,autonomousPolicy,autonomousPolicyBytes,
|
|
38
|
+
releaseManifestBytes,ownerTrust,ownerPublicKeyPem,approval,now=new Date(),epochStore=null
|
|
39
|
+
}={}){
|
|
40
|
+
insist(typeof runtimeDir==='string'&&path.isAbsolute(runtimeDir),'runtime directory invalid');
|
|
41
|
+
insist(typeof grantFile==='string'&&path.isAbsolute(grantFile),'grant path invalid');
|
|
42
|
+
for(const [name,value] of [['Coding policy',codingPolicyBytes],['Autonomous policy',autonomousPolicyBytes],['release manifest',releaseManifestBytes]])
|
|
43
|
+
insist(Buffer.isBuffer(value)&&value.length>0,`${name} bytes invalid`);
|
|
44
|
+
insist(autonomousPolicy?.schema==='deadbyte.autonomous-policy.v1'&&autonomousPolicy.enabled===true,'Autonomous policy invalid');
|
|
45
|
+
insist(path.resolve(autonomousPolicy.lease_file)===path.resolve(grantFile),'policy grant path mismatch');
|
|
46
|
+
const runtimePrefix=path.resolve(runtimeDir)+path.sep;
|
|
47
|
+
insist(path.resolve(grantFile).startsWith(runtimePrefix),'grant path outside runtime');
|
|
48
|
+
const codingSha=sha(codingPolicyBytes),autonomousSha=sha(autonomousPolicyBytes),manifestSha=sha(releaseManifestBytes);
|
|
49
|
+
const rootId=Array.isArray(autonomousPolicy.root_ids)&&autonomousPolicy.root_ids.length===1?autonomousPolicy.root_ids[0]:null;
|
|
50
|
+
const projectRoot=rootId?codingPolicy?.roots?.[rootId]?.path:null;
|
|
51
|
+
const binding=verifyR186PolicyRootBinding({codingPolicy,autonomousPolicy,rootId,projectRoot,codingPolicySha256:codingSha});
|
|
52
|
+
const consent=await verifyR186OwnerConsent({ownerTrust,ownerPublicKeyPem,approval,now,expected:{
|
|
53
|
+
rootId:binding.root_id,projectRoot:binding.project_root,codingPolicySha256:codingSha,
|
|
54
|
+
autonomousPolicySha256:autonomousSha,reviewedArtifactSha256:manifestSha
|
|
55
|
+
}});
|
|
56
|
+
const evidenceDir=path.join(runtimeDir,'owner-consent','r186');
|
|
57
|
+
const claimPath=path.join(evidenceDir,`${consent.subject_sha256}.claim.json`);
|
|
58
|
+
const terminalPath=path.join(evidenceDir,`${consent.subject_sha256}.terminal.json`);
|
|
59
|
+
try{await readFile(terminalPath);throw new Error('R186 owner approval already consumed; replay denied');}
|
|
60
|
+
catch(error){if(error?.code!=='ENOENT')throw error;}
|
|
61
|
+
const nonce=randomBytes(32).toString('hex');
|
|
62
|
+
const claim={schema:'deadbyte.r186-owner-grant-claim.v1',subject_sha256:consent.subject_sha256,
|
|
63
|
+
owner_key_id:consent.owner_key_id,policy_sha256:autonomousSha,coding_policy_sha256:codingSha,
|
|
64
|
+
release_manifest_sha256:manifestSha,root_id:binding.root_id,project_root:binding.project_root,
|
|
65
|
+
grant_instance_nonce:nonce,claimed_at_utc:now.toISOString()};
|
|
66
|
+
try{await writeExclusive(claimPath,bytes(claim));}
|
|
67
|
+
catch(error){if(error?.code==='EEXIST')throw new Error('R186 owner approval already consumed or in progress; replay denied');throw error;}
|
|
68
|
+
const grant={schema:'deadbyte.autonomous-grant.v2',policy_sha256:autonomousSha,
|
|
69
|
+
coding_policy_sha256:codingSha,instance_nonce:nonce,root_ids:[binding.root_id],
|
|
70
|
+
granted_at_utc:now.toISOString(),expires_at_utc:consent.expires_at_utc};
|
|
71
|
+
const grantBytes=bytes(grant);
|
|
72
|
+
try{await writeExclusive(grantFile,grantBytes);}
|
|
73
|
+
catch(error){throw Object.assign(new Error(`R186 grant write failed after durable claim: ${error?.message??error}`),{outcome:'OUTCOME_UNKNOWN'});}
|
|
74
|
+
const store=epochStore??createAuthorityEpochStore({runtimeDir,policyBytes:autonomousPolicyBytes});
|
|
75
|
+
let head;
|
|
76
|
+
try{head=await store.append({plane:'autonomous',action:'grant',grantBytes,grantInstanceNonce:nonce,updateId:null});}
|
|
77
|
+
catch(error){throw Object.assign(new Error(`R186 epoch issuance failed after grant write: ${error?.message??error}`),{outcome:'OUTCOME_UNKNOWN'});}
|
|
78
|
+
insist(SHA.test(String(head?.event_sha256)),'epoch identity invalid after issuance');
|
|
79
|
+
const verified=await store.verifyGrant({plane:'autonomous',grantBytes,grantInstanceNonce:nonce,policyBytes:autonomousPolicyBytes});
|
|
80
|
+
insist(verified?.trusted===true&&verified.event_sha256===head.event_sha256,'grant epoch read-back failed');
|
|
81
|
+
insist((await readFile(grantFile)).equals(grantBytes),'grant changed after epoch issuance');
|
|
82
|
+
const terminalBody={schema:'deadbyte.r186-owner-grant-terminal.v1',status:'GRANTED',
|
|
83
|
+
subject_sha256:consent.subject_sha256,owner_key_id:consent.owner_key_id,
|
|
84
|
+
claim_sha256:sha(bytes(claim)),grant_sha256:sha(grantBytes),grant_instance_nonce:nonce,
|
|
85
|
+
policy_sha256:autonomousSha,coding_policy_sha256:codingSha,release_manifest_sha256:manifestSha,
|
|
86
|
+
root_id:binding.root_id,project_root:binding.project_root,epoch_event_sha256:head.event_sha256,
|
|
87
|
+
epoch_seq:head.seq,expires_at_utc:consent.expires_at_utc,completed_at_utc:new Date().toISOString()};
|
|
88
|
+
const attestation=await signTerminal(runtimeDir,terminalBody);
|
|
89
|
+
const unsigned={body:terminalBody,attestation};
|
|
90
|
+
const terminal={...unsigned,receipt_sha256:sha(bytes(unsigned))};
|
|
91
|
+
try{await writeExclusive(terminalPath,bytes(terminal));}
|
|
92
|
+
catch(error){throw Object.assign(new Error(`R186 terminal receipt write failed: ${error?.message??error}`),{outcome:'OUTCOME_UNKNOWN'});}
|
|
93
|
+
return Object.freeze({status:'GRANTED',authority_granted:true,subject_sha256:consent.subject_sha256,
|
|
94
|
+
grant_sha256:terminalBody.grant_sha256,epoch_event_sha256:head.event_sha256,epoch_seq:head.seq,
|
|
95
|
+
terminal_path:terminalPath,receipt_sha256:terminal.receipt_sha256});
|
|
96
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Human-invoked local signer. Callers receive approval JSON only; the private
|
|
2
|
+
// key is never returned or embedded in a request/receipt.
|
|
3
|
+
import {createHash,createPrivateKey,createPublicKey,sign} from 'node:crypto';
|
|
4
|
+
import {canonicalJson} from './canonical-json.mjs';
|
|
5
|
+
|
|
6
|
+
const DOMAIN=Buffer.from('DEADBYTE-R70-PROJECT-OWNER-APPROVAL\0','utf8');
|
|
7
|
+
const SHA=/^[0-9a-f]{64}$/;
|
|
8
|
+
const fields=['schema','subject','subject_sha256','owner_signature_present','authority_granted'];
|
|
9
|
+
const hash=bytes=>createHash('sha256').update(bytes).digest('hex');
|
|
10
|
+
export function createR186OwnerApproval({request,privateKeyPem}={}){
|
|
11
|
+
if(!request||typeof request!=='object'||Array.isArray(request)||Object.keys(request).length!==fields.length||fields.some(k=>!Object.hasOwn(request,k))||
|
|
12
|
+
request.schema!=='deadbyte.r186-owner-review-request.v1'||request.owner_signature_present!==false||request.authority_granted!==false)
|
|
13
|
+
throw new Error('R186 owner review request invalid');
|
|
14
|
+
const subjectSha=hash(Buffer.from(canonicalJson(request.subject),'utf8'));
|
|
15
|
+
if(!SHA.test(String(request.subject_sha256))||request.subject_sha256!==subjectSha)
|
|
16
|
+
throw new Error('R186 owner review request subject mismatch');
|
|
17
|
+
if(typeof privateKeyPem!=='string'||!/PRIVATE KEY/.test(privateKeyPem))throw new Error('R186 owner private key invalid');
|
|
18
|
+
let privateKey;try{privateKey=createPrivateKey(privateKeyPem);}catch{throw new Error('R186 owner private key invalid');}
|
|
19
|
+
if(privateKey.asymmetricKeyType!=='ed25519')throw new Error('R186 owner private key must be Ed25519');
|
|
20
|
+
const publicKey=createPublicKey(privateKey);
|
|
21
|
+
const owner_key_id=hash(publicKey.export({type:'spki',format:'der'}));
|
|
22
|
+
const signed={schema:'deadbyte.project-owner-approval.r70.v1',subject_sha256:subjectSha,owner_key_id};
|
|
23
|
+
const signature_base64=sign(null,Buffer.concat([DOMAIN,Buffer.from(canonicalJson(signed),'utf8')]),privateKey).toString('base64');
|
|
24
|
+
return Object.freeze({...signed,subject:request.subject,signature_base64});
|
|
25
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Owner-review subject construction only. No keys, signatures or grants.
|
|
2
|
+
import {createHash} from 'node:crypto';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import {canonicalJson} from './canonical-json.mjs';
|
|
5
|
+
|
|
6
|
+
const SHA=/^[0-9a-f]{64}$/;
|
|
7
|
+
const ROOT=/^[A-Za-z0-9._-]{1,64}$/;
|
|
8
|
+
export function createR186OwnerReviewRequest({
|
|
9
|
+
rootId,projectRoot,codingPolicySha256,autonomousPolicySha256,
|
|
10
|
+
reviewedArtifactSha256,now=new Date(),durationMs=600000
|
|
11
|
+
}={}){
|
|
12
|
+
if(typeof rootId!=='string'||!ROOT.test(rootId))
|
|
13
|
+
throw new Error('R186 invalid root ID');
|
|
14
|
+
if(typeof projectRoot!=='string'||!path.win32.isAbsolute(projectRoot)||
|
|
15
|
+
projectRoot.startsWith('\\\\'))
|
|
16
|
+
throw new Error('R186 invalid project root');
|
|
17
|
+
for(const [name,value] of [
|
|
18
|
+
['codingPolicySha256',codingPolicySha256],
|
|
19
|
+
['autonomousPolicySha256',autonomousPolicySha256],
|
|
20
|
+
['reviewedArtifactSha256',reviewedArtifactSha256]
|
|
21
|
+
])if(typeof value!=='string'||!SHA.test(value))
|
|
22
|
+
throw new Error('R186 invalid '+name);
|
|
23
|
+
if(!(now instanceof Date)||!Number.isFinite(now.getTime())||
|
|
24
|
+
!Number.isSafeInteger(durationMs)||durationMs<1000||durationMs>3600000)
|
|
25
|
+
throw new Error('R186 invalid bounded consent duration');
|
|
26
|
+
const issued_at_utc=now.toISOString();
|
|
27
|
+
const expires_at_utc=new Date(now.getTime()+durationMs).toISOString();
|
|
28
|
+
const subject=Object.freeze({
|
|
29
|
+
schema:'deadbyte.project-authority-subject.r70.v1',
|
|
30
|
+
scope:'coding_autonomous_native_diff',
|
|
31
|
+
root_id:rootId,project_root:path.win32.normalize(projectRoot),
|
|
32
|
+
coding_policy_sha256:codingPolicySha256,
|
|
33
|
+
autonomous_policy_sha256:autonomousPolicySha256,
|
|
34
|
+
reviewed_artifact_sha256:reviewedArtifactSha256,
|
|
35
|
+
issued_at_utc,expires_at_utc
|
|
36
|
+
});
|
|
37
|
+
const subject_sha256=createHash('sha256').update(Buffer.from(canonicalJson(subject))).digest('hex');
|
|
38
|
+
return Object.freeze({schema:'deadbyte.r186-owner-review-request.v1',
|
|
39
|
+
subject,subject_sha256,owner_signature_present:false,authority_granted:false});
|
|
40
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Read-only export of the exact subject a physical owner may review and sign.
|
|
2
|
+
// This boundary neither reads a private key nor creates/changes authority.
|
|
3
|
+
import {createHash} from 'node:crypto';
|
|
4
|
+
import {lstat,open,readFile} from 'node:fs/promises';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import {canonicalJson} from './canonical-json.mjs';
|
|
7
|
+
import {verifyR186PolicyRootBinding} from './r186-owner-consent-preflight.mjs';
|
|
8
|
+
import {createR186OwnerReviewRequest} from './r186-owner-request.mjs';
|
|
9
|
+
|
|
10
|
+
const sha=bytes=>createHash('sha256').update(bytes).digest('hex');
|
|
11
|
+
async function boundedRegular(file,limit,label){
|
|
12
|
+
const info=await lstat(file);
|
|
13
|
+
if(!info.isFile()||info.isSymbolicLink()||info.size<1||info.size>limit)
|
|
14
|
+
throw new Error(`R186 ${label} is a reparse, symlink or invalid file`);
|
|
15
|
+
return readFile(file);
|
|
16
|
+
}
|
|
17
|
+
async function writeExclusive(file,bytes){
|
|
18
|
+
const parent=await lstat(path.dirname(file));
|
|
19
|
+
if(!parent.isDirectory()||parent.isSymbolicLink())
|
|
20
|
+
throw new Error('R186 review output parent is a reparse or symlink');
|
|
21
|
+
const handle=await open(file,'wx',0o600);
|
|
22
|
+
try{await handle.writeFile(bytes);await handle.sync();}
|
|
23
|
+
finally{await handle.close();}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function writeR186OwnerReviewRequest({
|
|
27
|
+
rootId,projectRoot,codingPolicyPath,autonomousPolicyPath,manifestPath,outputPath,
|
|
28
|
+
now=new Date(),durationMs=600000
|
|
29
|
+
}={}){
|
|
30
|
+
for(const [name,value] of [['project root',projectRoot],['Coding policy',codingPolicyPath],
|
|
31
|
+
['Autonomous policy',autonomousPolicyPath],['manifest',manifestPath],['output',outputPath]]){
|
|
32
|
+
if(typeof value!=='string'||!path.isAbsolute(value))throw new Error(`R186 ${name} path must be absolute`);
|
|
33
|
+
}
|
|
34
|
+
const [codingBytes,autonomousBytes,manifestBytes]=await Promise.all([
|
|
35
|
+
boundedRegular(codingPolicyPath,1024*1024,'Coding policy'),
|
|
36
|
+
boundedRegular(autonomousPolicyPath,1024*1024,'Autonomous policy'),
|
|
37
|
+
boundedRegular(manifestPath,512*1024,'release manifest')]);
|
|
38
|
+
const codingPolicy=JSON.parse(codingBytes.toString('utf8'));
|
|
39
|
+
const autonomousPolicy=JSON.parse(autonomousBytes.toString('utf8'));
|
|
40
|
+
const codingPolicySha256=sha(codingBytes);
|
|
41
|
+
const binding=verifyR186PolicyRootBinding({codingPolicy,autonomousPolicy,rootId,
|
|
42
|
+
projectRoot,codingPolicySha256});
|
|
43
|
+
const request=createR186OwnerReviewRequest({rootId:binding.root_id,
|
|
44
|
+
projectRoot:binding.project_root,codingPolicySha256,
|
|
45
|
+
autonomousPolicySha256:sha(autonomousBytes),reviewedArtifactSha256:sha(manifestBytes),
|
|
46
|
+
now,durationMs});
|
|
47
|
+
await writeExclusive(outputPath,Buffer.from(`${canonicalJson(request)}\n`,'utf8'));
|
|
48
|
+
return request;
|
|
49
|
+
}
|
package/src/remote-live-log.mjs
CHANGED
|
@@ -11,7 +11,7 @@ const LIFECYCLE=Object.freeze({
|
|
|
11
11
|
session_new:'[SESSION] New foreground session',
|
|
12
12
|
session_restored:'♻️ [SESSION] Previous result stream replayed',
|
|
13
13
|
ready:'✅ [READY] DEADBYTE MCP online',
|
|
14
|
-
cleanup:'
|
|
14
|
+
cleanup:'[SESSION] Closing observation session...'
|
|
15
15
|
});
|
|
16
16
|
const LIFECYCLE_ASCII=Object.freeze({
|
|
17
17
|
device_start:'[DEVICE] Starting DEADBYTE MCP...',
|
|
@@ -20,7 +20,7 @@ const LIFECYCLE_ASCII=Object.freeze({
|
|
|
20
20
|
session_new:'[SESSION] New foreground session',
|
|
21
21
|
session_restored:'[SESSION] Previous result stream replayed',
|
|
22
22
|
ready:'[READY] DEADBYTE MCP online',
|
|
23
|
-
cleanup:'[
|
|
23
|
+
cleanup:'[SESSION] Closing observation session...'
|
|
24
24
|
});
|
|
25
25
|
|
|
26
26
|
function safeAtom(value){
|
|
@@ -104,13 +104,33 @@ function rootedPath(args,key='path',rootPaths={}){
|
|
|
104
104
|
if(base){
|
|
105
105
|
const flavor=/^(?:[A-Za-z]:[\\/]|\\\\)/.test(base)?path.win32:path.posix;
|
|
106
106
|
const absolute=p==='.'?flavor.resolve(base):flavor.resolve(base,p);
|
|
107
|
+
const relative=flavor.relative(flavor.resolve(base),absolute);
|
|
108
|
+
// A display-only resolver must not misleadingly mark traversal as in-root.
|
|
109
|
+
if(relative==='..'||relative.startsWith('..'+flavor.sep)||flavor.isAbsolute(relative))
|
|
110
|
+
return '[outside root='+root+'] '+p;
|
|
107
111
|
return absolute+' [root='+root+']';
|
|
108
112
|
}
|
|
113
|
+
// We do not invent drive letters when the trusted runtime policy has
|
|
114
|
+
// not supplied the actual root mapping.
|
|
109
115
|
return root+':'+p;
|
|
110
116
|
}
|
|
111
117
|
if(root) return 'root='+root;
|
|
112
118
|
return p?'path='+p:'';
|
|
113
119
|
}
|
|
120
|
+
function detailedDisplayPaths(args,rootPaths){
|
|
121
|
+
const safe=sanitizeAuditArgs(args??{});
|
|
122
|
+
const paths=[];
|
|
123
|
+
const entry=(key,label)=>{
|
|
124
|
+
if(!safePathAtom(safe?.[key]))return;
|
|
125
|
+
const value=rootedPath(safe,key,rootPaths)+(safe.root_id&&!safePathAtom(rootPaths?.[safe.root_id])?' [unresolved root]':'');
|
|
126
|
+
if(value)paths.push((label?label+': ':'')+value);
|
|
127
|
+
};
|
|
128
|
+
if(safe?.source_path&&safe?.destination_path){
|
|
129
|
+
entry('source_path','FROM');entry('destination_path','TO');
|
|
130
|
+
}else if(safe?.path)entry('path','');
|
|
131
|
+
else if(safe?.script_path)entry('script_path','SCRIPT');
|
|
132
|
+
return paths;
|
|
133
|
+
}
|
|
114
134
|
function displayToolName(tool,args){
|
|
115
135
|
const op=safeAtom(args?.op);
|
|
116
136
|
if(op&&['machine_fs','coding_inspect','process_session','desktop_control','machine_observe'].includes(tool)) return tool+'.'+op;
|
|
@@ -399,10 +419,12 @@ export function createLiveTimelineWriter({stream=process.stdout,displayMode='uni
|
|
|
399
419
|
|
|
400
420
|
export function defaultForegroundTimelineOptions({verbose=false}={}){
|
|
401
421
|
if(typeof verbose!=='boolean')throw new Error('default live timeline verbosity invalid');
|
|
402
|
-
|
|
422
|
+
// R176 default: readable emoji timeline with compact heartbeat groups.
|
|
423
|
+
// Commander/verbose presentation remains explicitly selectable by callers.
|
|
424
|
+
return Object.freeze({commanderStyle:false,foldHeartbeats:!verbose,prettyStyle:!verbose});
|
|
403
425
|
}
|
|
404
426
|
|
|
405
|
-
export function createLiveTimelineTracker({clock=()=>Date.now(),verbose=false,foldHeartbeats=false,heartbeatBurstLimit=10,heartbeatWindowMs=3000,displayMode='unicode',rootPaths={},trace=false,slowCallMs=1000,commanderStyle=false}={}){
|
|
427
|
+
export function createLiveTimelineTracker({clock=()=>Date.now(),verbose=false,foldHeartbeats=false,heartbeatBurstLimit=10,heartbeatWindowMs=3000,displayMode='unicode',rootPaths={},trace=false,slowCallMs=1000,commanderStyle=false,prettyStyle=false}={}){
|
|
406
428
|
if(typeof clock!=='function') throw new Error('live timeline clock must be a function');
|
|
407
429
|
if(!REMOTE_DISPLAY_MODES.includes(displayMode)) throw new Error('remote display mode invalid');
|
|
408
430
|
if(!Number.isInteger(heartbeatBurstLimit)||heartbeatBurstLimit<2||heartbeatBurstLimit>100) throw new Error('heartbeatBurstLimit invalid');
|
|
@@ -410,6 +432,7 @@ export function createLiveTimelineTracker({clock=()=>Date.now(),verbose=false,fo
|
|
|
410
432
|
if(!rootPaths||typeof rootPaths!=='object'||Array.isArray(rootPaths)) throw new Error('live timeline rootPaths invalid');
|
|
411
433
|
if(typeof trace!=='boolean') throw new Error('live timeline trace invalid');
|
|
412
434
|
if(typeof commanderStyle!=='boolean') throw new Error('live timeline commanderStyle invalid');
|
|
435
|
+
if(typeof prettyStyle!=='boolean') throw new Error('live timeline prettyStyle invalid');
|
|
413
436
|
if(typeof foldHeartbeats!=='boolean') throw new Error('live timeline foldHeartbeats invalid');
|
|
414
437
|
if(!Number.isInteger(slowCallMs)||slowCallMs<100||slowCallMs>600000) throw new Error('live timeline slowCallMs invalid');
|
|
415
438
|
const pending=new Map();
|
|
@@ -428,6 +451,23 @@ export function createLiveTimelineTracker({clock=()=>Date.now(),verbose=false,fo
|
|
|
428
451
|
return visual+' '+displayClock(eventMs(event,clock))+' '+seq+'['+shortSpanId(event.request_id)+'] '+tool;
|
|
429
452
|
}
|
|
430
453
|
function traceSuffix(meta){return trace&&meta?' '+traceText(meta,displayMode):'';}
|
|
454
|
+
function prettyLine(event,tool,tag,detail='',glyph=null,displayPaths=[]){
|
|
455
|
+
const time=displayClock(eventMs(event,clock)).slice(0,8);
|
|
456
|
+
const title=tool.length>30?tool.slice(0,29)+'…':tool;
|
|
457
|
+
// Spaces avoid incorrect console-cell widths for double-width emoji.
|
|
458
|
+
const icon=glyph??iconForTool(tool,displayMode);
|
|
459
|
+
const leader=displayMode==='ascii'?' | ':' │ ';
|
|
460
|
+
const shortId=shortSpanId(event.request_id);
|
|
461
|
+
const label=title.padEnd(31,' ');
|
|
462
|
+
const headline=time+' '+icon+' '+tag.padEnd(8,' ')+' '+label+
|
|
463
|
+
leader+'['+shortId+']'+(detail?' '+clipped(detail,220):'');
|
|
464
|
+
if(!displayPaths.length)return remoteDisplayText(headline,displayMode);
|
|
465
|
+
const pathIcon=displayMode==='ascii'?'PATH: ':'📁 ';
|
|
466
|
+
// Paths are not shortened to an ambiguous basename: retain the Windows
|
|
467
|
+
// drive, directory tree and policy root. Respect the bounded input limit.
|
|
468
|
+
const descriptions=displayPaths.map(p=>' '+pathIcon+p);
|
|
469
|
+
return remoteDisplayText([headline,...descriptions].join('\n'),displayMode);
|
|
470
|
+
}
|
|
431
471
|
function resultLine(event,span,sequence){
|
|
432
472
|
const outcome=outcomeClass(event);
|
|
433
473
|
const error=outcome==='error';
|
|
@@ -458,6 +498,16 @@ export function createLiveTimelineTracker({clock=()=>Date.now(),verbose=false,fo
|
|
|
458
498
|
const prefixIcon=unknown?(displayMode==='ascii'?'WARN':'⚠️'):null;
|
|
459
499
|
const evidence=receipt?' '+(displayMode==='ascii'?'RECEIPT':'🧾 receipt')+(displayMode==='ascii'?'=':'=')+receipt:'';
|
|
460
500
|
const reasonSep=displayMode==='ascii'?' - ':' — ';
|
|
501
|
+
if(prettyStyle){
|
|
502
|
+
const tag=unknown?'UNKNOWN':pendingResult?'PENDING':error?(rawStatus==='denied'?'DENIED':'FAIL'):'PASS';
|
|
503
|
+
const glyph=displayMode==='ascii'?(unknown?'!':error?'X':pendingResult?'~':'+'):
|
|
504
|
+
unknown?'⚠️':error?'❌':pendingResult?'⏳':'✅';
|
|
505
|
+
const pathList=span?.display_paths??[];
|
|
506
|
+
const nonPathSummary=pathList.length?summary.replace(pathList[0],'').trim():summary;
|
|
507
|
+
const detail=[duration,trace&&span?.trace_meta?traceText(span.trace_meta,displayMode):'',nonPathSummary,statusDetail.trim(),receipt?'receipt='+receipt:'',
|
|
508
|
+
reason?reason:''].filter(Boolean).join(' | ');
|
|
509
|
+
return prettyLine(event,displayTool,tag,detail,glyph,pathList);
|
|
510
|
+
}
|
|
461
511
|
if(commanderStyle){
|
|
462
512
|
const tag=unknown?'OUTCOME_UNKNOWN':pendingResult?'pending':error?'failed':'completed';
|
|
463
513
|
const state=rawStatus?' status='+rawStatus:'';
|
|
@@ -475,6 +525,11 @@ export function createLiveTimelineTracker({clock=()=>Date.now(),verbose=false,fo
|
|
|
475
525
|
const endClock=displayClock(value.last_ms);
|
|
476
526
|
const range=value.first_ms===value.last_ms?endClock:startClock.slice(0,8)+'–'+endClock.slice(0,8);
|
|
477
527
|
if(value.count>1) foldedHeartbeatLines+=value.count-1;
|
|
528
|
+
if(prettyStyle){
|
|
529
|
+
return prettyLine({request_id:'polling',created_at_utc:new Date(value.last_ms).toISOString()},
|
|
530
|
+
value.tool,'FOLDED','×'+value.count+' avg '+formatDuration(avg),
|
|
531
|
+
displayMode==='ascii'?'H':'📡');
|
|
532
|
+
}
|
|
478
533
|
const marker=displayMode==='ascii'?'OK':'✓';
|
|
479
534
|
return remoteDisplayText(iconForTool(value.tool,displayMode)+' '+range+' '+seq+value.tool+' ×'+value.count+' avg '+formatDuration(avg)+' '+marker,displayMode);
|
|
480
535
|
}
|
|
@@ -512,12 +567,13 @@ export function createLiveTimelineTracker({clock=()=>Date.now(),verbose=false,fo
|
|
|
512
567
|
const args=event.args??{};
|
|
513
568
|
const displayTool=displayToolName(tool,args);
|
|
514
569
|
const summary=summarizeLiveToolArgs(displayTool,args,{rootPaths});
|
|
570
|
+
const displayPaths=prettyStyle?detailedDisplayPaths(args,rootPaths):[];
|
|
515
571
|
const traceMeta=traceIdentity(event,args);
|
|
516
572
|
if(pending.size>=MAX_PRESENTATION_IDENTITIES){
|
|
517
573
|
pendingOverflow++;
|
|
518
574
|
return pendingOverflow===1?remoteDisplayText('LIVELOG pending span cap reached; full audit remains on disk',displayMode):null;
|
|
519
575
|
}
|
|
520
|
-
pending.set(requestId,{request_id:requestId,tool,display_tool:displayTool,summary,trace_meta:traceMeta,started_at_ms:clock()});
|
|
576
|
+
pending.set(requestId,{request_id:requestId,tool,display_tool:displayTool,summary,display_paths:displayPaths,trace_meta:traceMeta,started_at_ms:clock()});
|
|
521
577
|
peakInFlight=Math.max(peakInFlight,pending.size);
|
|
522
578
|
if(commanderStyle){
|
|
523
579
|
const safeArgs=sanitizeAuditArgs(args);
|
|
@@ -527,6 +583,9 @@ export function createLiveTimelineTracker({clock=()=>Date.now(),verbose=false,fo
|
|
|
527
583
|
}
|
|
528
584
|
if(verbose) return formatRemoteToolCall({...event,tool:displayTool},{clock,displayMode})+traceSuffix(traceMeta);
|
|
529
585
|
if(visibleStart(displayTool)){
|
|
586
|
+
if(prettyStyle) return prettyLine(event,displayTool,'START',
|
|
587
|
+
[trace?traceText(traceMeta,displayMode):'',displayPaths.length?summary.replace(displayPaths[0],'').trim():summary].filter(Boolean).join(' | '),
|
|
588
|
+
null,displayPaths);
|
|
530
589
|
const startMarker=displayMode==='ascii'?'START':'▶ START';
|
|
531
590
|
return remoteDisplayText(eventPrefix(event,sequence,{displayTool})+traceSuffix(traceMeta)+' '+startMarker+(summary?' '+summary:''),displayMode);
|
|
532
591
|
}
|
|
@@ -606,6 +665,23 @@ export function createLiveTimelineTracker({clock=()=>Date.now(),verbose=false,fo
|
|
|
606
665
|
const p50=sorted.length?(sorted.length%2?sorted[(sorted.length-1)/2]:(sorted[sorted.length/2-1]+sorted[sorted.length/2])/2):null;
|
|
607
666
|
const p95=sorted.length?sorted[Math.max(0,Math.ceil(sorted.length*0.95)-1)]:null;
|
|
608
667
|
const p99=sorted.length?sorted[Math.max(0,Math.ceil(sorted.length*0.99)-1)]:null;
|
|
668
|
+
if(prettyStyle){
|
|
669
|
+
const ascii=displayMode==='ascii';
|
|
670
|
+
const lines=[
|
|
671
|
+
(ascii?'[SUMMARY]':'📊 SESSION')+' '+resultCount+' calls | '+okCount+' ok | '+
|
|
672
|
+
errorCount+(errorCount===1?' error | ':' errors | ')+unknownCount+' unknown'+(pendingCount?' | '+pendingCount+' pending':''),
|
|
673
|
+
(ascii?' work:':' 🛠 work:')+' '+mutationCount+' mutations | peak '+peakInFlight+' in-flight'+
|
|
674
|
+
(foldedHeartbeatLines?' | folded '+foldedHeartbeatLines+' heartbeats':''),
|
|
675
|
+
(ascii?' timing:':' ⏱ timing:')+' p50 '+(p50===null?'n/a':formatDuration(p50))+
|
|
676
|
+
' | p95 '+(p95===null?'n/a':formatDuration(p95))+
|
|
677
|
+
' | p99 '+(p99===null?'n/a':formatDuration(p99))+
|
|
678
|
+
(slowest?' | slowest '+slowest.tool+' '+formatDuration(slowest.duration_ms):''),
|
|
679
|
+
(ascii?' top:':' 🔝 top:')+(top?' '+top:' none')
|
|
680
|
+
];
|
|
681
|
+
if(pendingOverflow)lines.push((ascii?' WARNING:':' ⚠️ WARNING:')+
|
|
682
|
+
' '+pendingOverflow+' untracked presentation spans; inspect signed audit');
|
|
683
|
+
return remoteDisplayText(lines.join('\n'),displayMode);
|
|
684
|
+
}
|
|
609
685
|
return remoteDisplayText((displayMode==='ascii'?'[SUMMARY]':'📊 [SUMMARY]')+' '+resultCount+' calls | '+okCount+' ok | '+
|
|
610
686
|
errorCount+' error'+(errorCount===1?'':'s')+' | '+unknownCount+' unknown'+(pendingCount?' | '+pendingCount+' pending':'')+' | '+mutationCount+' mutation'+(mutationCount===1?'':'s')+
|
|
611
687
|
' | tool time '+(formatDuration(totalDurationMs)||'0ms')+
|
|
@@ -8,6 +8,7 @@ import { windowsPowerShellEnvironment } from './runtime-update.mjs';
|
|
|
8
8
|
import { resolveStateRoot } from './state-root.mjs';
|
|
9
9
|
import { AUTHORITY_PLANES, parseAuthorityObservation, assertAuthorityVector } from './remote-authority.mjs';
|
|
10
10
|
import { readRemoteMarkerSnapshot, deletePinnedRemoteMarker } from './foreground-marker-cas-win32.mjs';
|
|
11
|
+
import {assertObserveOnlyMarker} from './r175-foreground-boundary.mjs';
|
|
11
12
|
|
|
12
13
|
const CLEANUP=['disarmautonomous','disarmcoding','disarmprocess','disarmdesktop','disarmhost'];
|
|
13
14
|
const TRUSTED_WINDOWS_ROOT='C:\\Windows';
|
|
@@ -68,6 +69,9 @@ async function main(){
|
|
|
68
69
|
const ownerPid=Number(args['owner-pid']);
|
|
69
70
|
const nonce=String(args['session-nonce']||'');
|
|
70
71
|
const ipcOwner=args['owner-channel']==='ipc';
|
|
72
|
+
const observerGuard=args['authority-scope']==='observe-only';
|
|
73
|
+
if(args['authority-scope']!==undefined&&!observerGuard)
|
|
74
|
+
throw new Error('R175 guardian authority scope invalid');
|
|
71
75
|
if(ipcOwner&&typeof process.send!=='function')throw new Error('guardian owner IPC channel missing');
|
|
72
76
|
if(!Number.isInteger(ownerPid)||ownerPid<1) throw new Error('guardian owner pid invalid');
|
|
73
77
|
if(ipcOwner&&process.ppid!==ownerPid)throw new Error('guardian IPC parent does not match declared owner');
|
|
@@ -85,6 +89,25 @@ async function main(){
|
|
|
85
89
|
continue;
|
|
86
90
|
}
|
|
87
91
|
|
|
92
|
+
// R179: a missing command-line flag is never permission to downgrade an
|
|
93
|
+
// observation-only marker to legacy broad authority revocation.
|
|
94
|
+
if(assertObserveOnlyMarker(marker)&&!observerGuard)
|
|
95
|
+
throw new Error('R179_OBSERVER_GUARDIAN_SCOPE_DOWNGRADE: owner grants retained');
|
|
96
|
+
if(observerGuard){
|
|
97
|
+
// Guardian scope is inherited from the parent process command line.
|
|
98
|
+
// Marker mismatch can never degrade into legacy unconditional revoke.
|
|
99
|
+
if(!assertObserveOnlyMarker(marker))
|
|
100
|
+
throw new Error('R175 observer guardian marker invalid; no grants touched');
|
|
101
|
+
// Observation-only sessions own the foreground marker, never the
|
|
102
|
+
// process/coding/host/desktop/autonomous grants or runtime.
|
|
103
|
+
const current=await readRemoteMarkerSnapshot(markerPath);
|
|
104
|
+
if(!current)return;
|
|
105
|
+
if(!markerMatches(current.marker,{ownerPid,nonce})||!assertObserveOnlyMarker(current.marker))return;
|
|
106
|
+
if(deletePinnedRemoteMarker(markerPath,current.sha256)!=='REMOVED')
|
|
107
|
+
throw new Error('R175 guardian monitor marker CAS failed; marker retained');
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
88
111
|
const failed=[];
|
|
89
112
|
for(const command of CLEANUP){
|
|
90
113
|
const latest=await readMarker(markerPath);
|
package/src/version.mjs
CHANGED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import {createHash} from 'node:crypto';
|
|
2
|
+
import {mkdir,readFile,writeFile} from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
const sha256=value=>createHash('sha256').update(value).digest('hex');
|
|
6
|
+
|
|
7
|
+
export async function installExactActivePointer({stateRoot,packageRoot,launcherPath}){
|
|
8
|
+
const manifestBytes=await readFile(path.join(packageRoot,'MANIFEST.SHA256'));
|
|
9
|
+
const manifestSha256=sha256(manifestBytes);
|
|
10
|
+
const pkg=JSON.parse(await readFile(path.join(packageRoot,'package.json'),'utf8'));
|
|
11
|
+
const generationRoot=path.join(stateRoot,'generations','test-current');
|
|
12
|
+
const configPath=path.join(generationRoot,'controller-config.psd1');
|
|
13
|
+
await mkdir(generationRoot,{recursive:true});
|
|
14
|
+
await writeFile(configPath,"@{ Label = 'test-current' }\r\n",'utf8');
|
|
15
|
+
const pointer={
|
|
16
|
+
schema:'deadbyte.active-release.v1',
|
|
17
|
+
release_id:`v${pkg.version}-${manifestSha256.slice(0,16)}`,
|
|
18
|
+
version:pkg.version,
|
|
19
|
+
manifest_sha256:manifestSha256,
|
|
20
|
+
release_root:path.resolve(packageRoot),
|
|
21
|
+
generation_root:generationRoot,
|
|
22
|
+
generation_manifest_sha256:sha256(await readFile(configPath)),
|
|
23
|
+
launcher_sha256:sha256(await readFile(launcherPath)),
|
|
24
|
+
config_path:configPath
|
|
25
|
+
};
|
|
26
|
+
await writeFile(path.join(stateRoot,'active-release.json'),`${JSON.stringify(pointer)}\n`,'utf8');
|
|
27
|
+
return pointer;
|
|
28
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import {readFile} from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {fileURLToPath} from 'node:url';
|
|
4
|
+
|
|
5
|
+
export async function readReleaseLock(root){
|
|
6
|
+
const rootPath=root instanceof URL?fileURLToPath(root):root;
|
|
7
|
+
for(const relative of ['package-lock.json','docs/R184-package-lock.json']){
|
|
8
|
+
try{return JSON.parse(await readFile(path.join(rootPath,relative),'utf8'));}
|
|
9
|
+
catch(error){if(error?.code!=='ENOENT')throw error;}
|
|
10
|
+
}
|
|
11
|
+
throw new Error('R184_RELEASE_LOCK_EVIDENCE_MISSING');
|
|
12
|
+
}
|
|
@@ -3,6 +3,7 @@ import assert from 'node:assert/strict';
|
|
|
3
3
|
import {readFile} from 'node:fs/promises';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import {fileURLToPath} from 'node:url';
|
|
6
|
+
import {readReleaseLock} from './helpers/release-lock.mjs';
|
|
6
7
|
|
|
7
8
|
const root=path.resolve(fileURLToPath(new URL('..',import.meta.url)));
|
|
8
9
|
async function json(file){return JSON.parse(await readFile(path.join(root,file),'utf8'));}
|
|
@@ -29,7 +30,7 @@ test('R129 shipped client and server SDK exclude all recorded 2.1.0 security adv
|
|
|
29
30
|
});
|
|
30
31
|
|
|
31
32
|
test('R129 lockfile resolves security-fixed SDK packages without prerelease versions',async()=>{
|
|
32
|
-
const lock=await
|
|
33
|
+
const lock=await readReleaseLock(root);
|
|
33
34
|
const pins=['@modelcontextprotocol/client','@modelcontextprotocol/server'];
|
|
34
35
|
for(const dep of pins){
|
|
35
36
|
const version=lock.packages?.['node_modules/'+dep]?.version;
|
|
@@ -4,23 +4,24 @@ import {readFile} from 'node:fs/promises';
|
|
|
4
4
|
import {fileURLToPath} from 'node:url';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import {DEADBYTE_VERSION} from '../src/version.mjs';
|
|
7
|
+
import {readReleaseLock} from './helpers/release-lock.mjs';
|
|
7
8
|
|
|
8
9
|
const root=path.resolve(fileURLToPath(new URL('..',import.meta.url)));
|
|
9
10
|
const read=async file=>JSON.parse(await readFile(path.join(root,file),'utf8'));
|
|
10
|
-
test('current successor retains the R130 security baseline with truthful
|
|
11
|
+
test('current successor retains the R130 security baseline with truthful R190 identity',async()=>{
|
|
11
12
|
const [pkg,release,provenance,bom,lock,matrix]=await Promise.all([
|
|
12
13
|
read('package.json'),read('RELEASE.json'),read('PROVENANCE.json'),
|
|
13
|
-
read('SBOM.cdx.json'),
|
|
14
|
+
read('SBOM.cdx.json'),readReleaseLock(root),read('TEST-MATRIX.json')]);
|
|
14
15
|
assert.equal(pkg.name,'deadbyte-mcp');
|
|
15
16
|
for(const actual of [pkg.version,release.version,provenance.version,matrix.version,bom.metadata.component.version,lock.version,lock.packages[''].version,DEADBYTE_VERSION])
|
|
16
|
-
assert.equal(actual,'0.15.
|
|
17
|
-
assert.equal(release.train,'
|
|
18
|
-
assert.equal(matrix.train,'
|
|
19
|
-
assert.equal(release.source_predecessor.version,'0.15.
|
|
20
|
-
assert.equal(release.source_predecessor.train,'
|
|
21
|
-
assert.match(release.source_predecessor.source_path,/0\.15\.
|
|
22
|
-
assert.equal(release.source_predecessor.manifest_status,'
|
|
23
|
-
assert.equal(release.runtime_predecessor.version,'0.15.
|
|
17
|
+
assert.equal(actual,'0.15.70');
|
|
18
|
+
assert.equal(release.train,'R190');
|
|
19
|
+
assert.equal(matrix.train,'R190');
|
|
20
|
+
assert.equal(release.source_predecessor.version,'0.15.69');
|
|
21
|
+
assert.equal(release.source_predecessor.train,'R179');
|
|
22
|
+
assert.match(release.source_predecessor.source_path,/deadbyte-mcp-0\.15\.69\.tgz/i);
|
|
23
|
+
assert.equal(release.source_predecessor.manifest_status,'PUBLISHED_PREVIEW_MANIFEST_VERIFIED_NOT_ACTIVE');
|
|
24
|
+
assert.equal(release.runtime_predecessor.version,'0.15.68');
|
|
24
25
|
assert.equal(provenance.r160.source_predecessor_manifest_status,'STALE_REJECTED');
|
|
25
26
|
assert.equal(pkg.dependencies['@modelcontextprotocol/client'],'2.3.1');
|
|
26
27
|
assert.equal(pkg.dependencies['@modelcontextprotocol/server'],'2.3.1');
|