deadbyte-mcp 0.15.74 → 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.
Files changed (103) hide show
  1. package/MANIFEST.SHA256 +102 -42
  2. package/PROVENANCE.json +19 -21
  3. package/README.txt +23 -18
  4. package/RELEASE.json +51 -18
  5. package/SBOM.cdx.json +2 -2
  6. package/TEST-MATRIX.json +22 -2
  7. package/bin/deadbyte-desktop-uia-r199-fixed.exe +0 -0
  8. package/bin/deadbyte-desktop-uia-r199-fixed.obj +0 -0
  9. package/bin/deadbyte-desktop-uia-r199.exe +0 -0
  10. package/bin/deadbyte-desktop-uia-r199.obj +0 -0
  11. package/bin/deadbyte-fs-read-r197-fixed.exe +0 -0
  12. package/bin/deadbyte-fs-read-r198.exe +0 -0
  13. package/bin/deadbyte-fs-read-r198.obj +0 -0
  14. package/bin/deadbyte-root-relative-probe-r197.exe +0 -0
  15. package/bridge/package.json +1 -1
  16. package/controller/deadbyte-process-policy.json +4 -4
  17. package/docs/R184-package-lock.json +2 -8
  18. package/native/deadbyte-desktop-uia-r199.cpp +345 -0
  19. package/native/deadbyte-fs-read-r197.cpp +174 -0
  20. package/native/deadbyte-fs-read-r198.cpp +181 -0
  21. package/native/deadbyte-root-relative-probe-r197.cpp +97 -0
  22. package/package.json +4 -2
  23. package/scripts/build-native-desktop-uia-r199-fixed.ps1 +28 -0
  24. package/scripts/build-native-desktop-uia-r199.ps1 +28 -0
  25. package/scripts/build-native-fs-read-r197-fixed.ps1 +28 -0
  26. package/scripts/build-native-fs-read-r197.ps1 +28 -0
  27. package/scripts/build-native-fs-read-r198.ps1 +28 -0
  28. package/scripts/build-native-root-probe-r197.ps1 +28 -0
  29. package/scripts/gate-windows-r195.ps1 +114 -0
  30. package/scripts/gate-windows-r206.ps1 +116 -0
  31. package/scripts/init-desktop-policy-r199.ps1 +85 -0
  32. package/scripts/npm-release-r48.mjs +17 -0
  33. package/scripts/npm-release-readiness-r195.mjs +72 -0
  34. package/scripts/npm-release-readiness-r206.mjs +76 -0
  35. package/scripts/r193-owner-request-live.mjs +5 -6
  36. package/scripts/r193-owner-uac-broker.mjs +3 -3
  37. package/scripts/r205-candidate-selection.mjs +22 -0
  38. package/src/candidate-selection-evidence-r206.mjs +71 -0
  39. package/src/candidate-selection-r205.mjs +96 -0
  40. package/src/desktop-postcondition-r196.mjs +30 -0
  41. package/src/desktop-runtime-r40.mjs +44 -7
  42. package/src/desktop-uia-identity-r199.mjs +51 -0
  43. package/src/desktop-uia-r40.mjs +9 -1
  44. package/src/machine-fs-handle-read-r196.mjs +43 -0
  45. package/src/machine-fs-native-read-r197.mjs +73 -0
  46. package/src/machine-fs-native-read-r198.mjs +76 -0
  47. package/src/machine-fs-read-worker.mjs +5 -25
  48. package/src/machine-fs.mjs +31 -16
  49. package/src/r195-canonical-owner-observation.mjs +15 -0
  50. package/src/remote-destination-journal-r202.mjs +120 -0
  51. package/src/remote-device-r44.mjs +89 -14
  52. package/src/remote-payload-boundary-r201.mjs +71 -0
  53. package/src/remote-terminal-evidence-r200.mjs +60 -0
  54. package/src/version.mjs +1 -1
  55. package/test/desktop-runtime-r40.test.mjs +111 -0
  56. package/test/desktop-uia-r40.test.mjs +15 -0
  57. package/test/fixtures/r197-junction-swap-worker.mjs +21 -0
  58. package/test/fixtures/r203-journal-writer.mjs +6 -0
  59. package/test/process-policy-refresh.test.mjs +4 -4
  60. package/test/r130-release-security-contract.test.mjs +8 -8
  61. package/test/r132-release-identity.test.mjs +7 -7
  62. package/test/r153-prepublish-identity.test.mjs +1 -1
  63. package/test/r160-release-contract.test.mjs +3 -3
  64. package/test/r165-live-cli-skew.test.mjs +1 -1
  65. package/test/r167-release-contract.test.mjs +11 -11
  66. package/test/r168-release-contract.test.mjs +4 -4
  67. package/test/r172-release-contract.test.mjs +6 -6
  68. package/test/r173-release-contract.test.mjs +16 -16
  69. package/test/r179-release-contract.test.mjs +12 -12
  70. package/test/r190-release-contract.test.mjs +10 -10
  71. package/test/r191-release-contract.test.mjs +12 -12
  72. package/test/r192-release-identity.test.mjs +9 -9
  73. package/test/r193-release-identity.test.mjs +24 -22
  74. package/test/r193-release-readiness.test.mjs +2 -2
  75. package/test/r193-windows-gate.test.mjs +8 -8
  76. package/test/r194-release-identity.test.mjs +19 -15
  77. package/test/r194-release-readiness.test.mjs +2 -2
  78. package/test/r194-windows-gate.test.mjs +7 -6
  79. package/test/r195-canonical-owner-observation.test.mjs +32 -0
  80. package/test/r195-release-readiness.test.mjs +22 -0
  81. package/test/r196-desktop-postcondition.test.mjs +53 -0
  82. package/test/r196-handle-bound-read.test.mjs +99 -0
  83. package/test/r196-remote-outcome.test.mjs +94 -0
  84. package/test/r197-namespace-race.test.mjs +41 -0
  85. package/test/r197-native-dispatch-race.test.mjs +49 -0
  86. package/test/r197-native-file-read.test.mjs +60 -0
  87. package/test/r197-native-root-open.test.mjs +61 -0
  88. package/test/r198-root-identity.test.mjs +88 -0
  89. package/test/r199-native-uia-worker.test.mjs +28 -0
  90. package/test/r199-uia-identity.test.mjs +43 -0
  91. package/test/r200-remote-reconcile.test.mjs +106 -0
  92. package/test/r201-payload-boundary.test.mjs +78 -0
  93. package/test/r201-payload-fuzz.test.mjs +37 -0
  94. package/test/r201-tool-boundary-fuzz.test.mjs +120 -0
  95. package/test/r202-destination-journal.test.mjs +92 -0
  96. package/test/r203-atomic-journal.test.mjs +86 -0
  97. package/test/r205-candidate-selection-cli.test.mjs +31 -0
  98. package/test/r205-candidate-selection.test.mjs +83 -0
  99. package/test/r206-candidate-evidence.test.mjs +64 -0
  100. package/test/r206-release-contract.test.mjs +42 -0
  101. package/test/r206-release-readiness.test.mjs +23 -0
  102. package/test/r59-tool-surface-compat.test.mjs +1 -1
  103. 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
- semanticLabelSha256:grounded.query_sha256,windowBinding:fresh.frame.active_window
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
- while(targets.size>maxTargets)targets.delete(targets.keys().next().value);
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
+ }
@@ -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 { createHash } from 'node:crypto';
2
- import { createReadStream, promises as fs } from 'node:fs';
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 bytes;
16
+ let result;
17
17
  try {
18
- const buffer = Buffer.alloc(length);
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);