deadbyte-mcp 0.15.72 → 0.15.75
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 +143 -35
- package/PROVENANCE.json +47 -21
- package/README.txt +29 -18
- package/RELEASE.json +85 -18
- package/SBOM.cdx.json +14 -2
- package/TEST-MATRIX.json +50 -2
- package/bin/deadbyte-desktop-uia-r199-fixed.exe +0 -0
- package/bin/deadbyte-desktop-uia-r199-fixed.obj +0 -0
- package/bin/deadbyte-desktop-uia-r199.exe +0 -0
- package/bin/deadbyte-desktop-uia-r199.obj +0 -0
- package/bin/deadbyte-fs-read-r197-fixed.exe +0 -0
- package/bin/deadbyte-fs-read-r198.exe +0 -0
- package/bin/deadbyte-fs-read-r198.obj +0 -0
- package/bin/deadbyte-owner-token-r193.exe +0 -0
- package/bin/deadbyte-root-relative-probe-r197.exe +0 -0
- package/bridge/package.json +1 -1
- package/controller/deadbyte-process-policy.json +4 -4
- package/docs/R184-package-lock.json +2 -8
- package/native/deadbyte-desktop-uia-r199.cpp +345 -0
- package/native/deadbyte-fs-read-r197.cpp +174 -0
- package/native/deadbyte-fs-read-r198.cpp +181 -0
- package/native/deadbyte-owner-token-r193.cpp +156 -0
- package/native/deadbyte-root-relative-probe-r197.cpp +97 -0
- package/package.json +4 -2
- package/scripts/build-native-desktop-uia-r199-fixed.ps1 +28 -0
- package/scripts/build-native-desktop-uia-r199.ps1 +28 -0
- package/scripts/build-native-fs-read-r197-fixed.ps1 +28 -0
- package/scripts/build-native-fs-read-r197.ps1 +28 -0
- package/scripts/build-native-fs-read-r198.ps1 +28 -0
- package/scripts/build-native-root-probe-r197.ps1 +28 -0
- package/scripts/gate-windows-r193.ps1 +108 -0
- package/scripts/gate-windows-r194.ps1 +112 -0
- package/scripts/gate-windows-r195.ps1 +114 -0
- package/scripts/gate-windows-r206.ps1 +116 -0
- package/scripts/init-desktop-policy-r199.ps1 +85 -0
- package/scripts/npm-interactive-r194.mjs +13 -0
- package/scripts/npm-publication-r65.mjs +64 -4
- package/scripts/npm-release-r48.mjs +75 -11
- package/scripts/npm-release-readiness-r193.mjs +80 -0
- package/scripts/npm-release-readiness-r194.mjs +80 -0
- package/scripts/npm-release-readiness-r195.mjs +72 -0
- package/scripts/npm-release-readiness-r206.mjs +76 -0
- package/scripts/r188-owner-uac-review.ps1 +91 -0
- package/scripts/r188-uac-review-preview.mjs +65 -0
- package/scripts/r193-compile-token.ps1 +22 -0
- package/scripts/r193-evidence-relocate.ps1 +12 -0
- package/scripts/r193-focused-gate.mjs +20 -0
- package/scripts/r193-import-r188-scripts.ps1 +14 -0
- package/scripts/r193-limited-subject-probe.ps1 +25 -0
- package/scripts/r193-migrate-runtime-limited.ps1 +103 -0
- package/scripts/r193-owner-keygen.mjs +43 -0
- package/scripts/r193-owner-request-live.mjs +49 -0
- package/scripts/r193-owner-uac-approve.ps1 +42 -0
- package/scripts/r193-owner-uac-broker.mjs +139 -0
- package/scripts/r193-owner-uac-enroll-and-approve.ps1 +62 -0
- package/scripts/r193-test-limited-token.ps1 +49 -0
- package/scripts/r193-toolchain-inventory.ps1 +7 -0
- package/scripts/r193-uac-bridge-focused.mjs +21 -0
- package/scripts/r205-candidate-selection.mjs +22 -0
- package/src/candidate-selection-evidence-r206.mjs +71 -0
- package/src/candidate-selection-r205.mjs +96 -0
- package/src/desktop-postcondition-r196.mjs +30 -0
- package/src/desktop-runtime-r40.mjs +44 -7
- package/src/desktop-uia-identity-r199.mjs +51 -0
- package/src/desktop-uia-r40.mjs +9 -1
- package/src/machine-fs-handle-read-r196.mjs +43 -0
- package/src/machine-fs-native-read-r197.mjs +73 -0
- package/src/machine-fs-native-read-r198.mjs +76 -0
- package/src/machine-fs-read-worker.mjs +5 -25
- package/src/machine-fs.mjs +31 -16
- package/src/r188-owner-uac.mjs +105 -0
- package/src/r188-uac-boundary.mjs +21 -0
- package/src/r193-live-owner-subject.mjs +110 -0
- package/src/r193-owner-secret-acl.mjs +67 -0
- package/src/r193-owner-uac-broker.mjs +85 -0
- package/src/r193-uac-r70-bridge.mjs +54 -0
- package/src/r195-canonical-owner-observation.mjs +15 -0
- package/src/remote-destination-journal-r202.mjs +120 -0
- package/src/remote-device-r44.mjs +89 -14
- package/src/remote-payload-boundary-r201.mjs +71 -0
- package/src/remote-terminal-evidence-r200.mjs +60 -0
- package/src/version.mjs +1 -1
- package/test/desktop-runtime-r40.test.mjs +111 -0
- package/test/desktop-uia-r40.test.mjs +15 -0
- package/test/fixtures/r197-junction-swap-worker.mjs +21 -0
- package/test/fixtures/r203-journal-writer.mjs +6 -0
- package/test/process-policy-refresh.test.mjs +4 -4
- package/test/r130-release-security-contract.test.mjs +8 -8
- package/test/r132-release-identity.test.mjs +7 -7
- package/test/r153-prepublish-identity.test.mjs +1 -1
- package/test/r160-release-contract.test.mjs +3 -3
- package/test/r165-live-cli-skew.test.mjs +1 -1
- package/test/r167-release-contract.test.mjs +11 -11
- package/test/r168-release-contract.test.mjs +4 -4
- package/test/r172-release-contract.test.mjs +6 -6
- package/test/r173-release-contract.test.mjs +16 -16
- package/test/r179-release-contract.test.mjs +12 -12
- package/test/r188-owner-uac.test.mjs +48 -0
- package/test/r188-uac-boundary.test.mjs +42 -0
- package/test/r190-release-contract.test.mjs +10 -10
- package/test/r191-release-contract.test.mjs +12 -12
- package/test/r192-release-identity.test.mjs +10 -10
- package/test/r193-limited-migration-contract.test.mjs +26 -0
- package/test/r193-live-owner-subject.test.mjs +61 -0
- package/test/r193-native-process-observer.test.mjs +36 -0
- package/test/r193-native-token.test.mjs +43 -0
- package/test/r193-owner-enrollment-entrypoint.test.mjs +38 -0
- package/test/r193-owner-secret-acl.test.mjs +23 -0
- package/test/r193-owner-uac-broker.test.mjs +82 -0
- package/test/r193-owner-uac-entrypoint.test.mjs +36 -0
- package/test/r193-release-identity.test.mjs +58 -0
- package/test/r193-release-readiness.test.mjs +87 -0
- package/test/r193-uac-r70-bridge.test.mjs +88 -0
- package/test/r193-windows-gate.test.mjs +30 -0
- package/test/r194-npm-auth-recovery.test.mjs +77 -0
- package/test/r194-npm-preauth.test.mjs +57 -0
- package/test/r194-release-identity.test.mjs +47 -0
- package/test/r194-release-readiness.test.mjs +87 -0
- package/test/r194-windows-gate.test.mjs +22 -0
- package/test/r195-canonical-owner-observation.test.mjs +32 -0
- package/test/r195-release-readiness.test.mjs +22 -0
- package/test/r196-desktop-postcondition.test.mjs +53 -0
- package/test/r196-handle-bound-read.test.mjs +99 -0
- package/test/r196-remote-outcome.test.mjs +94 -0
- package/test/r197-namespace-race.test.mjs +41 -0
- package/test/r197-native-dispatch-race.test.mjs +49 -0
- package/test/r197-native-file-read.test.mjs +60 -0
- package/test/r197-native-root-open.test.mjs +61 -0
- package/test/r198-root-identity.test.mjs +88 -0
- package/test/r199-native-uia-worker.test.mjs +28 -0
- package/test/r199-uia-identity.test.mjs +43 -0
- package/test/r200-remote-reconcile.test.mjs +106 -0
- package/test/r201-payload-boundary.test.mjs +78 -0
- package/test/r201-payload-fuzz.test.mjs +37 -0
- package/test/r201-tool-boundary-fuzz.test.mjs +120 -0
- package/test/r202-destination-journal.test.mjs +92 -0
- package/test/r203-atomic-journal.test.mjs +86 -0
- package/test/r205-candidate-selection-cli.test.mjs +31 -0
- package/test/r205-candidate-selection.test.mjs +83 -0
- package/test/r206-candidate-evidence.test.mjs +64 -0
- package/test/r206-release-contract.test.mjs +42 -0
- package/test/r206-release-readiness.test.mjs +23 -0
- package/test/r59-tool-surface-compat.test.mjs +1 -1
- package/test/r64-machine-fs-large-read.test.mjs +26 -12
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { canonicalJson } from './canonical-json.mjs';
|
|
3
|
+
import { evaluateCandidateSelectionR205 } from './candidate-selection-r205.mjs';
|
|
4
|
+
|
|
5
|
+
const SHA=/^[0-9a-f]{64}$/;
|
|
6
|
+
const sha=value=>createHash('sha256').update(Buffer.from(canonicalJson(value),'utf8')).digest('hex');
|
|
7
|
+
const insist=(condition,message)=>{if(!condition)throw new Error(message);};
|
|
8
|
+
|
|
9
|
+
function receiptExpectations(input){
|
|
10
|
+
const expectations=[];
|
|
11
|
+
const addCorrectness=(label,artifact,value)=>expectations.push({
|
|
12
|
+
label,kind:'correctness',receipt_sha256:value.receipt_sha256,artifact_sha256:artifact,
|
|
13
|
+
fixture_sha256:input.fixture_sha256,status:value.status
|
|
14
|
+
});
|
|
15
|
+
const addBenchmark=(label,artifact,value)=>expectations.push({
|
|
16
|
+
label,kind:'benchmark',receipt_sha256:value.receipt_sha256,artifact_sha256:artifact,
|
|
17
|
+
fixture_sha256:input.fixture_sha256,environment_sha256:input.environment_sha256,
|
|
18
|
+
samples_sha256:sha(value.samples_ms)
|
|
19
|
+
});
|
|
20
|
+
addCorrectness('baseline correctness',input.baseline.artifact_sha256,input.baseline.correctness);
|
|
21
|
+
addBenchmark('baseline benchmark',input.baseline.artifact_sha256,input.baseline.benchmark);
|
|
22
|
+
for(const candidate of input.candidates){
|
|
23
|
+
addCorrectness(`candidate ${candidate.candidate_id} correctness`,candidate.artifact_sha256,candidate.correctness);
|
|
24
|
+
if(candidate.benchmark)addBenchmark(`candidate ${candidate.candidate_id} benchmark`,candidate.artifact_sha256,candidate.benchmark);
|
|
25
|
+
}
|
|
26
|
+
return expectations;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function verifyFact(expected,actual){
|
|
30
|
+
insist(actual&&typeof actual==='object'&&!Array.isArray(actual),`${expected.label} receipt verification failed`);
|
|
31
|
+
insist(actual.verified===true&&actual.receipt_sha256===expected.receipt_sha256,
|
|
32
|
+
`${expected.label} receipt verification failed`);
|
|
33
|
+
insist(SHA.test(actual.receipt_sha256),`${expected.label} receipt verification failed`);
|
|
34
|
+
if(expected.kind==='correctness'){
|
|
35
|
+
insist(actual.evidence_kind==='correctness'&&actual.signer_role==='independent_correctness_oracle'&&
|
|
36
|
+
actual.artifact_sha256===expected.artifact_sha256&&actual.fixture_sha256===expected.fixture_sha256&&
|
|
37
|
+
actual.status===expected.status,`${expected.label} receipt binding mismatch`);
|
|
38
|
+
}else{
|
|
39
|
+
insist(actual.evidence_kind==='benchmark'&&actual.signer_role==='independent_benchmark_runner'&&
|
|
40
|
+
actual.artifact_sha256===expected.artifact_sha256&&actual.fixture_sha256===expected.fixture_sha256&&
|
|
41
|
+
actual.environment_sha256===expected.environment_sha256&&actual.samples_sha256===expected.samples_sha256,
|
|
42
|
+
`${expected.label} receipt binding mismatch`);
|
|
43
|
+
}
|
|
44
|
+
return Object.freeze({receipt_sha256:actual.receipt_sha256,evidence_kind:actual.evidence_kind,
|
|
45
|
+
signer_role:actual.signer_role,artifact_sha256:actual.artifact_sha256,fixture_sha256:actual.fixture_sha256,
|
|
46
|
+
...(expected.kind==='correctness'?{status:actual.status}:{environment_sha256:actual.environment_sha256,
|
|
47
|
+
samples_sha256:actual.samples_sha256})});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function verifyCandidateSelectionEvidenceR206(input,{verifyReceipt}={}){
|
|
51
|
+
insist(typeof verifyReceipt==='function','independent receipt verifier required');
|
|
52
|
+
const provisional=evaluateCandidateSelectionR205(input);
|
|
53
|
+
const expectations=receiptExpectations(input);
|
|
54
|
+
const seen=new Set();
|
|
55
|
+
const verified=[];
|
|
56
|
+
for(const expected of expectations){
|
|
57
|
+
insist(SHA.test(expected.receipt_sha256),`${expected.label} receipt hash invalid`);
|
|
58
|
+
insist(!seen.has(expected.receipt_sha256),`${expected.label} receipt reused`);
|
|
59
|
+
seen.add(expected.receipt_sha256);
|
|
60
|
+
let actual;
|
|
61
|
+
try{actual=await verifyReceipt(expected.receipt_sha256,Object.freeze({...expected}));}
|
|
62
|
+
catch(error){throw new Error(`${expected.label} receipt verification failed: ${error?.message??error}`);}
|
|
63
|
+
verified.push(verifyFact(expected,actual));
|
|
64
|
+
}
|
|
65
|
+
const binding={schema:'deadbyte.candidate-selection-evidence-binding.r206.v1',
|
|
66
|
+
selection_report_sha256:provisional.report_sha256,verified_receipts:verified};
|
|
67
|
+
return Object.freeze({...provisional,schema:'deadbyte.candidate-selection-evidence-verdict.r206.v1',
|
|
68
|
+
evidence_verified:true,verified_receipt_count:verified.length,release_authorized:false,
|
|
69
|
+
reason:'Independent decision-bearing receipts verified. Candidate selection remains advisory and cannot authorize release.',
|
|
70
|
+
evidence_binding_sha256:sha(binding)});
|
|
71
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { canonicalJson } from './canonical-json.mjs';
|
|
3
|
+
|
|
4
|
+
export const CANDIDATE_SELECTION_SCHEMA='deadbyte.candidate-selection.r205.v1';
|
|
5
|
+
const H=/^[0-9a-f]{64}$/;
|
|
6
|
+
const ID=/^[A-Za-z0-9._-]{1,96}$/;
|
|
7
|
+
const STATES=new Set(['PASS','FAIL','UNKNOWN','INVALID','UNTESTED']);
|
|
8
|
+
const digest=value=>createHash('sha256').update(Buffer.from(canonicalJson(value),'utf8')).digest('hex');
|
|
9
|
+
|
|
10
|
+
function object(value,label,allowed){
|
|
11
|
+
if(!value||typeof value!=='object'||Array.isArray(value))throw Error(label+' invalid');
|
|
12
|
+
const extra=Object.keys(value).filter(k=>!allowed.includes(k));
|
|
13
|
+
if(extra.length)throw Error(label+' unexpected field '+extra[0]);
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
function hash(value,label){if(typeof value!=='string'||!H.test(value))throw Error(label+' invalid');return value;}
|
|
17
|
+
function sample(value,label){
|
|
18
|
+
if(!Array.isArray(value)||value.length<7||value.length>31||value.some(x=>typeof x!=='number'||!Number.isFinite(x)||x<=0||x>3_600_000))throw Error(label+' invalid');
|
|
19
|
+
return [...value];
|
|
20
|
+
}
|
|
21
|
+
const median=xs=>{const a=[...xs].sort((x,y)=>x-y);return a[Math.floor(a.length/2)];};
|
|
22
|
+
const mad=xs=>median(xs.map(x=>Math.abs(x-median(xs))));
|
|
23
|
+
function correctness(raw,label,artifact,fixture){
|
|
24
|
+
const v=object(raw,label,['status','artifact_sha256','fixture_sha256','receipt_sha256']);
|
|
25
|
+
if(!STATES.has(v.status))throw Error(label+' status invalid');
|
|
26
|
+
if(hash(v.artifact_sha256,label+' artifact')!==artifact||hash(v.fixture_sha256,label+' fixture')!==fixture)throw Error(label+' evidence identity mismatch');
|
|
27
|
+
hash(v.receipt_sha256,label+' receipt');
|
|
28
|
+
return Object.freeze({...v});
|
|
29
|
+
}
|
|
30
|
+
function benchmark(raw,label,artifact,fixture,environment){
|
|
31
|
+
const v=object(raw,label,['artifact_sha256','fixture_sha256','environment_sha256','receipt_sha256','samples_ms']);
|
|
32
|
+
if(hash(v.artifact_sha256,label+' artifact')!==artifact||hash(v.fixture_sha256,label+' fixture')!==fixture||hash(v.environment_sha256,label+' environment')!==environment)throw Error(label+' benchmark identity mismatch');
|
|
33
|
+
hash(v.receipt_sha256,label+' receipt');
|
|
34
|
+
return Object.freeze({...v,samples_ms:sample(v.samples_ms,label+' samples')});
|
|
35
|
+
}
|
|
36
|
+
function normalize(raw){
|
|
37
|
+
const v=object(raw,'candidate selection',['schema','fixture_sha256','environment_sha256','baseline','candidates']);
|
|
38
|
+
if(v.schema!==CANDIDATE_SELECTION_SCHEMA)throw Error('candidate selection schema invalid');
|
|
39
|
+
const fixture=hash(v.fixture_sha256,'fixture'),env=hash(v.environment_sha256,'environment');
|
|
40
|
+
const b=object(v.baseline,'baseline',['artifact_sha256','correctness','benchmark']);
|
|
41
|
+
const baseHash=hash(b.artifact_sha256,'baseline artifact');
|
|
42
|
+
const baseCorrect=correctness(b.correctness,'baseline correctness',baseHash,fixture);
|
|
43
|
+
if(baseCorrect.status!=='PASS')throw Error('baseline correctness not PASS');
|
|
44
|
+
const baseBench=benchmark(b.benchmark,'baseline benchmark',baseHash,fixture,env);
|
|
45
|
+
if(!Array.isArray(v.candidates)||v.candidates.length<1||v.candidates.length>8)throw Error('candidate count invalid');
|
|
46
|
+
const seen=new Set([baseHash]),ids=new Set();
|
|
47
|
+
const candidates=v.candidates.map((item,i)=>{
|
|
48
|
+
const c=object(item,'candidate '+i,['candidate_id','artifact_sha256','correctness','benchmark']);
|
|
49
|
+
if(typeof c.candidate_id!=='string'||!ID.test(c.candidate_id)||ids.has(c.candidate_id))throw Error('candidate id invalid or repeated');
|
|
50
|
+
ids.add(c.candidate_id);
|
|
51
|
+
const h=hash(c.artifact_sha256,'candidate artifact');
|
|
52
|
+
if(seen.has(h))throw Error('candidate artifact duplicated');seen.add(h);
|
|
53
|
+
const result=correctness(c.correctness,'candidate correctness',h,fixture);
|
|
54
|
+
const bench=c.benchmark===undefined?null:benchmark(c.benchmark,'candidate benchmark',h,fixture,env);
|
|
55
|
+
if(result.status==='PASS'&&!bench)throw Error('PASS candidate benchmark missing');
|
|
56
|
+
if(bench&&bench.samples_ms.length!==baseBench.samples_ms.length)throw Error('paired sample length mismatch');
|
|
57
|
+
return {candidate_id:c.candidate_id,artifact_sha256:h,correctness:result,benchmark:bench};
|
|
58
|
+
});
|
|
59
|
+
return {fixture,environment:env,baseline:{artifact_sha256:baseHash,correctness:baseCorrect,benchmark:baseBench},candidates};
|
|
60
|
+
}
|
|
61
|
+
export function buildBlindCandidateViewR205(input){
|
|
62
|
+
const v=normalize(input);
|
|
63
|
+
return Object.freeze({schema:CANDIDATE_SELECTION_SCHEMA,fixture_sha256:v.fixture,environment_sha256:v.environment,
|
|
64
|
+
baseline:{artifact_sha256:v.baseline.artifact_sha256,correctness:v.baseline.correctness,benchmark:v.baseline.benchmark},
|
|
65
|
+
candidates:v.candidates.map(c=>({artifact_sha256:c.artifact_sha256,correctness:c.correctness,benchmark:c.benchmark}))
|
|
66
|
+
.sort((a,b)=>a.artifact_sha256.localeCompare(b.artifact_sha256))});
|
|
67
|
+
}
|
|
68
|
+
export function evaluateCandidateSelectionR205(input,{minimum_gain_pct=5,minimum_win_rate=0.8,max_mad_pct=8,ambiguity_margin_pct=3}={}){
|
|
69
|
+
for(const [name,v,max] of [['minimum_gain_pct',minimum_gain_pct,100],['minimum_win_rate',minimum_win_rate,1],['max_mad_pct',max_mad_pct,100],['ambiguity_margin_pct',ambiguity_margin_pct,100]]){
|
|
70
|
+
if(typeof v!=='number'||!Number.isFinite(v)||v<0||v>max)throw Error(name+' invalid');
|
|
71
|
+
}
|
|
72
|
+
const parsed=normalize(input);
|
|
73
|
+
const baseline=parsed.baseline.benchmark.samples_ms;
|
|
74
|
+
const classified=parsed.candidates.map(c=>{
|
|
75
|
+
if(c.correctness.status!=='PASS')return {candidate_id:c.candidate_id,artifact_sha256:c.artifact_sha256,verdict:c.correctness.status==='FAIL'?'REJECT_CORRECTNESS':'UNGRADED',correctness_status:c.correctness.status};
|
|
76
|
+
const changes=c.benchmark.samples_ms.map((time,i)=>100*(baseline[i]-time)/baseline[i]);
|
|
77
|
+
const gain=median(changes),deviation=mad(changes),wins=changes.filter(x=>x>0).length;
|
|
78
|
+
const winRate=wins/changes.length;
|
|
79
|
+
const eligible=gain>=minimum_gain_pct&&winRate>=minimum_win_rate&&deviation<=max_mad_pct;
|
|
80
|
+
return {candidate_id:c.candidate_id,artifact_sha256:c.artifact_sha256,verdict:eligible?'PERFORMANCE_QUALIFIED':'REJECT_PERFORMANCE',
|
|
81
|
+
correctness_status:'PASS',median_gain_pct:Number(gain.toFixed(4)),mad_pct:Number(deviation.toFixed(4)),
|
|
82
|
+
win_rate:Number(winRate.toFixed(4)),sample_count:changes.length};
|
|
83
|
+
});
|
|
84
|
+
const ranked=classified.filter(x=>x.verdict==='PERFORMANCE_QUALIFIED').sort((a,b)=>b.median_gain_pct-a.median_gain_pct||a.artifact_sha256.localeCompare(b.artifact_sha256));
|
|
85
|
+
const ambiguous=ranked.length>1&&ranked[0].median_gain_pct-ranked[1].median_gain_pct<ambiguity_margin_pct;
|
|
86
|
+
const winner=ranked.length&&!ambiguous?ranked[0]:null;
|
|
87
|
+
const report={schema:'deadbyte.candidate-selection-verdict.r205.v1',
|
|
88
|
+
state:ambiguous?'AMBIGUOUS':winner?'PROVISIONAL_CANDIDATE':'NO_QUALIFIED_CANDIDATE',
|
|
89
|
+
winner_candidate_id:winner?.candidate_id??null,winner_artifact_sha256:winner?.artifact_sha256??null,
|
|
90
|
+
evidence_verified:false,release_authorized:false,
|
|
91
|
+
reason:'Claims and receipt hashes are syntax/identity checked only. Signed receipts, independent oracle and benchmark runner must be verified externally before promotion.',
|
|
92
|
+
fixture_sha256:parsed.fixture,environment_sha256:parsed.environment,baseline_artifact_sha256:parsed.baseline.artifact_sha256,
|
|
93
|
+
candidates:classified.sort((a,b)=>a.artifact_sha256.localeCompare(b.artifact_sha256)),
|
|
94
|
+
blind_evaluation_sha256:digest(buildBlindCandidateViewR205(input))};
|
|
95
|
+
return Object.freeze({...report,report_sha256:digest(report)});
|
|
96
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const SHA256=/^[0-9a-f]{64}$/;
|
|
2
|
+
|
|
3
|
+
export function validateDesktopPostconditionExpectation(expectation){
|
|
4
|
+
if(expectation===null)return null;
|
|
5
|
+
if(!expectation||typeof expectation!=='object'||Array.isArray(expectation)||
|
|
6
|
+
Object.keys(expectation).sort().join(',')!=='expected_sha256,kind'||
|
|
7
|
+
expectation.kind!=='frame_image_sha256'||!SHA256.test(expectation.expected_sha256??''))
|
|
8
|
+
throw new Error('R196 unsupported desktop postcondition expectation');
|
|
9
|
+
return Object.freeze({kind:expectation.kind,expected_sha256:expectation.expected_sha256});
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// An input injection receipt is not evidence that the application accepted it.
|
|
13
|
+
// This verifier only establishes the explicit observable postcondition.
|
|
14
|
+
export function evaluateDesktopPostcondition({expectation=null,beforeFrame,afterFrame}={}){
|
|
15
|
+
if(!beforeFrame||!afterFrame||!SHA256.test(beforeFrame.image_sha256??'')||
|
|
16
|
+
!SHA256.test(afterFrame.image_sha256??'')||!SHA256.test(beforeFrame.frame_id??'')||
|
|
17
|
+
!SHA256.test(afterFrame.frame_id??''))throw new Error('R196 desktop frames invalid');
|
|
18
|
+
const base={schema:'deadbyte.desktop-postcondition.v1',
|
|
19
|
+
before_frame_id:beforeFrame.frame_id,after_frame_id:afterFrame.frame_id,
|
|
20
|
+
before_image_sha256:beforeFrame.image_sha256,after_image_sha256:afterFrame.image_sha256};
|
|
21
|
+
if(expectation===null)return Object.freeze({...base,status:'unknown',reason:'no_explicit_expectation',kind:null,expected_sha256:null});
|
|
22
|
+
const verified=validateDesktopPostconditionExpectation(expectation);
|
|
23
|
+
const expected=verified.expected_sha256;
|
|
24
|
+
if(beforeFrame.image_sha256===expected)
|
|
25
|
+
return Object.freeze({...base,status:'unknown',reason:'expected_state_already_present_before_action',
|
|
26
|
+
kind:expectation.kind,expected_sha256:expected});
|
|
27
|
+
return Object.freeze({...base,status:afterFrame.image_sha256===expected?'satisfied':'unsatisfied',
|
|
28
|
+
reason:afterFrame.image_sha256===expected?'exact_post_image_observed':'post_image_differs_from_expected',
|
|
29
|
+
kind:expectation.kind,expected_sha256:expected});
|
|
30
|
+
}
|
|
@@ -4,6 +4,8 @@ import path from 'node:path';
|
|
|
4
4
|
import { canonicalJson } from './canonical-json.mjs';
|
|
5
5
|
import { createDesktopAction,createDesktopTarget,guardDesktopAction,bindDesktopPostObservation,bindDesktopActionChain } from './desktop-contract.mjs';
|
|
6
6
|
import { effectIdentity,bindEffectCompletion } from './effect-ledger.mjs';
|
|
7
|
+
import { evaluateDesktopPostcondition,validateDesktopPostconditionExpectation } from './desktop-postcondition-r196.mjs';
|
|
8
|
+
import { bindUiaElementR199,assertFreshUiaElementR199 } from './desktop-uia-identity-r199.mjs';
|
|
7
9
|
import { DESKTOP_ACTION_KINDS,DESKTOP_POLICY_SCHEMA,assertDesktopArmed,desktopSessionStatus } from './desktop-policy-r40.mjs';
|
|
8
10
|
|
|
9
11
|
export const DESKTOP_RECEIPT_SCHEMA='deadbyte.desktop-receipt.v1';
|
|
@@ -82,6 +84,8 @@ export function createDesktopRuntime({
|
|
|
82
84
|
insist(Number.isInteger(maxTargets)&&maxTargets>=1&&maxTargets<=1024,'maxTargets invalid');
|
|
83
85
|
const receiptPublicKey=publicKey??createPublicKey(privateKey);
|
|
84
86
|
const targets=new Map();
|
|
87
|
+
const groundedTargets=new Set();
|
|
88
|
+
const uiaBindings=new Map();
|
|
85
89
|
|
|
86
90
|
async function receipt(kind,payload,stamp=now()){
|
|
87
91
|
return writeReceipt({policy,privateKey,keyId,evidenceDir,kind,payload,nowUtc:stamp});
|
|
@@ -193,15 +197,29 @@ export function createDesktopRuntime({
|
|
|
193
197
|
const confidence=grounded.query_kind==='name_contains'?0.98:1;
|
|
194
198
|
const created=createDesktopTarget({
|
|
195
199
|
frame:fresh.frame,rect:candidate.rect,source:'uia',confidence,
|
|
196
|
-
|
|
200
|
+
// Hash the ephemeral native identity into the target handle to prevent
|
|
201
|
+
// old and new elements at identical coordinates sharing one target_id.
|
|
202
|
+
semanticLabelSha256:Array.isArray(candidate.runtime_id)&&candidate.runtime_id.length>0
|
|
203
|
+
?sha256Object({query_sha256:grounded.query_sha256,runtime_id:candidate.runtime_id})
|
|
204
|
+
:grounded.query_sha256,
|
|
205
|
+
windowBinding:fresh.frame.active_window
|
|
197
206
|
});
|
|
198
207
|
targets.set(created.target_id,created);
|
|
199
|
-
|
|
208
|
+
groundedTargets.add(created.target_id);
|
|
209
|
+
if(candidate.runtime_id!==null&&candidate.runtime_id!==undefined){
|
|
210
|
+
const binding=bindUiaElementR199({frame:fresh.frame,grounded,candidate,query});
|
|
211
|
+
uiaBindings.set(created.target_id,binding);
|
|
212
|
+
}
|
|
213
|
+
while(targets.size>maxTargets){
|
|
214
|
+
const removed=targets.keys().next().value;
|
|
215
|
+
targets.delete(removed);groundedTargets.delete(removed);uiaBindings.delete(removed);
|
|
216
|
+
}
|
|
200
217
|
const evidence=await receipt('desktop_grounding',{
|
|
201
218
|
requested_frame_id:expected.frame_id,grounded_frame_id:fresh.frame.frame_id,
|
|
202
219
|
target_id:created.target_id,query_sha256:grounded.query_sha256,query_kind:grounded.query_kind,
|
|
203
220
|
ordinal:grounded.ordinal,match_count:grounded.matches.length,visited:grounded.visited,truncated:grounded.truncated,
|
|
204
|
-
provider_receipt_sha256:grounded.provider_receipt_sha256,control_type:candidate.control_type
|
|
221
|
+
provider_receipt_sha256:grounded.provider_receipt_sha256,control_type:candidate.control_type,
|
|
222
|
+
element_token_sha256:uiaBindings.get(created.target_id)?.token_sha256??null
|
|
205
223
|
},fresh.frame.observed_at_utc);
|
|
206
224
|
return Object.freeze({
|
|
207
225
|
status:'ok',frame:fresh.frame,target:created,
|
|
@@ -209,15 +227,18 @@ export function createDesktopRuntime({
|
|
|
209
227
|
query_sha256:grounded.query_sha256,query_kind:grounded.query_kind,ordinal:grounded.ordinal,
|
|
210
228
|
match_count:grounded.matches.length,visited:grounded.visited,truncated:grounded.truncated,
|
|
211
229
|
control_type:candidate.control_type,provider_receipt_sha256:grounded.provider_receipt_sha256,
|
|
230
|
+
element_token_sha256:uiaBindings.get(created.target_id)?.token_sha256??null,
|
|
212
231
|
receipt_id:evidence.receipt_id,receipt_sha256:evidence.receipt_sha256
|
|
213
232
|
})
|
|
214
233
|
});
|
|
215
234
|
}
|
|
216
235
|
async function action({
|
|
217
236
|
intentId,kind,expectedFrameId,targetId=null,expectedWindow=undefined,point=null,
|
|
218
|
-
scrollDelta=null,keyChord=null,text=null,effectContext=null
|
|
237
|
+
scrollDelta=null,keyChord=null,text=null,effectContext=null,expectedPostcondition=null
|
|
219
238
|
}={}){
|
|
220
239
|
insist(DESKTOP_ACTION_KINDS.includes(kind),'desktop action kind invalid');
|
|
240
|
+
const postconditionExpectation=validateDesktopPostconditionExpectation(expectedPostcondition);
|
|
241
|
+
const expectedPostconditionSha256=postconditionExpectation?sha256Object(postconditionExpectation):null;
|
|
221
242
|
const authority=await assertDesktopArmed(policy,kind);
|
|
222
243
|
if(kind==='text_input'){
|
|
223
244
|
insist(typeof text==='string','desktop text input invalid');
|
|
@@ -243,6 +264,15 @@ export function createDesktopRuntime({
|
|
|
243
264
|
// bind it to the same fresh desktop/topology/foreground identity and let the native helper
|
|
244
265
|
// revalidate HWND/PID immediately before SendInput instead of requiring unrelated pixels to freeze.
|
|
245
266
|
if(kind!=='key_chord') insist(pre.frame.image_sha256===expected.image_sha256,'desktop pixels changed before action');
|
|
267
|
+
// Before any injection, reacquire the same UIA element through a fresh
|
|
268
|
+
// query bound to the current foreground window and runtime identity.
|
|
269
|
+
if(chosenTarget&&groundedTargets.has(chosenTarget.target_id)){
|
|
270
|
+
const identity=uiaBindings.get(chosenTarget.target_id);
|
|
271
|
+
insist(identity,'R199 UIA target has no RuntimeId binding and is not actionable');
|
|
272
|
+
insist(typeof grounderProvider==='function','R199 UIA re-ground provider unavailable');
|
|
273
|
+
const refreshed=await grounderProvider({frame:pre.frame,query:identity.query});
|
|
274
|
+
assertFreshUiaElementR199(identity,refreshed);
|
|
275
|
+
}
|
|
246
276
|
|
|
247
277
|
let actionPoint=point;
|
|
248
278
|
if(chosenTarget&&['pointer_move','click_primary','click_secondary','double_click_primary','scroll'].includes(kind)){
|
|
@@ -260,7 +290,8 @@ export function createDesktopRuntime({
|
|
|
260
290
|
const effectAction=Object.freeze({
|
|
261
291
|
kind:`desktop_${kind}`,action_sha256:actionObject.action_sha256,frame_id:pre.frame.frame_id,
|
|
262
292
|
target_id:actionObject.target_id,point:actionObject.point,scroll_delta:actionObject.scroll_delta,
|
|
263
|
-
key_chord:actionObject.key_chord,text_sha256:actionObject.text_sha256
|
|
293
|
+
key_chord:actionObject.key_chord,text_sha256:actionObject.text_sha256,
|
|
294
|
+
...(expectedPostconditionSha256?{expected_postcondition_sha256:expectedPostconditionSha256}:{})
|
|
264
295
|
});
|
|
265
296
|
const ctx=effectContext??{};
|
|
266
297
|
const effectLedger=effectIdentity({
|
|
@@ -272,7 +303,8 @@ export function createDesktopRuntime({
|
|
|
272
303
|
intent_id:intentId,kind,expected_frame_id:expected.frame_id,pre_frame_id:pre.frame.frame_id,
|
|
273
304
|
target_id:chosenTarget?.target_id??null,action_sha256:actionObject.action_sha256,
|
|
274
305
|
guard_sha256:guarded.guard_sha256,authority_instance_nonce:authority.instance_nonce,
|
|
275
|
-
effect_id:effectLedger.effect_id,effect_fingerprint:effectLedger.fingerprint
|
|
306
|
+
effect_id:effectLedger.effect_id,effect_fingerprint:effectLedger.fingerprint,
|
|
307
|
+
expected_postcondition_sha256:expectedPostconditionSha256
|
|
276
308
|
});
|
|
277
309
|
|
|
278
310
|
let effect;
|
|
@@ -303,6 +335,9 @@ export function createDesktopRuntime({
|
|
|
303
335
|
try{
|
|
304
336
|
const post=await observerRuntime.capture();
|
|
305
337
|
const bound=bindDesktopPostObservation({guard:guarded,beforeFrame:pre.frame,action:actionObject,afterFrame:post.frame});
|
|
338
|
+
const postcondition=evaluateDesktopPostcondition({expectation:postconditionExpectation,
|
|
339
|
+
beforeFrame:pre.frame,afterFrame:post.frame});
|
|
340
|
+
const postconditionSha256=sha256Object(postcondition);
|
|
306
341
|
const postEvidence=await receipt('desktop_post_observation',{
|
|
307
342
|
started_receipt_id:started.receipt_id,started_receipt_sha256:started.receipt_sha256,
|
|
308
343
|
effect_id:effectLedger.effect_id,effect_fingerprint:effectLedger.fingerprint,
|
|
@@ -329,11 +364,13 @@ export function createDesktopRuntime({
|
|
|
329
364
|
action_sha256:actionObject.action_sha256,guard_sha256:guarded.guard_sha256,
|
|
330
365
|
effect_id:effectLedger.effect_id,effect_fingerprint:effectLedger.fingerprint,effect_completion:effectCompletion,
|
|
331
366
|
effect:{injected:true,side_effect_state:'confirmed_injected',provider_receipt_sha256:HEX64.test(effect.provider_receipt_sha256??'')?effect.provider_receipt_sha256:null},
|
|
332
|
-
action_chain:chain,post_observation:bound
|
|
367
|
+
action_chain:chain,post_observation:bound,
|
|
368
|
+
postcondition,postcondition_sha256:postconditionSha256
|
|
333
369
|
},post.frame.observed_at_utc);
|
|
334
370
|
return Object.freeze({
|
|
335
371
|
status:'ok',injected:true,task_success:null,side_effect_state:'confirmed_injected',
|
|
336
372
|
action:actionObject,guard:guarded,post_observation:bound,action_chain:chain,
|
|
373
|
+
postcondition,postcondition_sha256:postconditionSha256,
|
|
337
374
|
effect_id:effectLedger.effect_id,effect_fingerprint:effectLedger.fingerprint,effect_completion:effectCompletion,
|
|
338
375
|
started_receipt_id:started.receipt_id,started_receipt_sha256:started.receipt_sha256,
|
|
339
376
|
post_receipt_id:postEvidence.receipt_id,post_receipt_sha256:postEvidence.receipt_sha256,
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import {createHash} from 'node:crypto';
|
|
2
|
+
import {canonicalJson} from './canonical-json.mjs';
|
|
3
|
+
|
|
4
|
+
const HEX64=/^[0-9a-f]{64}$/;
|
|
5
|
+
const sha=value=>createHash('sha256').update(canonicalJson(value),'utf8').digest('hex');
|
|
6
|
+
function insist(ok,message){if(!ok)throw new Error(message);}
|
|
7
|
+
export function normalizeUiaRuntimeIdR199(value){
|
|
8
|
+
insist(Array.isArray(value)&&value.length>=1&&value.length<=64,'R199 UIA RuntimeId missing or unbounded');
|
|
9
|
+
insist(value.every(x=>Number.isInteger(x)&&x>=-2147483648&&x<=2147483647),
|
|
10
|
+
'R199 UIA RuntimeId component invalid');
|
|
11
|
+
return Object.freeze([...value]);
|
|
12
|
+
}
|
|
13
|
+
function eqArray(a,b){return a.length===b.length&&a.every((v,i)=>v===b[i]);}
|
|
14
|
+
function eqRect(a,b){return ['left','top','width','height'].every(k=>a?.[k]===b?.[k]);}
|
|
15
|
+
export function bindUiaElementR199({frame,grounded,candidate,query}={}){
|
|
16
|
+
insist(frame?.frame_id&&HEX64.test(frame.frame_id),'R199 UIA frame identity missing');
|
|
17
|
+
insist(typeof grounded?.query_sha256==='string'&&HEX64.test(grounded.query_sha256),'R199 UIA query hash invalid');
|
|
18
|
+
insist(grounded.truncated===false,'R199 UIA scan truncated');
|
|
19
|
+
const runtimeId=normalizeUiaRuntimeIdR199(candidate?.runtime_id);
|
|
20
|
+
insist(candidate.enabled===true&&!candidate.offscreen,'R199 UIA element not actionable');
|
|
21
|
+
const q=Object.freeze({
|
|
22
|
+
kind:query.kind,value:query.value,
|
|
23
|
+
max_results:query.max_results??8,ordinal:query.ordinal??0
|
|
24
|
+
});
|
|
25
|
+
const tokenSha256=sha({schema:'deadbyte.uia-element-binding-r199.v1',
|
|
26
|
+
frame_id:frame.frame_id,desktop_epoch:frame.desktop_epoch,
|
|
27
|
+
active_window:frame.active_window,query_sha256:grounded.query_sha256,
|
|
28
|
+
runtime_id:runtimeId,control_type:candidate.control_type,rect:candidate.rect});
|
|
29
|
+
return Object.freeze({
|
|
30
|
+
schema:'deadbyte.uia-element-binding-r199.v1',
|
|
31
|
+
query:q,query_sha256:grounded.query_sha256,ordinal:grounded.ordinal,
|
|
32
|
+
runtime_id:runtimeId,control_type:candidate.control_type,
|
|
33
|
+
rect:Object.freeze({...candidate.rect}),token_sha256:tokenSha256
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
export function assertFreshUiaElementR199(binding,grounded){
|
|
37
|
+
insist(binding?.schema==='deadbyte.uia-element-binding-r199.v1','R199 UIA binding missing');
|
|
38
|
+
insist(grounded?.status==='ok'&&grounded.truncated===false,'R199 UIA reground scan invalid or incomplete');
|
|
39
|
+
insist(grounded.query_sha256===binding.query_sha256 &&
|
|
40
|
+
grounded.ordinal===binding.ordinal,'R199 UIA query identity changed');
|
|
41
|
+
insist(Array.isArray(grounded.matches)&&binding.ordinal<grounded.matches.length,
|
|
42
|
+
'R199 UIA stale target no longer found');
|
|
43
|
+
const candidate=grounded.matches[binding.ordinal];
|
|
44
|
+
const current=normalizeUiaRuntimeIdR199(candidate?.runtime_id);
|
|
45
|
+
insist(eqArray(binding.runtime_id,current),'R199 UIA element identity changed');
|
|
46
|
+
insist(candidate.control_type===binding.control_type&&candidate.enabled===true&&!candidate.offscreen,
|
|
47
|
+
'R199 UIA control state changed');
|
|
48
|
+
insist(eqRect(candidate.rect,binding.rect),'R199 UIA element rectangle changed');
|
|
49
|
+
return Object.freeze({token_sha256:binding.token_sha256,
|
|
50
|
+
runtime_id_sha256:sha(current),verified:true});
|
|
51
|
+
}
|
package/src/desktop-uia-r40.mjs
CHANGED
|
@@ -73,10 +73,18 @@ export function parseDesktopUiaResponse(bytes,{expectedWindow,query,expectedBoun
|
|
|
73
73
|
};
|
|
74
74
|
const inside=rect.left>=left&&rect.top>=top&&rect.left+rect.width<=left+width&&rect.top+rect.height<=top+height;
|
|
75
75
|
if(!inside) continue;
|
|
76
|
+
let runtimeId=null;
|
|
77
|
+
if(item.runtime_id!==undefined){
|
|
78
|
+
insist(Array.isArray(item.runtime_id)&&item.runtime_id.length<=64,'desktop UIA RuntimeId shape invalid');
|
|
79
|
+
if(item.runtime_id.length===0)insist(q.kind==='root','desktop UIA RuntimeId missing for non-root');
|
|
80
|
+
else runtimeId=Object.freeze(item.runtime_id.map(v=>
|
|
81
|
+
integer(v,'desktop UIA RuntimeId component',{min:-2147483648,max:2147483647})));
|
|
82
|
+
}
|
|
76
83
|
matches.push(Object.freeze({
|
|
77
84
|
rect:Object.freeze(rect),
|
|
78
85
|
control_type:integer(item.control_type,'desktop UIA control_type',{min:0,max:100000}),
|
|
79
|
-
enabled:item.enabled===true,offscreen:item.offscreen===true
|
|
86
|
+
enabled:item.enabled===true,offscreen:item.offscreen===true,
|
|
87
|
+
runtime_id:runtimeId
|
|
80
88
|
}));
|
|
81
89
|
}
|
|
82
90
|
return Object.freeze({
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import {createHash} from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
function sameMetadata(a,b){
|
|
4
|
+
return a.dev===b.dev&&a.ino===b.ino&&a.size===b.size&&
|
|
5
|
+
a.mtimeNs===b.mtimeNs&&a.ctimeNs===b.ctimeNs;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// Hash bytes through the same open FileHandle used for the range read.
|
|
9
|
+
// This does not prove that the pathname was safe before open, or that
|
|
10
|
+
// concurrent in-place writes cannot alter data between individual reads.
|
|
11
|
+
export async function readRangeFromHandle(handle,{offset,length,hashFile=false,signal}){
|
|
12
|
+
if(!handle||typeof handle.read!=='function'||typeof handle.stat!=='function'||
|
|
13
|
+
typeof handle.createReadStream!=='function')throw new TypeError('R196 requires a live FileHandle');
|
|
14
|
+
if(!Number.isSafeInteger(offset)||offset<0||!Number.isSafeInteger(length)||length<0||length>4194304)
|
|
15
|
+
throw new RangeError('R196 invalid bounded file range');
|
|
16
|
+
const before=await handle.stat({bigint:true});
|
|
17
|
+
if(!before.isFile())throw new Error('R196 read requires a regular file');
|
|
18
|
+
if(before.size>BigInt(Number.MAX_SAFE_INTEGER))throw new Error('R196 file size exceeds safe integer');
|
|
19
|
+
const fileSize=Number(before.size);
|
|
20
|
+
if(offset>fileSize)throw new Error('R196 offset exceeds opened file size');
|
|
21
|
+
const buffer=Buffer.alloc(Math.min(length,fileSize-offset));
|
|
22
|
+
const read=buffer.length?await handle.read(buffer,0,buffer.length,offset):{bytesRead:0};
|
|
23
|
+
let digest=null;
|
|
24
|
+
if(hashFile){
|
|
25
|
+
const hash=createHash('sha256');
|
|
26
|
+
await new Promise((resolve,reject)=>{
|
|
27
|
+
const stream=handle.createReadStream({start:0,autoClose:false,signal});
|
|
28
|
+
stream.on('data',chunk=>hash.update(chunk));
|
|
29
|
+
stream.once('error',reject);
|
|
30
|
+
stream.once('end',resolve);
|
|
31
|
+
});
|
|
32
|
+
digest=hash.digest('hex');
|
|
33
|
+
}
|
|
34
|
+
const after=await handle.stat({bigint:true});
|
|
35
|
+
if(!sameMetadata(before,after))throw new Error('R196 opened file changed during read/hash');
|
|
36
|
+
return {
|
|
37
|
+
schema:'deadbyte.machine-fs-read-worker.v1',
|
|
38
|
+
bytes_read:read.bytesRead,
|
|
39
|
+
data_base64:buffer.subarray(0,read.bytesRead).toString('base64'),
|
|
40
|
+
file_sha256:digest,file_size:fileSize,
|
|
41
|
+
file_dev:String(before.dev),file_ino:String(before.ino)
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import {createHash} from 'node:crypto';
|
|
2
|
+
import {lstat,readFile} from 'node:fs/promises';
|
|
3
|
+
import {spawn} from 'node:child_process';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import {fileURLToPath} from 'node:url';
|
|
6
|
+
import {validatePortableRelativePath} from './path-policy.mjs';
|
|
7
|
+
|
|
8
|
+
const SHA=/^[0-9a-f]{64}$/;
|
|
9
|
+
const DECIMAL=/^(?:0|[1-9][0-9]{0,19})$/;
|
|
10
|
+
const EXECUTABLE=fileURLToPath(new URL('../bin/deadbyte-fs-read-r197-fixed.exe',import.meta.url));
|
|
11
|
+
export const R197_NATIVE_READ_EXE_SHA256='7477ca3d94168c8b75711b734df5b9c20a43ecbd9338cfc234700d7e27982b63';
|
|
12
|
+
const hash=b=>createHash('sha256').update(b).digest('hex');
|
|
13
|
+
let pinnedInfo=null;
|
|
14
|
+
async function verifyExecutable(){
|
|
15
|
+
const info=await lstat(EXECUTABLE);
|
|
16
|
+
if(!info.isFile()||info.isSymbolicLink()||info.size<1024||info.size>4*1024*1024)
|
|
17
|
+
throw new Error('R197 native file reader binary is not a regular bounded executable');
|
|
18
|
+
// Only a trust-state fast path, NOT a general same-user tamper sandbox.
|
|
19
|
+
const current=hash(await readFile(EXECUTABLE));
|
|
20
|
+
if(current!==R197_NATIVE_READ_EXE_SHA256)throw new Error('R197 native reader executable SHA-256 mismatch');
|
|
21
|
+
pinnedInfo={sha256:current,bytes:info.size};
|
|
22
|
+
return pinnedInfo;
|
|
23
|
+
}
|
|
24
|
+
export async function boundedNativeReadR197({rootPath,relativePath,offset,length,hashFile=false,timeoutMs=30000}={}){
|
|
25
|
+
if(process.platform!=='win32')throw new Error('R197 native root-relative reader requires Windows');
|
|
26
|
+
if(typeof rootPath!=='string'||!path.win32.isAbsolute(rootPath)||!path.isAbsolute(rootPath))
|
|
27
|
+
throw new Error('R197 native root path invalid');
|
|
28
|
+
validatePortableRelativePath(relativePath);
|
|
29
|
+
if(!Number.isSafeInteger(offset)||offset<0||!Number.isSafeInteger(length)||length<0||length>4*1024*1024)
|
|
30
|
+
throw new Error('R197 native bounded range invalid');
|
|
31
|
+
if(!Number.isInteger(timeoutMs)||timeoutMs<100||timeoutMs>180000)
|
|
32
|
+
throw new Error('R197 native timeout invalid');
|
|
33
|
+
await verifyExecutable();
|
|
34
|
+
return await new Promise((resolve,reject)=>{
|
|
35
|
+
const args=[rootPath,relativePath,String(offset),String(length),hashFile?'1':'0'];
|
|
36
|
+
const child=spawn(EXECUTABLE,args,{windowsHide:true,shell:false,stdio:['ignore','pipe','pipe']});
|
|
37
|
+
const chunks=[];let byteCount=0,stderr='',settled=false,timer=null;
|
|
38
|
+
const finish=(error,value)=>{
|
|
39
|
+
if(settled)return;
|
|
40
|
+
settled=true;if(timer!==null)clearTimeout(timer);
|
|
41
|
+
error?reject(error):resolve(value);
|
|
42
|
+
};
|
|
43
|
+
child.stdout.on('data',chunk=>{
|
|
44
|
+
byteCount+=chunk.length;
|
|
45
|
+
if(byteCount>8*1024*1024){child.kill();finish(new Error('R197 native read stdout exceeds bound'));return;}
|
|
46
|
+
chunks.push(Buffer.from(chunk));
|
|
47
|
+
});
|
|
48
|
+
child.stderr.on('data',chunk=>{
|
|
49
|
+
stderr=(stderr+chunk.toString('utf8')).slice(-16384);
|
|
50
|
+
});
|
|
51
|
+
child.on('error',error=>finish(error));
|
|
52
|
+
child.on('close',(code,signal)=>{
|
|
53
|
+
if(settled)return;
|
|
54
|
+
if(code!==0)return finish(new Error('R197 native root-relative read refused exit='+code+' signal='+(signal??'none')+' '+stderr.slice(-512)));
|
|
55
|
+
let response;
|
|
56
|
+
try{response=JSON.parse(Buffer.concat(chunks).toString('utf8'));}
|
|
57
|
+
catch{return finish(new Error('R197 native read response invalid JSON'));}
|
|
58
|
+
const data=typeof response?.data_base64==='string'?Buffer.from(response.data_base64,'base64'):null;
|
|
59
|
+
if(response?.schema!=='deadbyte.machine-fs-read-worker.v1'||!data||
|
|
60
|
+
response.bytes_read!==data.length||data.length>length||
|
|
61
|
+
!Number.isSafeInteger(response.file_size)||!DECIMAL.test(response.file_dev??'')||
|
|
62
|
+
!DECIMAL.test(response.file_ino??'')||
|
|
63
|
+
(hashFile?!SHA.test(response.file_sha256??''):response.file_sha256!==null))
|
|
64
|
+
return finish(new Error('R197 native read response contract mismatch'));
|
|
65
|
+
return finish(null,response);
|
|
66
|
+
});
|
|
67
|
+
timer=setTimeout(()=>{
|
|
68
|
+
child.kill();
|
|
69
|
+
finish(new Error('R197 native read timed out after '+timeoutMs+'ms'));
|
|
70
|
+
},timeoutMs);
|
|
71
|
+
timer.unref?.();
|
|
72
|
+
});
|
|
73
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import {createHash} from 'node:crypto';
|
|
2
|
+
import {lstat,readFile} from 'node:fs/promises';
|
|
3
|
+
import {spawn} from 'node:child_process';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import {fileURLToPath} from 'node:url';
|
|
6
|
+
import {validatePortableRelativePath} from './path-policy.mjs';
|
|
7
|
+
|
|
8
|
+
const SHA=/^[0-9a-f]{64}$/;
|
|
9
|
+
const DECIMAL=/^(?:0|[1-9][0-9]{0,19})$/;
|
|
10
|
+
const EXECUTABLE=fileURLToPath(new URL('../bin/deadbyte-fs-read-r198.exe',import.meta.url));
|
|
11
|
+
export const R198_NATIVE_READ_EXE_SHA256='4325ae4f033bba195b34d2c49b48493a1a468f3443e369a9bb887a7a129eac16';
|
|
12
|
+
const hash=b=>createHash('sha256').update(b).digest('hex');
|
|
13
|
+
let pinnedInfo=null;
|
|
14
|
+
async function verifyExecutable(){
|
|
15
|
+
const info=await lstat(EXECUTABLE);
|
|
16
|
+
if(!info.isFile()||info.isSymbolicLink()||info.size<1024||info.size>4*1024*1024)
|
|
17
|
+
throw new Error('R198 native file reader binary is not a regular bounded executable');
|
|
18
|
+
// Only a trust-state fast path, NOT a general same-user tamper sandbox.
|
|
19
|
+
const current=hash(await readFile(EXECUTABLE));
|
|
20
|
+
if(current!==R198_NATIVE_READ_EXE_SHA256)throw new Error('R198 native reader executable SHA-256 mismatch');
|
|
21
|
+
pinnedInfo={sha256:current,bytes:info.size};
|
|
22
|
+
return pinnedInfo;
|
|
23
|
+
}
|
|
24
|
+
export async function boundedNativeReadR198({rootPath,relativePath,offset,length,hashFile=false,timeoutMs=30000,expectedRootDev,expectedRootIno}={}){
|
|
25
|
+
if(process.platform!=='win32')throw new Error('R198 native root-relative reader requires Windows');
|
|
26
|
+
if(typeof rootPath!=='string'||!path.win32.isAbsolute(rootPath)||!path.isAbsolute(rootPath))
|
|
27
|
+
throw new Error('R198 native root path invalid');
|
|
28
|
+
validatePortableRelativePath(relativePath);
|
|
29
|
+
if(!Number.isSafeInteger(offset)||offset<0||!Number.isSafeInteger(length)||length<0||length>4*1024*1024)
|
|
30
|
+
throw new Error('R198 native bounded range invalid');
|
|
31
|
+
if(!Number.isInteger(timeoutMs)||timeoutMs<100||timeoutMs>180000)
|
|
32
|
+
throw new Error('R198 native timeout invalid');
|
|
33
|
+
if(typeof expectedRootDev!=='string'||!DECIMAL.test(expectedRootDev)||
|
|
34
|
+
typeof expectedRootIno!=='string'||!DECIMAL.test(expectedRootIno)||expectedRootIno==='0')
|
|
35
|
+
throw new Error('R198 expected root identity missing or invalid');
|
|
36
|
+
await verifyExecutable();
|
|
37
|
+
return await new Promise((resolve,reject)=>{
|
|
38
|
+
const args=[rootPath,relativePath,String(offset),String(length),hashFile?'1':'0',expectedRootDev,expectedRootIno];
|
|
39
|
+
const child=spawn(EXECUTABLE,args,{windowsHide:true,shell:false,stdio:['ignore','pipe','pipe']});
|
|
40
|
+
const chunks=[];let byteCount=0,stderr='',settled=false,timer=null;
|
|
41
|
+
const finish=(error,value)=>{
|
|
42
|
+
if(settled)return;
|
|
43
|
+
settled=true;if(timer!==null)clearTimeout(timer);
|
|
44
|
+
error?reject(error):resolve(value);
|
|
45
|
+
};
|
|
46
|
+
child.stdout.on('data',chunk=>{
|
|
47
|
+
byteCount+=chunk.length;
|
|
48
|
+
if(byteCount>8*1024*1024){child.kill();finish(new Error('R198 native read stdout exceeds bound'));return;}
|
|
49
|
+
chunks.push(Buffer.from(chunk));
|
|
50
|
+
});
|
|
51
|
+
child.stderr.on('data',chunk=>{
|
|
52
|
+
stderr=(stderr+chunk.toString('utf8')).slice(-16384);
|
|
53
|
+
});
|
|
54
|
+
child.on('error',error=>finish(error));
|
|
55
|
+
child.on('close',(code,signal)=>{
|
|
56
|
+
if(settled)return;
|
|
57
|
+
if(code!==0)return finish(new Error('R198 native root-relative read refused exit='+code+' signal='+(signal??'none')+' '+stderr.slice(-512)));
|
|
58
|
+
let response;
|
|
59
|
+
try{response=JSON.parse(Buffer.concat(chunks).toString('utf8'));}
|
|
60
|
+
catch{return finish(new Error('R198 native read response invalid JSON'));}
|
|
61
|
+
const data=typeof response?.data_base64==='string'?Buffer.from(response.data_base64,'base64'):null;
|
|
62
|
+
if(response?.schema!=='deadbyte.machine-fs-read-worker.v1'||!data||
|
|
63
|
+
response.bytes_read!==data.length||data.length>length||
|
|
64
|
+
!Number.isSafeInteger(response.file_size)||!DECIMAL.test(response.file_dev??'')||
|
|
65
|
+
!DECIMAL.test(response.file_ino??'')||
|
|
66
|
+
(hashFile?!SHA.test(response.file_sha256??''):response.file_sha256!==null))
|
|
67
|
+
return finish(new Error('R198 native read response contract mismatch'));
|
|
68
|
+
return finish(null,response);
|
|
69
|
+
});
|
|
70
|
+
timer=setTimeout(()=>{
|
|
71
|
+
child.kill();
|
|
72
|
+
finish(new Error('R198 native read timed out after '+timeoutMs+'ms'));
|
|
73
|
+
},timeoutMs);
|
|
74
|
+
timer.unref?.();
|
|
75
|
+
});
|
|
76
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import { readRangeFromHandle } from './machine-fs-handle-read-r196.mjs';
|
|
3
3
|
|
|
4
4
|
const [file, offsetText, lengthText, hashText] = process.argv.slice(2);
|
|
5
5
|
const offset = Number(offsetText);
|
|
@@ -13,33 +13,13 @@ if (typeof file !== 'string' || !Number.isSafeInteger(offset) || offset < 0 ||
|
|
|
13
13
|
|
|
14
14
|
try {
|
|
15
15
|
const handle = await fs.open(file, 'r');
|
|
16
|
-
let
|
|
16
|
+
let result;
|
|
17
17
|
try {
|
|
18
|
-
|
|
19
|
-
const result = length ? await handle.read(buffer, 0, length, offset) : { bytesRead: 0 };
|
|
20
|
-
bytes = buffer.subarray(0, result.bytesRead);
|
|
18
|
+
result = await readRangeFromHandle(handle,{offset,length,hashFile:hashText==='1'});
|
|
21
19
|
} finally {
|
|
22
20
|
await handle.close();
|
|
23
21
|
}
|
|
24
|
-
|
|
25
|
-
let fileSha256 = null;
|
|
26
|
-
if (hashText === '1') {
|
|
27
|
-
const hash = createHash('sha256');
|
|
28
|
-
await new Promise((resolve, reject) => {
|
|
29
|
-
const stream = createReadStream(file);
|
|
30
|
-
stream.on('data', chunk => hash.update(chunk));
|
|
31
|
-
stream.once('error', reject);
|
|
32
|
-
stream.once('end', resolve);
|
|
33
|
-
});
|
|
34
|
-
fileSha256 = hash.digest('hex');
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
process.stdout.write(JSON.stringify({
|
|
38
|
-
schema: 'deadbyte.machine-fs-read-worker.v1',
|
|
39
|
-
bytes_read: bytes.length,
|
|
40
|
-
data_base64: bytes.toString('base64'),
|
|
41
|
-
file_sha256: fileSha256
|
|
42
|
-
}));
|
|
22
|
+
process.stdout.write(JSON.stringify(result));
|
|
43
23
|
} catch (error) {
|
|
44
24
|
process.stderr.write(String(error?.stack ?? error));
|
|
45
25
|
process.exit(1);
|