deadbyte-mcp 0.15.68 → 0.15.70

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/MANIFEST.SHA256 +85 -38
  2. package/PROVENANCE.json +52 -24
  3. package/README.txt +44 -31
  4. package/RELEASE.json +47 -17
  5. package/SBOM.cdx.json +2 -2
  6. package/TEST-MATRIX.json +44 -2
  7. package/bridge/package.json +1 -1
  8. package/controller/deadbyte-controller.ps1 +65 -1
  9. package/controller/deadbyte-process-policy.json +4 -4
  10. package/docs/R184-package-lock.json +97 -0
  11. package/docs/R186-OWNER-CONSENT-HANDOFF.txt +87 -0
  12. package/package.json +1 -1
  13. package/scripts/install-r186-owner-trust.ps1 +59 -0
  14. package/scripts/r175-focused-runner.mjs +38 -0
  15. package/scripts/r176-log-preview.mjs +18 -0
  16. package/scripts/r177-owner-trust-inventory.ps1 +36 -0
  17. package/scripts/r178-full-regression.mjs +21 -0
  18. package/scripts/r178-npm-ci.mjs +10 -0
  19. package/scripts/r178-npm-preflight.mjs +16 -0
  20. package/scripts/r178-release-gates-readonly.mjs +9 -0
  21. package/scripts/r178-shard-regression.mjs +25 -0
  22. package/scripts/r179-predecessor-registry.mjs +24 -0
  23. package/scripts/r179-protected-inventory.ps1 +21 -0
  24. package/scripts/r186-focused-runner.mjs +22 -0
  25. package/scripts/r186-owner-anchor-inspect.mjs +6 -0
  26. package/scripts/r186-owner-consent-check.mjs +86 -0
  27. package/scripts/r186-owner-grant-cli.mjs +45 -0
  28. package/scripts/r186-owner-key-local.mjs +22 -0
  29. package/scripts/r186-owner-review-export.mjs +34 -0
  30. package/src/deadbyte-cli.mjs +130 -56
  31. package/src/foreground-owner-recovery-r190.mjs +117 -0
  32. package/src/r174-owner-webauthn.mjs +176 -0
  33. package/src/r175-foreground-boundary.mjs +28 -0
  34. package/src/r186-owner-anchor.mjs +12 -0
  35. package/src/r186-owner-consent-preflight.mjs +63 -0
  36. package/src/r186-owner-grant-broker.mjs +96 -0
  37. package/src/r186-owner-local-signing.mjs +25 -0
  38. package/src/r186-owner-request.mjs +40 -0
  39. package/src/r186-owner-review-export.mjs +49 -0
  40. package/src/remote-live-log.mjs +81 -5
  41. package/src/remote-session-guardian.mjs +23 -0
  42. package/src/version.mjs +1 -1
  43. package/test/helpers/r175-active-pointer.mjs +28 -0
  44. package/test/helpers/release-lock.mjs +12 -0
  45. package/test/r129-sdk-security-baseline.test.mjs +2 -1
  46. package/test/r130-release-security-contract.test.mjs +11 -10
  47. package/test/r132-release-identity.test.mjs +12 -11
  48. package/test/r153-prepublish-identity.test.mjs +1 -1
  49. package/test/r155-default-live-log.test.mjs +17 -16
  50. package/test/r160-package-decision-manifest.test.mjs +1 -1
  51. package/test/r160-release-contract.test.mjs +4 -5
  52. package/test/r163-isolated-slot-rollback-restore.test.mjs +8 -8
  53. package/test/r165-live-cli-skew.test.mjs +6 -6
  54. package/test/r167-release-contract.test.mjs +15 -13
  55. package/test/r168-protected-entrypoint.test.mjs +5 -2
  56. package/test/r168-release-contract.test.mjs +4 -3
  57. package/test/r168-remote-one-shot-coherence.test.mjs +2 -1
  58. package/test/r172-release-contract.test.mjs +7 -7
  59. package/test/r173-release-contract.test.mjs +18 -17
  60. package/test/r174-owner-webauthn.test.mjs +144 -0
  61. package/test/r175-cli-flags.test.mjs +23 -0
  62. package/test/r175-foreground-boundary.test.mjs +87 -0
  63. package/test/r175-guardian-owner-boundary.test.mjs +56 -0
  64. package/test/r176-pretty-live-log.test.mjs +54 -0
  65. package/test/r177-windows-absolute-live-path.test.mjs +37 -0
  66. package/test/r179-absolute-path-live-log.test.mjs +40 -0
  67. package/test/r179-release-contract.test.mjs +62 -0
  68. package/test/r186-controller-owner-grant.test.mjs +20 -0
  69. package/test/r186-owner-consent-preflight.test.mjs +98 -0
  70. package/test/r186-owner-enrollment.test.mjs +20 -0
  71. package/test/r186-owner-grant-broker.test.mjs +85 -0
  72. package/test/r186-owner-local-signing.test.mjs +30 -0
  73. package/test/r186-owner-request.test.mjs +28 -0
  74. package/test/r186-owner-review-export.test.mjs +52 -0
  75. package/test/r190-foreground-cli.test.mjs +41 -0
  76. package/test/r190-foreground-owner-recovery.test.mjs +61 -0
  77. package/test/r190-release-contract.test.mjs +33 -0
  78. package/test/r48-release-contract.test.mjs +2 -1
  79. package/test/r51-runtime-coherence.test.mjs +4 -2
  80. package/test/r58-cleanup-retention.test.mjs +5 -12
  81. package/test/r58-concurrent-recovery.test.mjs +5 -1
  82. package/test/r58-stale-owner.test.mjs +9 -3
  83. package/test/r59-tool-surface-compat.test.mjs +1 -1
  84. package/test/remote-live-log-r37.test.mjs +2 -2
  85. package/test/remote-live-log-recovery-r36.test.mjs +1 -1
  86. package/test/remote-session-cli.test.mjs +92 -126
  87. package/node_modules/which/CHANGELOG.md +0 -166
