deadbyte-mcp 0.15.67 → 0.15.69
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 -40
- package/PROVENANCE.json +46 -24
- package/README.txt +46 -33
- package/RELEASE.json +44 -17
- package/SBOM.cdx.json +2 -2
- package/TEST-MATRIX.json +25 -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 +3 -2
- package/scripts/install-r186-owner-trust.ps1 +59 -0
- package/scripts/r173-protected-preflight.mjs +42 -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/activation-authority-proof-r75.mjs +28 -4
- package/src/activation-preflight-r65.mjs +2 -1
- package/src/deadbyte-cli.mjs +69 -56
- 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 +6 -6
- 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 +14 -11
- 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 +8 -8
- package/test/r173-release-contract.test.mjs +33 -0
- 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/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/r75-authority-evidence.test.mjs +32 -0
- 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
|
@@ -63,7 +63,8 @@ export function classifyActivationPreflight({pointer,expectedParent,candidate,ex
|
|
|
63
63
|
return Object.freeze({state:'AUTHORITY_UNVERIFIED',safe_to_activate:false,reason:`${plane} authority proof missing`,...bound});
|
|
64
64
|
if(proof&&(proof.policy_sha256!==observation?.policy_sha256?.[plane]||
|
|
65
65
|
proof.state!==(authority[plane]?'VERIFIED_ARMED':'VERIFIED_DISARMED')||
|
|
66
|
-
(!authority[plane]&&proof.grant_absent!==true
|
|
66
|
+
(!authority[plane]&&proof.grant_absent!==true&&
|
|
67
|
+
!(entry?.reason==='epoch-revoked'&&proof.transaction_revoked===true))))
|
|
67
68
|
return Object.freeze({state:'AUTHORITY_UNVERIFIED',safe_to_activate:false,reason:`${plane} authority proof unverified`,...bound});
|
|
68
69
|
}
|
|
69
70
|
if(PLANES.some(plane=>!HEX64.test(String(observation?.policy_sha256?.[plane]))))
|
package/src/deadbyte-cli.mjs
CHANGED
|
@@ -27,6 +27,7 @@ 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';
|
|
@@ -61,10 +62,10 @@ function printHelp(){
|
|
|
61
62
|
` npx -y ${exact} doctor # verify current package/release truth`,
|
|
62
63
|
` npx -y ${exact} mcp # stdio MCP server for local clients`,
|
|
63
64
|
` 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 #
|
|
65
|
+
` npx -y ${exact} remote --operator # R175 blocked pending session-owned authority issuance`,
|
|
66
|
+
` npx -y ${exact} remote --desktop # R175 blocked pending session-owned authority issuance`,
|
|
67
|
+
` npx -y ${exact} remote --host # R175 blocked pending session-owned authority issuance`,
|
|
68
|
+
` npx -y ${exact} remote --full # R175 blocked pending session-owned authority issuance`,
|
|
68
69
|
` npx -y ${exact} remote --resume <cursor> # replay missed results, then reconnect`,
|
|
69
70
|
` npx -y ${exact} remote --trace # show trace/span identity without payload detail`,
|
|
70
71
|
|
|
@@ -400,8 +401,8 @@ function timelineFilterEvent(timeline){
|
|
|
400
401
|
}
|
|
401
402
|
async function tailRemoteLogs(root,signal,{journal=null,offsets=null,activityTracker=null,timelineFilter=null,timelineWriter=null,verbose=false,displayMode='unicode'}={}){
|
|
402
403
|
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
|
|
404
|
+
? '[LOG] Verbose live timeline. Full sanitized request detail enabled; Ctrl+C disconnects; owner standing grants remain unchanged.\n'
|
|
405
|
+
: '[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
406
|
const activeOffsets=offsets??await snapshotRemoteLogOffsets(root);
|
|
406
407
|
const partialLines=new Map();
|
|
407
408
|
let liveFiles=[...activeOffsets.keys()];
|
|
@@ -455,7 +456,13 @@ function remoteTag(kind,displayMode){
|
|
|
455
456
|
return (displayMode==='ascii'?ascii:unicode)[kind];
|
|
456
457
|
}
|
|
457
458
|
function writeRemote(stream,text,displayMode){stream.write(remoteDisplayText(text,displayMode));}
|
|
458
|
-
async function cleanupRemoteSession(ctx,{displayMode='unicode'}={}){
|
|
459
|
+
async function cleanupRemoteSession(ctx,{displayMode='unicode',marker=null}={}){
|
|
460
|
+
// R175 observation sessions issue no grants and must never revoke owner grants or park runtime.
|
|
461
|
+
// An invalid R175 marker must fail closed, not fall through to legacy revoke.
|
|
462
|
+
if(marker!==null){
|
|
463
|
+
if(!assertObserveOnlyMarker(marker))throw new Error('R175_INVALID_OBSERVE_ONLY_MARKER: no authority cleanup performed');
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
459
466
|
const failed=[];
|
|
460
467
|
for(const command of SESSION_CLEANUP){
|
|
461
468
|
try{
|
|
@@ -669,6 +676,15 @@ async function recoverRemoteSession(ctx,{displayMode='unicode'}={}){
|
|
|
669
676
|
const markerPath=remoteSessionMarker(ctx.root);
|
|
670
677
|
const snapshot=await readRemoteMarkerSnapshot(markerPath);
|
|
671
678
|
if(!snapshot) return null;
|
|
679
|
+
if(assertObserveOnlyMarker(prior)){
|
|
680
|
+
// Owner and guardian lifetime were verified above. A monitor-only session
|
|
681
|
+
// owns the CAS marker, not any authority epoch or runtime lifecycle.
|
|
682
|
+
if(snapshot.marker?.owner_pid!==prior.owner_pid||snapshot.marker?.session_nonce!==prior.session_nonce)
|
|
683
|
+
throw new Error('R175 observation-only stale marker identity changed; marker retained');
|
|
684
|
+
if(deletePinnedRemoteMarker(markerPath,snapshot.sha256)!=='REMOVED')
|
|
685
|
+
throw new Error('R175 observation-only stale marker CAS failed; marker retained');
|
|
686
|
+
return null;
|
|
687
|
+
}
|
|
672
688
|
const active=await readActiveRuntimeBinding(ctx.root);
|
|
673
689
|
if(!active)throw new Error(`remote session ownership unverified for stale pid=${ownerPid}; active pointer missing; marker retained`);
|
|
674
690
|
const observed=observedAuthority(ctx);
|
|
@@ -740,7 +756,7 @@ async function claimRemoteSession(ctx){
|
|
|
740
756
|
const dir=path.join(ctx.root,'runtime');await mkdir(dir,{recursive:true});
|
|
741
757
|
const nonce=randomBytes(32).toString('hex');
|
|
742
758
|
const resultSessionId=randomBytes(12).toString('hex');
|
|
743
|
-
const marker={schema:'deadbyte.remote-session.v2',owner_pid:process.pid,guardian_pid:0,session_nonce:nonce,
|
|
759
|
+
const marker={schema:'deadbyte.remote-session.v2',authority_scope:'observe-only',owner_pid:process.pid,guardian_pid:0,session_nonce:nonce,
|
|
744
760
|
result_session_id:resultSessionId,started_at_utc:new Date().toISOString()};
|
|
745
761
|
marker.owner_identity=queryForegroundIdentity(process.pid);
|
|
746
762
|
if(!marker.owner_identity)throw new Error('remote foreground identity unavailable');
|
|
@@ -749,7 +765,7 @@ async function claimRemoteSession(ctx){
|
|
|
749
765
|
catch(error){if(error?.code==='EEXIST') throw new Error('remote session already claimed');throw error;}
|
|
750
766
|
let guardian=null;
|
|
751
767
|
try{
|
|
752
|
-
guardian=spawn(process.execPath,[GUARDIAN_SCRIPT,'--state-root',ctx.root,'--owner-pid',String(process.pid),'--session-nonce',nonce,'--owner-channel','ipc'],{
|
|
768
|
+
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
769
|
detached:true,stdio:['ignore','ignore','ignore','ipc'],windowsHide:true,shell:false,env:process.env
|
|
754
770
|
});
|
|
755
771
|
if(!Number.isInteger(guardian.pid)||guardian.pid<1) throw new Error('remote session guardian did not start');
|
|
@@ -786,6 +802,10 @@ async function releaseRemoteSession(ctx,marker,{displayMode='unicode'}={}){
|
|
|
786
802
|
throw new Error('remote session ownership marker replaced before native deletion; marker retained');
|
|
787
803
|
}
|
|
788
804
|
async function runRemoteSession({resumeCursor=null,verbose=false,trace=false,filterExpression='',mode='default',hostCommandPins=[]}={}){
|
|
805
|
+
// R175: elevated session flags need a distinct signed session-owned lease
|
|
806
|
+
// issuer before they can safely coexist with owner standing grants.
|
|
807
|
+
if(mode!=='default') throw new Error('R175_FOREGROUND_OWNER_BOUNDARY: elevated foreground modes require session-owned grants; use explicit remote arm* commands');
|
|
808
|
+
if(hostCommandPins.length) throw new Error('R175_FOREGROUND_OBSERVE_ONLY: host command repins require an explicit owner action outside foreground logging');
|
|
789
809
|
const display=prepareRemoteConsole();
|
|
790
810
|
const displayMode=display.mode;
|
|
791
811
|
const ctx=await remoteContext();
|
|
@@ -793,12 +813,15 @@ async function runRemoteSession({resumeCursor=null,verbose=false,trace=false,fil
|
|
|
793
813
|
// an EPOCH_NATIVE production runtime. This guard precedes session recovery,
|
|
794
814
|
// marker claims, revocations and any controller startup.
|
|
795
815
|
const active=await readActiveRuntimeBinding(ctx.root);
|
|
816
|
+
const currentManifestSha256=createHash('sha256')
|
|
817
|
+
.update(await readFile(path.join(CURRENT_PACKAGE_ROOT,'MANIFEST.SHA256'))).digest('hex');
|
|
818
|
+
assertMatchingForegroundRelease(active,{version:DEADBYTE_VERSION,manifest_sha256:currentManifestSha256});
|
|
796
819
|
if(active){
|
|
797
820
|
const decision=selectR168OneShotRemotePath({command:'start',active,cliVersion:DEADBYTE_VERSION});
|
|
798
821
|
if(decision.mode==='blocked_version_skew') throw new Error(decision.message);
|
|
799
822
|
}
|
|
800
|
-
//
|
|
801
|
-
//
|
|
823
|
+
// R175 foreground is monitor-only: deployment and upgrades need separate owner action.
|
|
824
|
+
// Never synchronize runtime or mint/revoke a grant from this path.
|
|
802
825
|
if(displayMode==='ascii') writeRemote(process.stdout,`[DISPLAY] ASCII fallback: ${display.reason}\n`,displayMode);
|
|
803
826
|
if(resumeCursor!==null) await replayRemoteResults(ctx.root,resumeCursor,{displayMode});
|
|
804
827
|
const startupLock=await acquireForegroundSessionLock({root:ctx.root,powershell:ctx.powershell});
|
|
@@ -807,9 +830,21 @@ async function runRemoteSession({resumeCursor=null,verbose=false,trace=false,fil
|
|
|
807
830
|
startupLock.assertHeld();
|
|
808
831
|
await verifyPackageDecisionManifest({root:CURRENT_PACKAGE_ROOT,expectedVersion:DEADBYTE_VERSION});
|
|
809
832
|
startupLock.assertHeld();
|
|
810
|
-
|
|
833
|
+
// R179: an observation session may recover ONLY its own observation marker.
|
|
834
|
+
// Legacy migration/revocation is a separately authorized controller action.
|
|
835
|
+
const staleObserver=await readRemoteSessionMarker(ctx.root);
|
|
836
|
+
if(staleObserver){
|
|
837
|
+
if(!assertObserveOnlyMarker(staleObserver))
|
|
838
|
+
throw new Error('R179_LEGACY_FOREGROUND_RECOVERY_REQUIRES_EXPLICIT_OWNER_COMMAND: no authority touched');
|
|
839
|
+
await recoverRemoteSession(ctx,{displayMode});
|
|
840
|
+
}
|
|
811
841
|
startupLock.assertHeld();
|
|
812
|
-
await
|
|
842
|
+
const pinned=await readActiveRuntimeBinding(ctx.root);
|
|
843
|
+
assertMatchingForegroundRelease(pinned,{version:DEADBYTE_VERSION,manifest_sha256:currentManifestSha256});
|
|
844
|
+
for(const field of ['release_id','generation_manifest_sha256','generation_root']){
|
|
845
|
+
if(pinned[field]!==active[field])
|
|
846
|
+
throw new Error('R175_FOREGROUND_ACTIVE_DRIFT: no runtime update or grant mutation attempted');
|
|
847
|
+
}
|
|
813
848
|
startupLock.assertHeld();
|
|
814
849
|
marker=await claimRemoteSession(ctx);
|
|
815
850
|
startupLock.assertHeld();
|
|
@@ -838,51 +873,29 @@ async function runRemoteSession({resumeCursor=null,verbose=false,trace=false,fil
|
|
|
838
873
|
process.stdout.write('[BOOT] Starting local + remote runtime...\n');
|
|
839
874
|
const preflight=invokeController(ctx,'status',{capture:true,allowFailure:true,echoCaptured:false,timeoutMs:REMOTE_STATUS_TIMEOUT_MS});
|
|
840
875
|
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
|
-
}
|
|
876
|
+
if(preflight.status!==0||!controllerRuntimeReady(preflight.stdout))
|
|
877
|
+
throw new Error('R175_FOREGROUND_RUNTIME_NOT_READY: monitoring cannot start or restart a runtime; use explicit remote start');
|
|
878
|
+
process.stdout.write('[BOOT] Runtime already ONLINE; reusing verified bridge/cloud/supervisor.\n');
|
|
864
879
|
process.stdout.write(`${formatRemoteLifecycle('bridge_connected',{displayMode})}\n`);
|
|
865
880
|
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);
|
|
881
|
+
// Observation is never a grant transition or an expectation that live
|
|
882
|
+
// owner grants remain frozen for the lifetime of this log session.
|
|
880
883
|
const current=await readActiveRuntimeBinding(ctx.root);
|
|
881
|
-
const
|
|
882
|
-
|
|
883
|
-
|
|
884
|
+
for(const field of ['release_id','manifest_sha256','generation_manifest_sha256','generation_root'])
|
|
885
|
+
if(current?.[field]!==active[field])throw new Error('R175_FOREGROUND_ACTIVE_DRIFT: observed release changed');
|
|
886
|
+
const authority=assertRuntimeReadyObservation(observedAuthority(ctx),{
|
|
887
|
+
coreRoot:active.release_root
|
|
888
|
+
});
|
|
889
|
+
observeOnlyAuthority(authority);
|
|
890
|
+
const url=startText.match(/^URL\s*:\s*(\S+)/m)?.[1]||'';
|
|
891
|
+
writeRemote(process.stdout,`${remoteTag('auth',displayMode)} Observing existing grants without ARM or DISARM...\n`,displayMode);
|
|
884
892
|
process.stdout.write('[BOOT] Verifying public signed MCP execution path...\n');
|
|
885
|
-
|
|
893
|
+
// Signed machine test is safe only for already-disarmed authority vectors.
|
|
894
|
+
// Do not test against a made-up all-disarmed expectation while owner
|
|
895
|
+
// grants are live; never revoke them as a way to make that test pass.
|
|
896
|
+
if(AUTHORITY_PLANES.every(plane=>!authority.authority[plane].armed))
|
|
897
|
+
invokeController(ctx,'machinetest',{capture:true,echoCaptured:false,timeoutMs:REMOTE_START_TIMEOUT_MS,probeMode:'revoked'});
|
|
898
|
+
else process.stdout.write('[VERIFY] pre-existing owner grants preserved; revoked-plane probe omitted\n');
|
|
886
899
|
ready=true;
|
|
887
900
|
writeRemote(process.stdout,`${remoteTag('auth',displayMode)} ${authorityBanner(authority)}\n`,displayMode);
|
|
888
901
|
process.stdout.write('[OK] ONLINE\n');
|
|
@@ -906,8 +919,8 @@ async function runRemoteSession({resumeCursor=null,verbose=false,trace=false,fil
|
|
|
906
919
|
process.removeListener('SIGINT',abort);process.removeListener('SIGTERM',abort);
|
|
907
920
|
if(watchStdin){process.stdin.removeListener('end',abort);process.stdin.removeListener('close',abort);process.stdin.pause();}
|
|
908
921
|
process.stdout.write(`${formatRemoteLifecycle('cleanup',{displayMode})}\n`);
|
|
909
|
-
if(ready||!wasAborted) process.stdout.write('[INFO] Disconnecting;
|
|
910
|
-
try{await cleanupRemoteSession(ctx,{displayMode});}
|
|
922
|
+
if(ready||!wasAborted) process.stdout.write('[INFO] Disconnecting; preserving owner standing grants...\n');
|
|
923
|
+
try{await cleanupRemoteSession(ctx,{displayMode,marker});}
|
|
911
924
|
catch(cleanupError){
|
|
912
925
|
if(sessionError) throw new AggregateError([sessionError,cleanupError],
|
|
913
926
|
`remote activation failed: ${String(sessionError?.message||sessionError)}; cleanup failed, ownership marker retained: ${String(cleanupError?.message||cleanupError)}`);
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import {createHash,createPublicKey} from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
export function createR186OwnerAnchor(publicKeyPem){
|
|
4
|
+
if(typeof publicKeyPem!=='string'||publicKeyPem.length<32||publicKeyPem.length>8192||/PRIVATE KEY/.test(publicKeyPem))
|
|
5
|
+
throw new Error('R186 owner enrollment requires a public PEM only');
|
|
6
|
+
let key;try{key=createPublicKey(publicKeyPem);}catch{throw new Error('R186 owner public key invalid');}
|
|
7
|
+
if(key.asymmetricKeyType!=='ed25519')throw new Error('R186 owner public key must be Ed25519');
|
|
8
|
+
const canonicalPem=key.export({type:'spki',format:'pem'}).toString();
|
|
9
|
+
const owner_key_id=createHash('sha256').update(key.export({type:'spki',format:'der'})).digest('hex');
|
|
10
|
+
return Object.freeze({anchor:Object.freeze({schema:'deadbyte.r186-owner-anchor.v1',owner_key_id}),
|
|
11
|
+
public_key_pem:canonicalPem});
|
|
12
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// R186: only a read-only verifier for independently enrolled owner consent.
|
|
2
|
+
// A successful result MUST NOT be treated as an Autonomous grant. The caller
|
|
3
|
+
// must separately prove protected enrollment, single-use consumption and epoch.
|
|
4
|
+
import {createHash,createPublicKey} from 'node:crypto';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import {verifyProjectOwnerApproval} from './r70-owner-approval.mjs';
|
|
7
|
+
|
|
8
|
+
const SHA=/^[0-9a-f]{64}$/;
|
|
9
|
+
function exact(value,fields,label){
|
|
10
|
+
if(!value||typeof value!=='object'||Array.isArray(value)||
|
|
11
|
+
Object.keys(value).length!==fields.length||
|
|
12
|
+
fields.some(field=>!Object.hasOwn(value,field)))
|
|
13
|
+
throw new Error('R186 '+label+' invalid');
|
|
14
|
+
}
|
|
15
|
+
export async function verifyR186OwnerConsent({ownerTrust,ownerPublicKeyPem,approval,expected,now=new Date()}={}){
|
|
16
|
+
exact(ownerTrust,['schema','owner_key_id'],'owner anchor');
|
|
17
|
+
if(ownerTrust.schema!=='deadbyte.r186-owner-anchor.v1'||
|
|
18
|
+
typeof ownerTrust.owner_key_id!=='string'||!SHA.test(ownerTrust.owner_key_id))
|
|
19
|
+
throw new Error('R186 owner anchor schema or fingerprint invalid');
|
|
20
|
+
if(typeof ownerPublicKeyPem!=='string'||ownerPublicKeyPem.length>8192)
|
|
21
|
+
throw new Error('R186 owner anchor public key invalid');
|
|
22
|
+
let pub;
|
|
23
|
+
try{pub=createPublicKey(ownerPublicKeyPem);}catch{throw new Error('R186 owner anchor public key invalid');}
|
|
24
|
+
if(pub.asymmetricKeyType!=='ed25519')throw new Error('R186 owner anchor must be Ed25519');
|
|
25
|
+
const keySha=createHash('sha256').update(pub.export({type:'spki',format:'der'})).digest('hex');
|
|
26
|
+
if(ownerTrust.owner_key_id!==keySha)
|
|
27
|
+
throw new Error('R186 owner anchor public key mismatch');
|
|
28
|
+
const approved=await verifyProjectOwnerApproval({approval,ownerPublicKey:pub,expected,now});
|
|
29
|
+
return Object.freeze({status:'verified_readonly',authority_granted:false,
|
|
30
|
+
owner_key_id:keySha,subject_sha256:approved.subject_sha256,
|
|
31
|
+
root_id:approved.root_id,project_root:approved.project_root,
|
|
32
|
+
expires_at_utc:approved.expires_at_utc,
|
|
33
|
+
next_gate:'PROTECTED_OWNER_ENROLLMENT_AND_SINGLE_USE_EPOCH_ISSUANCE_REQUIRED'});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function verifyR186PolicyRootBinding({
|
|
37
|
+
codingPolicy,autonomousPolicy,rootId,projectRoot,codingPolicySha256
|
|
38
|
+
}={}){
|
|
39
|
+
if(!codingPolicy||typeof codingPolicy!=='object'||
|
|
40
|
+
codingPolicy.schema!=='deadbyte.coding-policy.v1'||codingPolicy.enabled!==true)
|
|
41
|
+
throw new Error('R186 coding policy schema invalid');
|
|
42
|
+
if(!autonomousPolicy||typeof autonomousPolicy!=='object'||
|
|
43
|
+
autonomousPolicy.schema!=='deadbyte.autonomous-policy.v1'||autonomousPolicy.enabled!==true)
|
|
44
|
+
throw new Error('R186 autonomous policy schema invalid');
|
|
45
|
+
if(typeof rootId!=='string'||!/^[A-Za-z0-9._-]{1,64}$/.test(rootId)||
|
|
46
|
+
!Array.isArray(autonomousPolicy.root_ids)||
|
|
47
|
+
autonomousPolicy.root_ids.length!==1||autonomousPolicy.root_ids[0]!==rootId)
|
|
48
|
+
throw new Error('R186 Autonomous root vector exceeds owner-reviewed scope');
|
|
49
|
+
if(typeof codingPolicySha256!=='string'||!SHA.test(codingPolicySha256)||
|
|
50
|
+
autonomousPolicy.coding_policy_sha256!==codingPolicySha256)
|
|
51
|
+
throw new Error('R186 coding-policy digest binding mismatch');
|
|
52
|
+
const bound=codingPolicy.roots?.[rootId]?.path;
|
|
53
|
+
if(typeof bound!=='string'||typeof projectRoot!=='string'||
|
|
54
|
+
!path.win32.isAbsolute(bound)||!path.win32.isAbsolute(projectRoot)||
|
|
55
|
+
bound.startsWith('\\\\')||projectRoot.startsWith('\\\\'))
|
|
56
|
+
throw new Error('R186 coding project root is not a real absolute drive path');
|
|
57
|
+
const actual=path.win32.normalize(bound).toLowerCase();
|
|
58
|
+
const requested=path.win32.normalize(projectRoot).toLowerCase();
|
|
59
|
+
if(actual!==requested||
|
|
60
|
+
actual===path.win32.parse(actual).root.toLowerCase())
|
|
61
|
+
throw new Error('R186 reviewed root must match selected Coding project root');
|
|
62
|
+
return Object.freeze({root_id:rootId,project_root:bound});
|
|
63
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// Single-use R186 Autonomous grant issuance. This module does not enroll owner
|
|
2
|
+
// keys and accepts no caller-selected authority scope: the policy, reviewed
|
|
3
|
+
// manifest and independently protected owner anchor are the complete subject.
|
|
4
|
+
import {createHash,createPrivateKey,createPublicKey,randomBytes,sign,verify} from 'node:crypto';
|
|
5
|
+
import {mkdir,open,readFile,writeFile} from 'node:fs/promises';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import {canonicalJson} from './canonical-json.mjs';
|
|
8
|
+
import {createAuthorityEpochStore} from './authority-continuity-r65.mjs';
|
|
9
|
+
import {verifyR186OwnerConsent,verifyR186PolicyRootBinding} from './r186-owner-consent-preflight.mjs';
|
|
10
|
+
|
|
11
|
+
const SHA=/^[0-9a-f]{64}$/;
|
|
12
|
+
const TERMINAL_DOMAIN=Buffer.from('DEADBYTE-R186-OWNER-GRANT-TERMINAL\0','utf8');
|
|
13
|
+
const sha=bytes=>createHash('sha256').update(bytes).digest('hex');
|
|
14
|
+
const bytes=value=>Buffer.from(canonicalJson(value),'utf8');
|
|
15
|
+
async function writeExclusive(file,data){
|
|
16
|
+
await mkdir(path.dirname(file),{recursive:true});
|
|
17
|
+
const handle=await open(file,'wx',0o600);
|
|
18
|
+
try{await handle.writeFile(data);await handle.sync();}
|
|
19
|
+
finally{await handle.close();}
|
|
20
|
+
}
|
|
21
|
+
function insist(condition,message){if(!condition)throw new Error(`R186 owner grant blocked: ${message}`);}
|
|
22
|
+
async function signTerminal(runtimeDir,body){
|
|
23
|
+
const trustDir=path.join(path.dirname(path.resolve(runtimeDir)),'trust');
|
|
24
|
+
const [privatePem,publicPem]=await Promise.all([
|
|
25
|
+
readFile(path.join(trustDir,'ed25519-private.pem')),readFile(path.join(trustDir,'ed25519-public.pem'))]);
|
|
26
|
+
const privateKey=createPrivateKey(privatePem),publicKey=createPublicKey(publicPem);
|
|
27
|
+
insist(privateKey.asymmetricKeyType==='ed25519'&&publicKey.asymmetricKeyType==='ed25519','machine receipt key type invalid');
|
|
28
|
+
const privateKeyId=sha(createPublicKey(privateKey).export({type:'spki',format:'der'}));
|
|
29
|
+
const publicKeyId=sha(publicKey.export({type:'spki',format:'der'}));
|
|
30
|
+
insist(privateKeyId===publicKeyId,'machine receipt key pair mismatch');
|
|
31
|
+
const signature=sign(null,Buffer.concat([TERMINAL_DOMAIN,bytes(body)]),privateKey);
|
|
32
|
+
insist(verify(null,Buffer.concat([TERMINAL_DOMAIN,bytes(body)]),publicKey,signature),'machine terminal signature self-check failed');
|
|
33
|
+
return Object.freeze({algorithm:'ed25519',domain:'DEADBYTE-R186-OWNER-GRANT-TERMINAL',key_id:publicKeyId,signature_base64:signature.toString('base64')});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function issueR186AutonomousGrant({
|
|
37
|
+
runtimeDir,grantFile,codingPolicy,codingPolicyBytes,autonomousPolicy,autonomousPolicyBytes,
|
|
38
|
+
releaseManifestBytes,ownerTrust,ownerPublicKeyPem,approval,now=new Date(),epochStore=null
|
|
39
|
+
}={}){
|
|
40
|
+
insist(typeof runtimeDir==='string'&&path.isAbsolute(runtimeDir),'runtime directory invalid');
|
|
41
|
+
insist(typeof grantFile==='string'&&path.isAbsolute(grantFile),'grant path invalid');
|
|
42
|
+
for(const [name,value] of [['Coding policy',codingPolicyBytes],['Autonomous policy',autonomousPolicyBytes],['release manifest',releaseManifestBytes]])
|
|
43
|
+
insist(Buffer.isBuffer(value)&&value.length>0,`${name} bytes invalid`);
|
|
44
|
+
insist(autonomousPolicy?.schema==='deadbyte.autonomous-policy.v1'&&autonomousPolicy.enabled===true,'Autonomous policy invalid');
|
|
45
|
+
insist(path.resolve(autonomousPolicy.lease_file)===path.resolve(grantFile),'policy grant path mismatch');
|
|
46
|
+
const runtimePrefix=path.resolve(runtimeDir)+path.sep;
|
|
47
|
+
insist(path.resolve(grantFile).startsWith(runtimePrefix),'grant path outside runtime');
|
|
48
|
+
const codingSha=sha(codingPolicyBytes),autonomousSha=sha(autonomousPolicyBytes),manifestSha=sha(releaseManifestBytes);
|
|
49
|
+
const rootId=Array.isArray(autonomousPolicy.root_ids)&&autonomousPolicy.root_ids.length===1?autonomousPolicy.root_ids[0]:null;
|
|
50
|
+
const projectRoot=rootId?codingPolicy?.roots?.[rootId]?.path:null;
|
|
51
|
+
const binding=verifyR186PolicyRootBinding({codingPolicy,autonomousPolicy,rootId,projectRoot,codingPolicySha256:codingSha});
|
|
52
|
+
const consent=await verifyR186OwnerConsent({ownerTrust,ownerPublicKeyPem,approval,now,expected:{
|
|
53
|
+
rootId:binding.root_id,projectRoot:binding.project_root,codingPolicySha256:codingSha,
|
|
54
|
+
autonomousPolicySha256:autonomousSha,reviewedArtifactSha256:manifestSha
|
|
55
|
+
}});
|
|
56
|
+
const evidenceDir=path.join(runtimeDir,'owner-consent','r186');
|
|
57
|
+
const claimPath=path.join(evidenceDir,`${consent.subject_sha256}.claim.json`);
|
|
58
|
+
const terminalPath=path.join(evidenceDir,`${consent.subject_sha256}.terminal.json`);
|
|
59
|
+
try{await readFile(terminalPath);throw new Error('R186 owner approval already consumed; replay denied');}
|
|
60
|
+
catch(error){if(error?.code!=='ENOENT')throw error;}
|
|
61
|
+
const nonce=randomBytes(32).toString('hex');
|
|
62
|
+
const claim={schema:'deadbyte.r186-owner-grant-claim.v1',subject_sha256:consent.subject_sha256,
|
|
63
|
+
owner_key_id:consent.owner_key_id,policy_sha256:autonomousSha,coding_policy_sha256:codingSha,
|
|
64
|
+
release_manifest_sha256:manifestSha,root_id:binding.root_id,project_root:binding.project_root,
|
|
65
|
+
grant_instance_nonce:nonce,claimed_at_utc:now.toISOString()};
|
|
66
|
+
try{await writeExclusive(claimPath,bytes(claim));}
|
|
67
|
+
catch(error){if(error?.code==='EEXIST')throw new Error('R186 owner approval already consumed or in progress; replay denied');throw error;}
|
|
68
|
+
const grant={schema:'deadbyte.autonomous-grant.v2',policy_sha256:autonomousSha,
|
|
69
|
+
coding_policy_sha256:codingSha,instance_nonce:nonce,root_ids:[binding.root_id],
|
|
70
|
+
granted_at_utc:now.toISOString(),expires_at_utc:consent.expires_at_utc};
|
|
71
|
+
const grantBytes=bytes(grant);
|
|
72
|
+
try{await writeExclusive(grantFile,grantBytes);}
|
|
73
|
+
catch(error){throw Object.assign(new Error(`R186 grant write failed after durable claim: ${error?.message??error}`),{outcome:'OUTCOME_UNKNOWN'});}
|
|
74
|
+
const store=epochStore??createAuthorityEpochStore({runtimeDir,policyBytes:autonomousPolicyBytes});
|
|
75
|
+
let head;
|
|
76
|
+
try{head=await store.append({plane:'autonomous',action:'grant',grantBytes,grantInstanceNonce:nonce,updateId:null});}
|
|
77
|
+
catch(error){throw Object.assign(new Error(`R186 epoch issuance failed after grant write: ${error?.message??error}`),{outcome:'OUTCOME_UNKNOWN'});}
|
|
78
|
+
insist(SHA.test(String(head?.event_sha256)),'epoch identity invalid after issuance');
|
|
79
|
+
const verified=await store.verifyGrant({plane:'autonomous',grantBytes,grantInstanceNonce:nonce,policyBytes:autonomousPolicyBytes});
|
|
80
|
+
insist(verified?.trusted===true&&verified.event_sha256===head.event_sha256,'grant epoch read-back failed');
|
|
81
|
+
insist((await readFile(grantFile)).equals(grantBytes),'grant changed after epoch issuance');
|
|
82
|
+
const terminalBody={schema:'deadbyte.r186-owner-grant-terminal.v1',status:'GRANTED',
|
|
83
|
+
subject_sha256:consent.subject_sha256,owner_key_id:consent.owner_key_id,
|
|
84
|
+
claim_sha256:sha(bytes(claim)),grant_sha256:sha(grantBytes),grant_instance_nonce:nonce,
|
|
85
|
+
policy_sha256:autonomousSha,coding_policy_sha256:codingSha,release_manifest_sha256:manifestSha,
|
|
86
|
+
root_id:binding.root_id,project_root:binding.project_root,epoch_event_sha256:head.event_sha256,
|
|
87
|
+
epoch_seq:head.seq,expires_at_utc:consent.expires_at_utc,completed_at_utc:new Date().toISOString()};
|
|
88
|
+
const attestation=await signTerminal(runtimeDir,terminalBody);
|
|
89
|
+
const unsigned={body:terminalBody,attestation};
|
|
90
|
+
const terminal={...unsigned,receipt_sha256:sha(bytes(unsigned))};
|
|
91
|
+
try{await writeExclusive(terminalPath,bytes(terminal));}
|
|
92
|
+
catch(error){throw Object.assign(new Error(`R186 terminal receipt write failed: ${error?.message??error}`),{outcome:'OUTCOME_UNKNOWN'});}
|
|
93
|
+
return Object.freeze({status:'GRANTED',authority_granted:true,subject_sha256:consent.subject_sha256,
|
|
94
|
+
grant_sha256:terminalBody.grant_sha256,epoch_event_sha256:head.event_sha256,epoch_seq:head.seq,
|
|
95
|
+
terminal_path:terminalPath,receipt_sha256:terminal.receipt_sha256});
|
|
96
|
+
}
|