deadbyte-mcp 0.15.68 → 0.15.70
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/MANIFEST.SHA256 +85 -38
- package/PROVENANCE.json +52 -24
- package/README.txt +44 -31
- package/RELEASE.json +47 -17
- package/SBOM.cdx.json +2 -2
- package/TEST-MATRIX.json +44 -2
- package/bridge/package.json +1 -1
- package/controller/deadbyte-controller.ps1 +65 -1
- package/controller/deadbyte-process-policy.json +4 -4
- package/docs/R184-package-lock.json +97 -0
- package/docs/R186-OWNER-CONSENT-HANDOFF.txt +87 -0
- package/package.json +1 -1
- package/scripts/install-r186-owner-trust.ps1 +59 -0
- package/scripts/r175-focused-runner.mjs +38 -0
- package/scripts/r176-log-preview.mjs +18 -0
- package/scripts/r177-owner-trust-inventory.ps1 +36 -0
- package/scripts/r178-full-regression.mjs +21 -0
- package/scripts/r178-npm-ci.mjs +10 -0
- package/scripts/r178-npm-preflight.mjs +16 -0
- package/scripts/r178-release-gates-readonly.mjs +9 -0
- package/scripts/r178-shard-regression.mjs +25 -0
- package/scripts/r179-predecessor-registry.mjs +24 -0
- package/scripts/r179-protected-inventory.ps1 +21 -0
- package/scripts/r186-focused-runner.mjs +22 -0
- package/scripts/r186-owner-anchor-inspect.mjs +6 -0
- package/scripts/r186-owner-consent-check.mjs +86 -0
- package/scripts/r186-owner-grant-cli.mjs +45 -0
- package/scripts/r186-owner-key-local.mjs +22 -0
- package/scripts/r186-owner-review-export.mjs +34 -0
- package/src/deadbyte-cli.mjs +130 -56
- package/src/foreground-owner-recovery-r190.mjs +117 -0
- package/src/r174-owner-webauthn.mjs +176 -0
- package/src/r175-foreground-boundary.mjs +28 -0
- package/src/r186-owner-anchor.mjs +12 -0
- package/src/r186-owner-consent-preflight.mjs +63 -0
- package/src/r186-owner-grant-broker.mjs +96 -0
- package/src/r186-owner-local-signing.mjs +25 -0
- package/src/r186-owner-request.mjs +40 -0
- package/src/r186-owner-review-export.mjs +49 -0
- package/src/remote-live-log.mjs +81 -5
- package/src/remote-session-guardian.mjs +23 -0
- package/src/version.mjs +1 -1
- package/test/helpers/r175-active-pointer.mjs +28 -0
- package/test/helpers/release-lock.mjs +12 -0
- package/test/r129-sdk-security-baseline.test.mjs +2 -1
- package/test/r130-release-security-contract.test.mjs +11 -10
- package/test/r132-release-identity.test.mjs +12 -11
- package/test/r153-prepublish-identity.test.mjs +1 -1
- package/test/r155-default-live-log.test.mjs +17 -16
- package/test/r160-package-decision-manifest.test.mjs +1 -1
- package/test/r160-release-contract.test.mjs +4 -5
- package/test/r163-isolated-slot-rollback-restore.test.mjs +8 -8
- package/test/r165-live-cli-skew.test.mjs +6 -6
- package/test/r167-release-contract.test.mjs +15 -13
- package/test/r168-protected-entrypoint.test.mjs +5 -2
- package/test/r168-release-contract.test.mjs +4 -3
- package/test/r168-remote-one-shot-coherence.test.mjs +2 -1
- package/test/r172-release-contract.test.mjs +7 -7
- package/test/r173-release-contract.test.mjs +18 -17
- package/test/r174-owner-webauthn.test.mjs +144 -0
- package/test/r175-cli-flags.test.mjs +23 -0
- package/test/r175-foreground-boundary.test.mjs +87 -0
- package/test/r175-guardian-owner-boundary.test.mjs +56 -0
- package/test/r176-pretty-live-log.test.mjs +54 -0
- package/test/r177-windows-absolute-live-path.test.mjs +37 -0
- package/test/r179-absolute-path-live-log.test.mjs +40 -0
- package/test/r179-release-contract.test.mjs +62 -0
- package/test/r186-controller-owner-grant.test.mjs +20 -0
- package/test/r186-owner-consent-preflight.test.mjs +98 -0
- package/test/r186-owner-enrollment.test.mjs +20 -0
- package/test/r186-owner-grant-broker.test.mjs +85 -0
- package/test/r186-owner-local-signing.test.mjs +30 -0
- package/test/r186-owner-request.test.mjs +28 -0
- package/test/r186-owner-review-export.test.mjs +52 -0
- package/test/r190-foreground-cli.test.mjs +41 -0
- package/test/r190-foreground-owner-recovery.test.mjs +61 -0
- package/test/r190-release-contract.test.mjs +33 -0
- package/test/r48-release-contract.test.mjs +2 -1
- package/test/r51-runtime-coherence.test.mjs +4 -2
- package/test/r58-cleanup-retention.test.mjs +5 -12
- package/test/r58-concurrent-recovery.test.mjs +5 -1
- package/test/r58-stale-owner.test.mjs +9 -3
- package/test/r59-tool-surface-compat.test.mjs +1 -1
- package/test/remote-live-log-r37.test.mjs +2 -2
- package/test/remote-live-log-recovery-r36.test.mjs +1 -1
- package/test/remote-session-cli.test.mjs +92 -126
- package/node_modules/which/CHANGELOG.md +0 -166
package/src/deadbyte-cli.mjs
CHANGED
|
@@ -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 #
|
|
65
|
-
` npx -y ${exact} remote --desktop #
|
|
66
|
-
` npx -y ${exact} remote --host #
|
|
67
|
-
` npx -y ${exact} remote --full #
|
|
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
|
|
404
|
-
: '[LOG] Live tool calls with individual START/DONE, sanitized arguments and results. Transport hidden; signed audit remains on disk. Ctrl+C disconnects
|
|
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
|
-
//
|
|
801
|
-
//
|
|
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
|
-
|
|
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
|
|
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
|
|
842
|
-
|
|
843
|
-
|
|
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
|
-
|
|
867
|
-
|
|
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
|
|
882
|
-
|
|
883
|
-
|
|
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
|
-
|
|
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;
|
|
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
|
+
}
|