@@ -27,11 +27,14 @@ import {foregroundIdentityMatches,queryForegroundIdentity,validForegroundIdentit
27
27
  import {readRemoteMarkerSnapshot,deletePinnedRemoteMarker} from './foreground-marker-cas-win32.mjs';
28
28
  import {acquireForegroundSessionLock} from './foreground-session-lock.mjs';
29
29
  import { AUTHORITY_PLANES, authorityVector, authorityRestorePlan, parseAuthorityObservation, assertAuthorityVector, assertRuntimeReadyObservation, authorityBanner } from './remote-authority.mjs';
30
+ import {observeOnlyAuthority,assertObserveOnlyMarker,assertMatchingForegroundRelease} from './r175-foreground-boundary.mjs';
30
31
  import { decodeRemoteLogBytes, boundedRemoteDiagnostic, readRemoteLogAppend } from './remote-log-decode-r60.mjs';
31
32
  import {reconcileStaleMarker,resumeMarkerReconciliation,recoverPublishedMarkerTerminal} from './clean-activation-recovery-r153.mjs';
32
33
  import {selectLegacyMarkerEvidence,verifyLocatedLegacyMarkerReceipt} from './legacy-marker-evidence-r159.mjs';
33
34
  import {normalizeHostCommandPins,encodeHostCommandPins} from './host-command-pins-r154.mjs';
34
35
  import {verifyPackageDecisionManifest} from './package-identity-r160.mjs';
36
+ import {inspectTrustedInstallation} from './privileged-install-trust-r89.mjs';
37
+ import {recoverLegacyForegroundOwnership} from './foreground-owner-recovery-r190.mjs';
35
38
 
36
39
  const REMOTE_COMMANDS=new Set([
37
40
  'menu','start','resume','park','stop','restart','status','url','test','fulltest','toggle','clean','machinetest',
@@ -61,14 +64,15 @@ function printHelp(){
61
64
  ` npx -y ${exact} doctor # verify current package/release truth`,
62
65
  ` npx -y ${exact} mcp # stdio MCP server for local clients`,
63
66
  ` npx -y ${exact} remote # safe observation-only foreground session`,
64
- ` npx -y ${exact} remote --operator # process/coding/autonomous; explicit owner gates still apply`,
65
- ` npx -y ${exact} remote --desktop # session with Desktop authority; Host remains disarmed`,
66
- ` npx -y ${exact} remote --host # session with Host authority; Desktop remains disarmed`,
67
- ` npx -y ${exact} remote --full # explicit five-plane foreground session`,
67
+ ` npx -y ${exact} remote --operator # R175 blocked pending session-owned authority issuance`,
68
+ ` npx -y ${exact} remote --desktop # R175 blocked pending session-owned authority issuance`,
69
+ ` npx -y ${exact} remote --host # R175 blocked pending session-owned authority issuance`,
70
+ ` npx -y ${exact} remote --full # R175 blocked pending session-owned authority issuance`,
68
71
  ` npx -y ${exact} remote --resume <cursor> # replay missed results, then reconnect`,
69
72
  ` npx -y ${exact} remote --trace # show trace/span identity without payload detail`,
70
73
 
71
74
  ` npx -y ${exact} remote --filter <expr> # presentation filter: tool/category/status/trace`,
75
+ ` npx -y ${exact} remote reconcileforeground --confirm-marker-sha256 <sha256> # protected marker-only recovery`,
72
76
  ` npx -y ${exact} remote status`,
73
77
  ` npx -y ${exact} remote restart`,
74
78
  ` npx -y ${exact} supervisor status`,
@@ -400,8 +404,8 @@ function timelineFilterEvent(timeline){
400
404
  }
401
405
  async function tailRemoteLogs(root,signal,{journal=null,offsets=null,activityTracker=null,timelineFilter=null,timelineWriter=null,verbose=false,displayMode='unicode'}={}){
402
406
  process.stdout.write(verbose
403
- ? '[LOG] Verbose live timeline. Full sanitized request detail enabled; Ctrl+C disconnects and revokes authority.\n'
404
- : '[LOG] Live tool calls with individual START/DONE, sanitized arguments and results. Transport hidden; signed audit remains on disk. Ctrl+C disconnects and revokes authority.\n');
407
+ ? '[LOG] Verbose live timeline. Full sanitized request detail enabled; Ctrl+C disconnects; owner standing grants remain unchanged.\n'
408
+ : '[LOG] Live tool calls with individual START/DONE, sanitized arguments and results. Transport hidden; signed audit remains on disk. Ctrl+C disconnects; owner standing grants remain unchanged.\n');
405
409
  const activeOffsets=offsets??await snapshotRemoteLogOffsets(root);
406
410
  const partialLines=new Map();
407
411
  let liveFiles=[...activeOffsets.keys()];
@@ -455,7 +459,13 @@ function remoteTag(kind,displayMode){
455
459
  return (displayMode==='ascii'?ascii:unicode)[kind];
456
460
  }
457
461
  function writeRemote(stream,text,displayMode){stream.write(remoteDisplayText(text,displayMode));}
458
- async function cleanupRemoteSession(ctx,{displayMode='unicode'}={}){
462
+ async function cleanupRemoteSession(ctx,{displayMode='unicode',marker=null}={}){
463
+ // R175 observation sessions issue no grants and must never revoke owner grants or park runtime.
464
+ // An invalid R175 marker must fail closed, not fall through to legacy revoke.
465
+ if(marker!==null){
466
+ if(!assertObserveOnlyMarker(marker))throw new Error('R175_INVALID_OBSERVE_ONLY_MARKER: no authority cleanup performed');
467
+ return;
468
+ }
459
469
  const failed=[];
460
470
  for(const command of SESSION_CLEANUP){
461
471
  try{
@@ -669,6 +679,15 @@ async function recoverRemoteSession(ctx,{displayMode='unicode'}={}){
669
679
  const markerPath=remoteSessionMarker(ctx.root);
670
680
  const snapshot=await readRemoteMarkerSnapshot(markerPath);
671
681
  if(!snapshot) return null;
682
+ if(assertObserveOnlyMarker(prior)){
683
+ // Owner and guardian lifetime were verified above. A monitor-only session
684
+ // owns the CAS marker, not any authority epoch or runtime lifecycle.
685
+ if(snapshot.marker?.owner_pid!==prior.owner_pid||snapshot.marker?.session_nonce!==prior.session_nonce)
686
+ throw new Error('R175 observation-only stale marker identity changed; marker retained');
687
+ if(deletePinnedRemoteMarker(markerPath,snapshot.sha256)!=='REMOVED')
688
+ throw new Error('R175 observation-only stale marker CAS failed; marker retained');
689
+ return null;
690
+ }
672
691
  const active=await readActiveRuntimeBinding(ctx.root);
673
692
  if(!active)throw new Error(`remote session ownership unverified for stale pid=${ownerPid}; active pointer missing; marker retained`);
674
693
  const observed=observedAuthority(ctx);
@@ -740,7 +759,7 @@ async function claimRemoteSession(ctx){
740
759
  const dir=path.join(ctx.root,'runtime');await mkdir(dir,{recursive:true});
741
760
  const nonce=randomBytes(32).toString('hex');
742
761
  const resultSessionId=randomBytes(12).toString('hex');
743
- const marker={schema:'deadbyte.remote-session.v2',owner_pid:process.pid,guardian_pid:0,session_nonce:nonce,
762
+ const marker={schema:'deadbyte.remote-session.v2',authority_scope:'observe-only',owner_pid:process.pid,guardian_pid:0,session_nonce:nonce,
744
763
  result_session_id:resultSessionId,started_at_utc:new Date().toISOString()};
745
764
  marker.owner_identity=queryForegroundIdentity(process.pid);
746
765
  if(!marker.owner_identity)throw new Error('remote foreground identity unavailable');
@@ -749,7 +768,7 @@ async function claimRemoteSession(ctx){
749
768
  catch(error){if(error?.code==='EEXIST') throw new Error('remote session already claimed');throw error;}
750
769
  let guardian=null;
751
770
  try{
752
- guardian=spawn(process.execPath,[GUARDIAN_SCRIPT,'--state-root',ctx.root,'--owner-pid',String(process.pid),'--session-nonce',nonce,'--owner-channel','ipc'],{
771
+ guardian=spawn(process.execPath,[GUARDIAN_SCRIPT,'--state-root',ctx.root,'--owner-pid',String(process.pid),'--session-nonce',nonce,'--owner-channel','ipc','--authority-scope','observe-only'],{
753
772
  detached:true,stdio:['ignore','ignore','ignore','ipc'],windowsHide:true,shell:false,env:process.env
754
773
  });
755
774
  if(!Number.isInteger(guardian.pid)||guardian.pid<1) throw new Error('remote session guardian did not start');
@@ -786,6 +805,10 @@ async function releaseRemoteSession(ctx,marker,{displayMode='unicode'}={}){
786
805
  throw new Error('remote session ownership marker replaced before native deletion; marker retained');
787
806
  }
788
807
  async function runRemoteSession({resumeCursor=null,verbose=false,trace=false,filterExpression='',mode='default',hostCommandPins=[]}={}){
808
+ // R175: elevated session flags need a distinct signed session-owned lease
809
+ // issuer before they can safely coexist with owner standing grants.
810
+ if(mode!=='default') throw new Error('R175_FOREGROUND_OWNER_BOUNDARY: elevated foreground modes require session-owned grants; use explicit remote arm* commands');
811
+ if(hostCommandPins.length) throw new Error('R175_FOREGROUND_OBSERVE_ONLY: host command repins require an explicit owner action outside foreground logging');
789
812
  const display=prepareRemoteConsole();
790
813
  const displayMode=display.mode;
791
814
  const ctx=await remoteContext();
@@ -793,12 +816,15 @@ async function runRemoteSession({resumeCursor=null,verbose=false,trace=false,fil
793
816
  // an EPOCH_NATIVE production runtime. This guard precedes session recovery,
794
817
  // marker claims, revocations and any controller startup.
795
818
  const active=await readActiveRuntimeBinding(ctx.root);
819
+ const currentManifestSha256=createHash('sha256')
820
+ .update(await readFile(path.join(CURRENT_PACKAGE_ROOT,'MANIFEST.SHA256'))).digest('hex');
821
+ assertMatchingForegroundRelease(active,{version:DEADBYTE_VERSION,manifest_sha256:currentManifestSha256});
796
822
  if(active){
797
823
  const decision=selectR168OneShotRemotePath({command:'start',active,cliVersion:DEADBYTE_VERSION});
798
824
  if(decision.mode==='blocked_version_skew') throw new Error(decision.message);
799
825
  }
800
- // First installation without an active pointer retains the existing
801
- // unbound bootstrap path. A bound older release must never auto-upgrade.
826
+ // R175 foreground is monitor-only: deployment and upgrades need separate owner action.
827
+ // Never synchronize runtime or mint/revoke a grant from this path.
802
828
  if(displayMode==='ascii') writeRemote(process.stdout,`[DISPLAY] ASCII fallback: ${display.reason}\n`,displayMode);
803
829
  if(resumeCursor!==null) await replayRemoteResults(ctx.root,resumeCursor,{displayMode});
804
830
  const startupLock=await acquireForegroundSessionLock({root:ctx.root,powershell:ctx.powershell});
@@ -807,9 +833,21 @@ async function runRemoteSession({resumeCursor=null,verbose=false,trace=false,fil
807
833
  startupLock.assertHeld();
808
834
  await verifyPackageDecisionManifest({root:CURRENT_PACKAGE_ROOT,expectedVersion:DEADBYTE_VERSION});
809
835
  startupLock.assertHeld();
810
- const migrationContext=await recoverRemoteSession(ctx,{displayMode});
836
+ // R179: an observation session may recover ONLY its own observation marker.
837
+ // Legacy migration/revocation is a separately authorized controller action.
838
+ const staleObserver=await readRemoteSessionMarker(ctx.root);
839
+ if(staleObserver){
840
+ if(!assertObserveOnlyMarker(staleObserver))
841
+ throw new Error('R179_LEGACY_FOREGROUND_RECOVERY_REQUIRES_EXPLICIT_OWNER_COMMAND: no authority touched');
842
+ await recoverRemoteSession(ctx,{displayMode});
843
+ }
811
844
  startupLock.assertHeld();
812
- await synchronizeForegroundRuntime(ctx,{displayMode,migrationContext,hostCommandPins});
845
+ const pinned=await readActiveRuntimeBinding(ctx.root);
846
+ assertMatchingForegroundRelease(pinned,{version:DEADBYTE_VERSION,manifest_sha256:currentManifestSha256});
847
+ for(const field of ['release_id','generation_manifest_sha256','generation_root']){
848
+ if(pinned[field]!==active[field])
849
+ throw new Error('R175_FOREGROUND_ACTIVE_DRIFT: no runtime update or grant mutation attempted');
850
+ }
813
851
  startupLock.assertHeld();
814
852
  marker=await claimRemoteSession(ctx);
815
853
  startupLock.assertHeld();
@@ -838,51 +876,29 @@ async function runRemoteSession({resumeCursor=null,verbose=false,trace=false,fil
838
876
  process.stdout.write('[BOOT] Starting local + remote runtime...\n');
839
877
  const preflight=invokeController(ctx,'status',{capture:true,allowFailure:true,echoCaptured:false,timeoutMs:REMOTE_STATUS_TIMEOUT_MS});
840
878
  let startText=preflight.stdout||'';
841
- if(preflight.status===0&&controllerRuntimeReady(preflight.stdout)){
842
- process.stdout.write('[BOOT] Runtime already ONLINE; reusing verified bridge/cloud/supervisor.\n');
843
- }else{
844
- const recovery=preflight.status===0&&controllerRuntimeDegraded(preflight.stdout);
845
- process.stdout.write(recovery?'[RECOVERY] Runtime failed readiness; restarting verified owned components...\n':'[BOOT] Starting bridge/runtime...\n');
846
- const startCommand=recovery?'restart':'start';
847
- const controllerLogDir=path.join(ctx.root,'runtime','logs');
848
- await mkdir(controllerLogDir,{recursive:true});
849
- const launchOutput={
850
- stdout_path:path.join(controllerLogDir,`controller-${startCommand}.stdout.log`),
851
- stderr_path:path.join(controllerLogDir,`controller-${startCommand}.stderr.log`)
852
- };
853
- const launch=invokeController(ctx,startCommand,{allowFailure:true,redirectOutput:launchOutput,timeoutMs:REMOTE_START_TIMEOUT_MS});
854
- if(launch.status!==0){
855
- const evidence=await readControllerStartFailure(ctx.root,startCommand);
856
- const fallback=evidence?'':await readTailBounded(launchOutput.stderr_path,2048);
857
- const detail=evidence?`; controller-start-failure: ${evidence.message}`:(fallback?`; controller-stderr: ${fallback}`:'');
858
- throw new Error(`remote controller '${startCommand}' failed exit=${launch.status}${detail}`);
859
- }
860
- const started=invokeController(ctx,'status',{capture:true,echoCaptured:false,timeoutMs:REMOTE_STATUS_TIMEOUT_MS});
861
- if(started.status!==0||!controllerRuntimeReady(started.stdout)) throw new Error(`remote controller ${recovery?'restart':'start'} returned without a verified ONLINE runtime`);
862
- startText=started.stdout||'';
863
- }
879
+ if(preflight.status!==0||!controllerRuntimeReady(preflight.stdout))
880
+ throw new Error('R175_FOREGROUND_RUNTIME_NOT_READY: monitoring cannot start or restart a runtime; use explicit remote start');
881
+ process.stdout.write('[BOOT] Runtime already ONLINE; reusing verified bridge/cloud/supervisor.\n');
864
882
  process.stdout.write(`${formatRemoteLifecycle('bridge_connected',{displayMode})}\n`);
865
883
  process.stdout.write(`${formatRemoteLifecycle('cloud_connected',{displayMode})}\n`);
866
- const desired=authorityVector(mode);
867
- const priorAuthority=observedAuthority(ctx);
868
- for(const plane of ['host','desktop','autonomous','coding','process'].filter(name=>!desired[name]&&priorAuthority.authority[name].armed)){
869
- const command=`disarm${plane}`;
870
- writeRemote(process.stdout,`${remoteTag('auth',displayMode)} Revoking unwanted ${plane} authority...\n`,displayMode);
871
- invokeController(ctx,command,{capture:true,echoCaptured:false,timeoutMs:REMOTE_STATUS_TIMEOUT_MS});
872
- }
873
- for(const plane of AUTHORITY_PLANES.filter(name=>desired[name])){
874
- const command=`arm${plane}`;
875
- writeRemote(process.stdout,`${remoteTag('auth',displayMode)} Arming ${command.slice(3)}...\n`,displayMode);
876
- invokeController(ctx,command,{capture:true,echoCaptured:false,timeoutMs:REMOTE_STATUS_TIMEOUT_MS});
877
- }
878
- const url=startText.match(/^URL\s*:\s*(\S+)/m)?.[1]||'';
879
- writeRemote(process.stdout,`${remoteTag('auth',displayMode)} Verifying exact ${mode} authority vector...\n`,displayMode);
884
+ // Observation is never a grant transition or an expectation that live
885
+ // owner grants remain frozen for the lifetime of this log session.
880
886
  const current=await readActiveRuntimeBinding(ctx.root);
881
- const authority=assertAuthorityVector(assertRuntimeReadyObservation(observedAuthority(ctx),{
882
- coreRoot:current?.release_root??null
883
- }),desired);
887
+ for(const field of ['release_id','manifest_sha256','generation_manifest_sha256','generation_root'])
888
+ if(current?.[field]!==active[field])throw new Error('R175_FOREGROUND_ACTIVE_DRIFT: observed release changed');
889
+ const authority=assertRuntimeReadyObservation(observedAuthority(ctx),{
890
+ coreRoot:active.release_root
891
+ });
892
+ observeOnlyAuthority(authority);
893
+ const url=startText.match(/^URL\s*:\s*(\S+)/m)?.[1]||'';
894
+ writeRemote(process.stdout,`${remoteTag('auth',displayMode)} Observing existing grants without ARM or DISARM...\n`,displayMode);
884
895
  process.stdout.write('[BOOT] Verifying public signed MCP execution path...\n');
885
- invokeController(ctx,'machinetest',{capture:true,echoCaptured:false,timeoutMs:REMOTE_START_TIMEOUT_MS,probeMode:mode==='default'?'revoked':mode});
896
+ // Signed machine test is safe only for already-disarmed authority vectors.
897
+ // Do not test against a made-up all-disarmed expectation while owner
898
+ // grants are live; never revoke them as a way to make that test pass.
899
+ if(AUTHORITY_PLANES.every(plane=>!authority.authority[plane].armed))
900
+ invokeController(ctx,'machinetest',{capture:true,echoCaptured:false,timeoutMs:REMOTE_START_TIMEOUT_MS,probeMode:'revoked'});
901
+ else process.stdout.write('[VERIFY] pre-existing owner grants preserved; revoked-plane probe omitted\n');
886
902
  ready=true;
887
903
  writeRemote(process.stdout,`${remoteTag('auth',displayMode)} ${authorityBanner(authority)}\n`,displayMode);
888
904
  process.stdout.write('[OK] ONLINE\n');
@@ -906,8 +922,8 @@ async function runRemoteSession({resumeCursor=null,verbose=false,trace=false,fil
906
922
  process.removeListener('SIGINT',abort);process.removeListener('SIGTERM',abort);
907
923
  if(watchStdin){process.stdin.removeListener('end',abort);process.stdin.removeListener('close',abort);process.stdin.pause();}
908
924
  process.stdout.write(`${formatRemoteLifecycle('cleanup',{displayMode})}\n`);
909
- if(ready||!wasAborted) process.stdout.write('[INFO] Disconnecting; revoking authority...\n');
910
- try{await cleanupRemoteSession(ctx,{displayMode});}
925
+ if(ready||!wasAborted) process.stdout.write('[INFO] Disconnecting; preserving owner standing grants...\n');
926
+ try{await cleanupRemoteSession(ctx,{displayMode,marker});}
911
927
  catch(cleanupError){
912
928
  if(sessionError) throw new AggregateError([sessionError,cleanupError],
913
929
  `remote activation failed: ${String(sessionError?.message||sessionError)}; cleanup failed, ownership marker retained: ${String(cleanupError?.message||cleanupError)}`);
@@ -921,8 +937,55 @@ async function runRemoteSession({resumeCursor=null,verbose=false,trace=false,fil
921
937
  }
922
938
  }
923
939
  }
940
+ async function readR190Evidence(file){
941
+ try{
942
+ const info=await lstat(file);
943
+ if(!info.isFile()||info.isSymbolicLink()||info.size<1||info.size>64*1024)
944
+ throw new Error(`R190 recovery evidence is unsafe: ${path.basename(file)}`);
945
+ return JSON.parse((await readFile(file,'utf8')).replace(/^\uFEFF/,''));
946
+ }catch(error){if(error?.code==='ENOENT')return null;throw error;}
947
+ }
948
+ async function runRemoteForegroundRecovery(expectedMarkerSha256){
949
+ const ctx=await remoteContext();
950
+ const thisFile=fileURLToPath(import.meta.url);
951
+ const protectedFiles=[process.execPath,thisFile,path.join(CURRENT_PACKAGE_ROOT,'MANIFEST.SHA256'),
952
+ path.join(CURRENT_PACKAGE_ROOT,'src','foreground-owner-recovery-r190.mjs'),
953
+ path.join(CURRENT_PACKAGE_ROOT,'src','foreground-marker-cas-win32.mjs'),
954
+ path.join(CURRENT_PACKAGE_ROOT,'src','foreground-process-identity.mjs'),
955
+ path.join(CURRENT_PACKAGE_ROOT,'src','foreground-session-lock.mjs'),
956
+ path.join(CURRENT_PACKAGE_ROOT,'src','privileged-install-trust-r89.mjs'),
957
+ path.join(CURRENT_PACKAGE_ROOT,'src','canonical-json.mjs'),
958
+ path.join(CURRENT_PACKAGE_ROOT,'bin','deadbyte-epoch-lock-r85.node')];
959
+ let installation;
960
+ try{installation=await inspectTrustedInstallation(protectedFiles);}
961
+ catch(error){throw new Error(`R190_PROTECTED_RECOVERY_REQUIRED: ${boundedSingleLine(error?.message||error,512)}`);}
962
+ if(!installation.trusted)throw new Error('R190_PROTECTED_RECOVERY_REQUIRED: package ACL is not R89-trusted');
963
+ await verifyPackageDecisionManifest({root:CURRENT_PACKAGE_ROOT,expectedVersion:DEADBYTE_VERSION});
964
+ const evidenceDir=path.join(ctx.root,'evidence','foreground-recovery-r190');
965
+ const intentPath=path.join(evidenceDir,`${expectedMarkerSha256}.intent.json`);
966
+ const terminalPath=path.join(evidenceDir,`${expectedMarkerSha256}.terminal.json`);
967
+ const startupLock=await acquireForegroundSessionLock({root:ctx.root,powershell:ctx.powershell});
968
+ try{
969
+ startupLock.assertHeld();
970
+ const result=await recoverLegacyForegroundOwnership({
971
+ expectedMarkerSha256,
972
+ readMarkerSnapshot:()=>readRemoteMarkerSnapshot(remoteSessionMarker(ctx.root)),
973
+ deleteMarkerIfHash:expected=>deletePinnedRemoteMarker(remoteSessionMarker(ctx.root),expected),
974
+ readActivePointer:()=>readActiveRuntimeBinding(ctx.root),
975
+ readAuthority:()=>observedAuthority(ctx),
976
+ queryIdentity:identity=>queryForegroundIdentity(identity.pid),
977
+ loadIntent:()=>readR190Evidence(intentPath),loadTerminal:()=>readR190Evidence(terminalPath),
978
+ publishIntent:value=>publishExclusiveJson(intentPath,value),
979
+ publishTerminal:value=>publishExclusiveJson(terminalPath,value)
980
+ });
981
+ startupLock.assertHeld();
982
+ process.stdout.write(`${JSON.stringify({...result,intent_path:intentPath,terminal_path:terminalPath,
983
+ authority_mutated:false,runtime_mutated:false})}\n`);
984
+ }finally{await startupLock.release();}
985
+ }
924
986
  async function runRemote(args){
925
987
  let verbose=false,trace=false,filterExpression='',resumeCursor=null,mode='default';
988
+ let recoveryMarkerSha256=null;
926
989
  const hostCommandPinValues=[];
927
990
  const positional=[];
928
991
  for(let index=0;index<args.length;index++){
@@ -947,12 +1010,23 @@ async function runRemote(args){
947
1010
  if(index+1>=args.length) throw new Error('remote --resume requires a cursor');
948
1011
  resumeCursor=args[++index];continue;
949
1012
  }
1013
+ if(arg==='--confirm-marker-sha256'){
1014
+ if(recoveryMarkerSha256!==null) throw new Error('remote --confirm-marker-sha256 may be specified once');
1015
+ recoveryMarkerSha256=String(args[++index]??'');continue;
1016
+ }
950
1017
  positional.push(arg);
951
1018
  }
952
1019
  let hostCommandPins;
953
1020
  try{hostCommandPins=normalizeHostCommandPins(hostCommandPinValues);}
954
1021
  catch(error){throw new Error(`remote --host-command-pin ${error.message}`);}
955
1022
  const presentation=verbose||trace||Boolean(filterExpression)||resumeCursor!==null||mode!=='default'||hostCommandPins.length>0;
1023
+ if(positional.length===1&&positional[0]==='reconcileforeground'){
1024
+ if(presentation)throw new Error('remote reconcileforeground does not accept presentation or authority flags');
1025
+ if(!/^[0-9a-f]{64}$/.test(String(recoveryMarkerSha256??'')))
1026
+ throw new Error('remote reconcileforeground requires --confirm-marker-sha256 with the exact current marker hash');
1027
+ await runRemoteForegroundRecovery(recoveryMarkerSha256);return;
1028
+ }
1029
+ if(recoveryMarkerSha256!==null)throw new Error('remote --confirm-marker-sha256 is valid only with reconcileforeground');
956
1030
  if(positional.length===0){await runRemoteSession({resumeCursor,verbose,trace,filterExpression,mode,hostCommandPins});return;}
957
1031
  if(presentation) throw new Error('remote presentation flags are valid only for foreground session');
958
1032
  if(positional.length>1) throw new Error('remote accepts one controller command');
@@ -0,0 +1,117 @@
1
+ // Explicit, marker-only recovery for pre-R175 foreground ownership records.
2
+ // It never starts/stops runtime processes and never creates, revokes or restores
3
+ // authority. The caller must provide native CAS deletion and no-clobber evidence.
4
+ import {createHash} from 'node:crypto';
5
+ import path from 'node:path';
6
+ import {canonicalJson} from './canonical-json.mjs';
7
+ import {foregroundIdentityMatches,validForegroundIdentity} from './foreground-process-identity.mjs';
8
+
9
+ const SHA=/^[0-9a-f]{64}$/;
10
+ const PLANES=['process','coding','autonomous','desktop','host'];
11
+ const hash=value=>createHash('sha256').update(Buffer.from(canonicalJson(value))).digest('hex');
12
+ function insist(condition,message){if(!condition)throw new Error(`R190 foreground recovery blocked: ${message}`);}
13
+ function pointerSubject(pointer){
14
+ const fields=['release_id','version','manifest_sha256','generation_manifest_sha256','generation_root'];
15
+ insist(pointer&&typeof pointer==='object','active pointer missing');
16
+ for(const field of fields)insist(typeof pointer[field]==='string'&&pointer[field].length>0,`active pointer ${field} invalid`);
17
+ insist(SHA.test(pointer.manifest_sha256)&&SHA.test(pointer.generation_manifest_sha256),'active pointer digest invalid');
18
+ insist(path.isAbsolute(pointer.generation_root),'active generation root invalid');
19
+ return Object.freeze(Object.fromEntries(fields.map(field=>[field,pointer[field]])));
20
+ }
21
+ function authoritySubject(observation){
22
+ insist(observation?.state==='ONLINE'&&observation.local_ready===true,'runtime is not verified ONLINE');
23
+ insist(observation?.supervisor?.running===true,'supervisor is not verified running');
24
+ const subject={state:'ONLINE',local_ready:true,supervisor_running:true};
25
+ for(const plane of PLANES){
26
+ const value=observation.authority?.[plane];
27
+ insist(value?.configured===true&&typeof value.armed==='boolean'&&typeof value.reason==='string',`${plane} authority unverified`);
28
+ subject[plane]=Object.freeze({configured:true,armed:value.armed,reason:value.reason});
29
+ }
30
+ return Object.freeze(subject);
31
+ }
32
+ function staleIdentity(identity,queryIdentity,label){
33
+ insist(validForegroundIdentity(identity),`${label} identity invalid`);
34
+ let actual;
35
+ try{actual=queryIdentity(identity);}catch{throw new Error(`R190 foreground recovery blocked: ${label} identity observation unavailable`);}
36
+ if(actual===null)return Object.freeze({pid:identity.pid,state:'absent'});
37
+ insist(validForegroundIdentity(actual),`${label} current identity invalid`);
38
+ insist(!foregroundIdentityMatches(identity,actual),`${label} process is still active`);
39
+ return Object.freeze({pid:identity.pid,state:'pid_reused',actual_creation_utc:actual.creation_utc,
40
+ actual_executable_path:actual.executable_path});
41
+ }
42
+ function terminalFor(intent,status,completedAt){
43
+ const body={schema:'deadbyte.r190-foreground-recovery-terminal.v1',status,
44
+ intent_sha256:intent.intent_sha256,marker_sha256:intent.marker_sha256,
45
+ pointer:intent.pointer,authority:intent.authority,completed_at_utc:completedAt};
46
+ return Object.freeze({...body,receipt_sha256:hash(body)});
47
+ }
48
+ function validateIntent(intent,expected,pointer,authority){
49
+ insist(intent?.schema==='deadbyte.r190-foreground-recovery-intent.v1','recovery intent schema invalid');
50
+ insist(intent.marker_sha256===expected,'recovery intent marker mismatch');
51
+ const body={...intent};delete body.intent_sha256;
52
+ insist(SHA.test(String(intent.intent_sha256))&&hash(body)===intent.intent_sha256,'recovery intent digest invalid');
53
+ insist(canonicalJson(intent.pointer)===canonicalJson(pointer),'active pointer drift after marker delete');
54
+ insist(canonicalJson(intent.authority)===canonicalJson(authority),'authority drift after marker delete');
55
+ }
56
+
57
+ export async function recoverLegacyForegroundOwnership({
58
+ expectedMarkerSha256,readMarkerSnapshot,deleteMarkerIfHash,readActivePointer,readAuthority,
59
+ queryIdentity,loadIntent,loadTerminal,publishIntent,publishTerminal,now=()=>new Date()
60
+ }={}){
61
+ insist(typeof expectedMarkerSha256==='string'&&SHA.test(expectedMarkerSha256),'confirmation SHA-256 invalid');
62
+ for(const [name,fn] of Object.entries({readMarkerSnapshot,deleteMarkerIfHash,readActivePointer,
63
+ readAuthority,queryIdentity,loadIntent,loadTerminal,publishIntent,publishTerminal}))
64
+ insist(typeof fn==='function',`${name} callback missing`);
65
+ const pointer=pointerSubject(await readActivePointer());
66
+ const authority=authoritySubject(await readAuthority());
67
+ const snapshot=await readMarkerSnapshot();
68
+ const priorIntent=await loadIntent();
69
+ const priorTerminal=await loadTerminal();
70
+ if(snapshot===null){
71
+ insist(priorIntent,'marker absent without a durable R190 intent');
72
+ validateIntent(priorIntent,expectedMarkerSha256,pointer,authority);
73
+ if(priorTerminal){
74
+ insist(priorTerminal.intent_sha256===priorIntent.intent_sha256&&priorTerminal.marker_sha256===expectedMarkerSha256,
75
+ 'terminal identity mismatch');
76
+ return Object.freeze({status:'MARKER_ALREADY_RECONCILED',marker_sha256:expectedMarkerSha256,
77
+ intent_sha256:priorIntent.intent_sha256,receipt_sha256:priorTerminal.receipt_sha256});
78
+ }
79
+ const terminal=terminalFor(priorIntent,'MARKER_RECONCILED_AFTER_DELETE',now().toISOString());
80
+ await publishTerminal(terminal);
81
+ return Object.freeze({status:terminal.status,marker_sha256:expectedMarkerSha256,
82
+ intent_sha256:priorIntent.intent_sha256,receipt_sha256:terminal.receipt_sha256});
83
+ }
84
+ insist(snapshot?.sha256===expectedMarkerSha256,'marker hash differs from explicit confirmation');
85
+ const marker=snapshot.marker;
86
+ insist(marker?.schema==='deadbyte.remote-session.v2','marker schema is not recoverable');
87
+ insist(marker.authority_scope===undefined||marker.authority_scope===null,'scoped marker must use its normal lifecycle');
88
+ insist(Number.isInteger(marker.owner_pid)&&marker.owner_pid>0&&marker.owner_identity,'owner marker identity invalid');
89
+ insist(Number.isInteger(marker.guardian_pid)&&marker.guardian_pid>0&&marker.guardian_identity,'guardian marker identity invalid');
90
+ const owner=staleIdentity(marker.owner_identity,queryIdentity,'owner');
91
+ const guardian=staleIdentity(marker.guardian_identity,queryIdentity,'guardian');
92
+ const intentBody={schema:'deadbyte.r190-foreground-recovery-intent.v1',action:'reconcile_legacy_foreground_marker_only',
93
+ marker_sha256:expectedMarkerSha256,pointer,authority,owner,guardian,requested_at_utc:now().toISOString(),
94
+ authority_mutation_allowed:false,runtime_mutation_allowed:false};
95
+ const intent=Object.freeze({...intentBody,intent_sha256:hash(intentBody)});
96
+ if(priorIntent)validateIntent(priorIntent,expectedMarkerSha256,pointer,authority);
97
+ else await publishIntent(intent);
98
+ const pinnedIntent=priorIntent??intent;
99
+ const pointerBeforeDelete=pointerSubject(await readActivePointer());
100
+ const authorityBeforeDelete=authoritySubject(await readAuthority());
101
+ insist(canonicalJson(pointerBeforeDelete)===canonicalJson(pointer),'active pointer drift before CAS delete');
102
+ insist(canonicalJson(authorityBeforeDelete)===canonicalJson(authority),'authority drift before CAS delete');
103
+ const current=await readMarkerSnapshot();
104
+ insist(current?.sha256===expectedMarkerSha256,'marker changed before CAS delete');
105
+ staleIdentity(marker.owner_identity,queryIdentity,'owner');
106
+ staleIdentity(marker.guardian_identity,queryIdentity,'guardian');
107
+ insist(await deleteMarkerIfHash(expectedMarkerSha256)==='REMOVED','native marker CAS did not remove exact identity');
108
+ insist(await readMarkerSnapshot()===null,'marker remained or reappeared after CAS delete');
109
+ const finalPointer=pointerSubject(await readActivePointer());
110
+ const finalAuthority=authoritySubject(await readAuthority());
111
+ insist(canonicalJson(finalPointer)===canonicalJson(pointer),'active pointer drift after CAS delete');
112
+ insist(canonicalJson(finalAuthority)===canonicalJson(authority),'authority drift after CAS delete');
113
+ const terminal=terminalFor(pinnedIntent,'MARKER_RECONCILED',now().toISOString());
114
+ await publishTerminal(terminal);
115
+ return Object.freeze({status:terminal.status,marker_sha256:expectedMarkerSha256,
116
+ intent_sha256:pinnedIntent.intent_sha256,receipt_sha256:terminal.receipt_sha256});
117
+ }
@@ -0,0 +1,176 @@
1
+ // R174 pure WebAuthn owner assertion verifier.
2
+ //
3
+ // This module does not enroll a credential, solicit owner consent, consume a
4
+ // challenge, write an authority epoch, or grant autonomous access. Those are
5
+ // separate protected-host boundaries and MUST NOT be supplied by an agent.
6
+ import {createHash,createPublicKey,verify,timingSafeEqual} from 'node:crypto';
7
+ import {lstat,realpath,open} from 'node:fs/promises';
8
+ import path from 'node:path';
9
+ import {canonicalJson} from './canonical-json.mjs';
10
+
11
+ const HEX64=/^[0-9a-f]{64}$/;
12
+ const B64URL=/^[A-Za-z0-9_-]+$/;
13
+ const sha256=x=>createHash('sha256').update(x).digest();
14
+ const canonicalHash=x=>sha256(Buffer.from(canonicalJson(x),'utf8')).toString('hex');
15
+ function requireRecord(x,label){
16
+ if(!x||typeof x!=='object'||Array.isArray(x))throw new Error('R174 '+label+' invalid');
17
+ return x;
18
+ }
19
+ function keys(x,names,label){
20
+ requireRecord(x,label);
21
+ if(Object.keys(x).length!==names.length||names.some(n=>!Object.hasOwn(x,n)))
22
+ throw new Error('R174 '+label+' fields invalid');
23
+ }
24
+ function base64url(value,label,minimum=1,maximum=16384){
25
+ if(typeof value!=='string'||value.length>maximum*2||!B64URL.test(value))
26
+ throw new Error('R174 '+label+' encoding invalid');
27
+ const decoded=Buffer.from(value,'base64url');
28
+ if(decoded.length<minimum||decoded.length>maximum||decoded.toString('base64url')!==value)
29
+ throw new Error('R174 '+label+' encoding invalid');
30
+ return decoded;
31
+ }
32
+ function matches(a,b){return a.length===b.length&&timingSafeEqual(a,b);}
33
+ function instant(value){
34
+ if(typeof value!=='string'||!/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d\.\d{3}Z$/.test(value))
35
+ throw new Error('R174 transaction UTC invalid');
36
+ const timestamp=Date.parse(value);
37
+ if(!Number.isFinite(timestamp)||new Date(timestamp).toISOString()!==value)
38
+ throw new Error('R174 transaction UTC invalid');
39
+ return timestamp;
40
+ }
41
+ function validateSubject(s){
42
+ keys(s,['schema','release_manifest_sha256','coding_policy_sha256',
43
+ 'autonomous_policy_sha256','root_id','project_root','effect','max_seconds'],'subject');
44
+ if(s.schema!=='deadbyte.r174-autonomous-owner-subject.v1'||
45
+ [s.release_manifest_sha256,s.coding_policy_sha256,s.autonomous_policy_sha256]
46
+ .some(x=>typeof x!=='string'||!HEX64.test(x))||
47
+ s.root_id!=='core'||typeof s.project_root!=='string'||
48
+ !path.isAbsolute(s.project_root)||s.effect!=='autonomous_goal_run'||
49
+ !Number.isSafeInteger(s.max_seconds)||s.max_seconds<1||s.max_seconds>3600)
50
+ throw new Error('R174 subject scope invalid');
51
+ }
52
+ export async function verifyR174OwnerAssertion({credential,subject,transaction,assertion,now}={}){
53
+ validateSubject(subject);
54
+ keys(transaction,['schema','challenge_b64url','subject_sha256','issued_at_utc','expires_at_utc'],'challenge');
55
+ if(transaction.schema!=='deadbyte.r174-owner-challenge.v1'||!HEX64.test(transaction.subject_sha256))
56
+ throw new Error('R174 challenge schema invalid');
57
+ const challenge=base64url(transaction.challenge_b64url,'challenge',32,32);
58
+ if(canonicalHash(subject)!==transaction.subject_sha256)
59
+ throw new Error('R174 subject hash mismatch');
60
+ const issued=instant(transaction.issued_at_utc),expires=instant(transaction.expires_at_utc);
61
+ if(expires<=issued||expires-issued>300000)
62
+ throw new Error('R174 challenge lifetime exceeds five minutes');
63
+ if(!(now instanceof Date)||!Number.isFinite(now.getTime())||
64
+ now.getTime()<issued||now.getTime()>=expires)
65
+ throw new Error('R174 challenge expired or not yet valid');
66
+ keys(credential,['schema','credential_id_b64url','rp_id','origin',
67
+ 'public_key_spki_pem','algorithm','sign_count'],'credential');
68
+ if(credential.schema!=='deadbyte.r174-owner-credential.v1'||credential.algorithm!=='ES256'||
69
+ typeof credential.rp_id!=='string'||!credential.rp_id||typeof credential.origin!=='string'||
70
+ !Number.isSafeInteger(credential.sign_count)||credential.sign_count<0)
71
+ throw new Error('R174 credential identity invalid');
72
+ if(credential.origin!=='https://'+credential.rp_id)
73
+ throw new Error('R174 RP ID and origin binding mismatch');
74
+ const credentialId=base64url(credential.credential_id_b64url,'credential id',16,1024);
75
+ keys(assertion,['credential_id_b64url','client_data_json_b64url',
76
+ 'authenticator_data_b64url','signature_b64url'],'assertion');
77
+ if(!matches(credentialId,base64url(assertion.credential_id_b64url,'credential id',16,1024)))
78
+ throw new Error('R174 credential id mismatch');
79
+ const rawClient=base64url(assertion.client_data_json_b64url,'client data',16,16384);
80
+ let client;
81
+ try{client=JSON.parse(new TextDecoder('utf-8',{fatal:true}).decode(rawClient));}
82
+ catch{throw new Error('R174 client data JSON invalid');}
83
+ if(!client||typeof client!=='object'||Array.isArray(client)||client.type!=='webauthn.get')
84
+ throw new Error('R174 client data type invalid');
85
+ if(client.challenge!==transaction.challenge_b64url||
86
+ !matches(base64url(client.challenge,'challenge',32,32),challenge))
87
+ throw new Error('R174 client challenge mismatch');
88
+ if(client.origin!==credential.origin)throw new Error('R174 client origin mismatch');
89
+ if(client.crossOrigin===true||client.topOrigin!==undefined)
90
+ throw new Error('R174 cross-origin assertion denied');
91
+ const authenticator=base64url(assertion.authenticator_data_b64url,'authenticator data',37,16384);
92
+ const rpIdHash=sha256(Buffer.from(credential.rp_id,'utf8'));
93
+ if(!matches(authenticator.subarray(0,32),rpIdHash))
94
+ throw new Error('R174 RP ID hash mismatch');
95
+ const flags=authenticator[32];
96
+ if((flags&0x01)!==0x01)throw new Error('R174 user presence required');
97
+ if((flags&0x04)!==0x04)throw new Error('R174 user verification required');
98
+ const signCount=authenticator.readUInt32BE(33);
99
+ // Zero counters are common with platform authenticators. Replay resistance
100
+ // for such devices MUST be enforced by a one-time protected challenge journal.
101
+ if(credential.sign_count>0&&signCount<=credential.sign_count)
102
+ throw new Error('R174 sign counter rollback');
103
+ let key;
104
+ try{key=createPublicKey(credential.public_key_spki_pem);}
105
+ catch{throw new Error('R174 owner key invalid');}
106
+ if(key.asymmetricKeyType!=='ec'||key.asymmetricKeyDetails?.namedCurve!=='prime256v1')
107
+ throw new Error('R174 owner key must be ES256 P-256');
108
+ const signed=Buffer.concat([authenticator,sha256(rawClient)]);
109
+ const signature=base64url(assertion.signature_b64url,'signature',8,256);
110
+ if(!verify('sha256',signed,key,signature))
111
+ throw new Error('R174 signature verification failed');
112
+ return Object.freeze({status:'verified',subject_sha256:transaction.subject_sha256,
113
+ challenge_b64url:transaction.challenge_b64url,credential_id_b64url:credential.credential_id_b64url,
114
+ sign_count:signCount,user_verified:true,
115
+ // NOT an authority grant. Protected challenge consumption still mandatory.
116
+ authority_granted:false});
117
+ }
118
+
119
+ // The protected-host use-once boundary is intentionally separate from signature verification.
120
+ export async function acceptR174OwnerProof({ledgerRoot,ownerTrust,...inputs}={}){
121
+ // A trusted caller MUST load ownerTrust from an independently pinned
122
+ // owner-controlled trust store, not copy it from request JSON. This
123
+ // primitive deliberately does not authenticate its caller or issue grants.
124
+ keys(ownerTrust,['schema','credential_id_b64url','public_key_spki_sha256',
125
+ 'rp_id','origin','subject_sha256'],'owner trust');
126
+ if(ownerTrust.schema!=='deadbyte.r174-owner-trust-binding.v1'||
127
+ typeof ownerTrust.public_key_spki_sha256!=='string'||
128
+ !HEX64.test(ownerTrust.public_key_spki_sha256)||
129
+ typeof ownerTrust.subject_sha256!=='string'||
130
+ !HEX64.test(ownerTrust.subject_sha256))
131
+ throw new Error('R174 owner trust metadata invalid');
132
+ const {credential,transaction,subject}=inputs;
133
+ const credentialKey=createPublicKey(credential?.public_key_spki_pem??'');
134
+ const keySha=sha256(credentialKey.export({type:'spki',format:'der'})).toString('hex');
135
+ if(keySha!==ownerTrust.public_key_spki_sha256 ||
136
+ ownerTrust.credential_id_b64url!==credential?.credential_id_b64url ||
137
+ ownerTrust.rp_id!==credential?.rp_id ||
138
+ ownerTrust.origin!==credential?.origin ||
139
+ ownerTrust.subject_sha256!==transaction?.subject_sha256 ||
140
+ ownerTrust.subject_sha256!==canonicalHash(subject))
141
+ throw new Error('R174 owner trust credential or reviewed subject binding mismatch');
142
+ // Then check signed owner assertion. Invalid assertions must never
143
+ // consume a challenge or produce a receipt.
144
+ const proof=await verifyR174OwnerAssertion(inputs);
145
+ if(typeof ledgerRoot!=='string'||!path.isAbsolute(ledgerRoot))
146
+ throw new Error('R174 replay ledger root must be absolute');
147
+ const dir=await lstat(ledgerRoot);
148
+ if(!dir.isDirectory()||dir.isSymbolicLink()||
149
+ path.resolve(await realpath(ledgerRoot)).toLowerCase()!==path.resolve(ledgerRoot).toLowerCase())
150
+ throw new Error('R174 replay ledger must be a real directory');
151
+ // Caller must independently attest protected ACLs and owner-only trust.
152
+ // This primitive does NOT establish those conditions or grant authority.
153
+ // A challenge is globally one-use within this protected journal regardless
154
+ // of owner credential or scoped subject. Identity is checked by the signature
155
+ // verifier; it must not widen the replay key.
156
+ const id=sha256(Buffer.concat([
157
+ Buffer.from('DEADBYTE-R174-OWNER-CHALLENGE-CONSUMPTION-v1','utf8'),
158
+ base64url(proof.challenge_b64url,'challenge',32,32)
159
+ ])).toString('hex');
160
+ const file=path.join(ledgerRoot,'used-'+id+'.json');
161
+ const receipt={schema:'deadbyte.r174-consumed-owner-challenge.v1',
162
+ id,subject_sha256:proof.subject_sha256,credential_id_b64url:proof.credential_id_b64url,
163
+ challenge_b64url:proof.challenge_b64url,sign_count:proof.sign_count,
164
+ authority_granted:false,consumed_at_utc:new Date().toISOString()};
165
+ let handle;
166
+ try{handle=await open(file,'wx',0o600);}
167
+ catch(error){
168
+ if(error?.code==='EEXIST')throw new Error('R174 owner challenge replay denied');
169
+ throw error;
170
+ }
171
+ try{
172
+ await handle.writeFile(JSON.stringify(receipt)+'\n');
173
+ await handle.sync();
174
+ }finally{await handle.close();}
175
+ return Object.freeze({...proof,status:'consumed',id,authority_granted:false});
176
+ }
@@ -0,0 +1,28 @@
1
+ // R175: foreground log sessions never own operator standing grants.
2
+ import {AUTHORITY_PLANES} from './remote-authority.mjs';
3
+ export function observeOnlyAuthority(snapshot){
4
+ const expected={};
5
+ if(snapshot?.state!=='ONLINE')throw new Error('R175 foreground requires ONLINE runtime');
6
+ for(const plane of AUTHORITY_PLANES){
7
+ const observed=snapshot.authority?.[plane];
8
+ if(!observed||typeof observed.armed!=='boolean'||typeof observed.configured!=='boolean')
9
+ throw new Error('R175 foreground authority observation invalid: '+plane);
10
+ expected[plane]=observed.armed;
11
+ }
12
+ return Object.freeze({authority_scope:'observe-only',expected:Object.freeze(expected),
13
+ commands:Object.freeze([])});
14
+ }
15
+ export function assertObserveOnlyMarker(marker){
16
+ return marker?.schema==='deadbyte.remote-session.v2'&&
17
+ marker.authority_scope==='observe-only'&&
18
+ Number.isInteger(marker.owner_pid)&&marker.owner_pid>0&&
19
+ typeof marker.session_nonce==='string'&&/^[0-9a-f]{64}$/.test(marker.session_nonce);
20
+ }
21
+
22
+ export function assertMatchingForegroundRelease(active,{version,manifest_sha256}={}){
23
+ if(!active||typeof version!=='string'||!/^[0-9]+\.[0-9]+\.[0-9]+$/.test(version)||
24
+ typeof manifest_sha256!=='string'||!/^[0-9a-f]{64}$/.test(manifest_sha256)||
25
+ active.version!==version||active.manifest_sha256!==manifest_sha256)
26
+ throw new Error('R175_EXACT_ACTIVE_RELEASE_REQUIRED: observation session will not activate, downgrade or upgrade runtime');
27
+ return active;
28
+ }